fix: resolve story automator review findings
This commit is contained in:
parent
21d8da520b
commit
9ff25ae547
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -41,8 +41,8 @@ attempt_5_progress = {agent: primary, tasks: 5/9} # confirmed plateau
|
||||||
| 3 | FAILURE, plateau at same task (any agent) | Continue to attempt 4 (confirm with other agent) |
|
| 3 | FAILURE, plateau at same task (any agent) | Continue to attempt 4 (confirm with other agent) |
|
||||||
| 4 | FAILURE, plateau confirmed across agents | **DEFER** story (complexity/context limit hit) |
|
| 4 | FAILURE, plateau confirmed across agents | **DEFER** story (complexity/context limit hit) |
|
||||||
| 4 | FAILURE, variable progress | One more retry with extended timeout |
|
| 4 | FAILURE, variable progress | One more retry with extended timeout |
|
||||||
| 5 | FAILURE, plateau confirmed | **DEFER** story |
|
|
||||||
| 5 | FAILURE, zero progress all attempts | **ESCALATE** (likely API/connection issue) |
|
| 5 | FAILURE, zero progress all attempts | **ESCALATE** (likely API/connection issue) |
|
||||||
|
| 5 | FAILURE, plateau confirmed (and progress > 0) | **DEFER** story |
|
||||||
| 5 | FAILURE, variable but incomplete | **ESCALATE** (all retries exhausted) |
|
| 5 | FAILURE, variable but incomplete | **ESCALATE** (all retries exhausted) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -64,12 +64,13 @@ session=$("$scripts" tmux-wrapper spawn dev {epic} {story_id} \
|
||||||
|
|
||||||
## Codex Monitoring Notes
|
## Codex Monitoring Notes
|
||||||
|
|
||||||
- **No todo checkboxes:** Codex doesn't use ☒/☐ - `todos_done` and `todos_total` will be 0
|
- **No native todo checkboxes:** Codex doesn't emit ☒/☐, so active sessions report `0/0`
|
||||||
- **Longer waits:** Status check script returns 90s wait estimate for Codex (vs 60s for Claude)
|
- **Longer waits:** Status check script returns 90s wait estimate for Codex (vs 60s for Claude)
|
||||||
- **Different activity detection:** Uses output freshness + heartbeat (no marker reliance)
|
- **Different activity detection:** Uses output freshness + heartbeat (no marker reliance)
|
||||||
- **Output staleness window:** `CODEX_OUTPUT_STALE_SECONDS` (default: 300)
|
- **Output staleness window:** `CODEX_OUTPUT_STALE_SECONDS` (default: 300)
|
||||||
- **1.5x timeout multiplier:** `story-automator monitor-session` applies 1.5x multiplier when `--agent codex`
|
- **1.5x timeout multiplier:** `story-automator monitor-session` applies 1.5x multiplier when `--agent codex`
|
||||||
- **Fake todo progress (v2.2):** When Codex is idle after activity, reports `1/1` to indicate "work done, needs verification"
|
- **Output-fresh idle normalization:** If heartbeat is idle but output is still fresh, Codex reports `0/1` while work is still considered in progress
|
||||||
|
- **Completion normalization (v2.2):** When Codex finishes or prompt returns after activity, it reports `1/1` to indicate "work done, needs verification"
|
||||||
- **Idle vs Completed (v2.2):** Codex sessions report "idle" instead of "completed" when CLI stops but no terminal markers
|
- **Idle vs Completed (v2.2):** Codex sessions report "idle" instead of "completed" when CLI stops but no terminal markers
|
||||||
|
|
||||||
## ⚠️ Codex Code-Review Limitations (v1.5.0)
|
## ⚠️ Codex Code-Review Limitations (v1.5.0)
|
||||||
|
|
|
||||||
|
|
@ -101,13 +101,13 @@ is_done=$(echo "$status" | jq -r '.done')
|
||||||
**IF final_state == "incomplete":** (v2.2 - Codex-specific)
|
**IF final_state == "incomplete":** (v2.2 - Codex-specific)
|
||||||
- Session idle but sprint-status NOT updated
|
- Session idle but sprint-status NOT updated
|
||||||
- Cleanup: `"$scripts" tmux-wrapper kill "$session_name"`
|
- Cleanup: `"$scripts" tmux-wrapper kill "$session_name"`
|
||||||
- Count this as a failed attempt and **retry** until `reviewCycle == maxCycles`
|
- Increment `reviewCycle`
|
||||||
- **After maxCycles exhausted:** Escalate with CRITICAL priority (Trigger #8)
|
- If `reviewCycle <= maxCycles`: count this as a failed attempt and **CONTINUE** with a retry
|
||||||
- Present options:
|
- If `reviewCycle > maxCycles`: Escalate with CRITICAL priority (Trigger #8), then present options:
|
||||||
1. **[1] Manual Fix** - Update sprint-status.yaml yourself
|
1. **[1] Manual Fix** - Update sprint-status.yaml yourself
|
||||||
2. **[2] Run with Claude** - Re-run code-review with Claude agent
|
2. **[2] Run with Claude** - Re-run code-review with Claude agent
|
||||||
3. **[3] Skip Story** - Mark story as skipped and continue
|
3. **[3] Skip Story** - Mark story as skipped and continue
|
||||||
- **HALT** — wait for user choice
|
- **HALT** — wait for user choice only after maxCycles is exhausted
|
||||||
|
|
||||||
**IF final_state == "crashed" or "stuck":**
|
**IF final_state == "crashed" or "stuck":**
|
||||||
- Log "Review session failed: $final_state"
|
- Log "Review session failed: $final_state"
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@ tests_pass=$([[ "$test_result" != *"FAIL"* ]] && echo "true" || echo "false")
|
||||||
|
|
||||||
Story: {story_name}
|
Story: {story_name}
|
||||||
Story ID: {story_id}
|
Story ID: {story_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2. Cannot Parse Session Output
|
## 2. Cannot Parse Session Output
|
||||||
|
|
||||||
|
|
@ -64,7 +67,7 @@ Story: {story_name}
|
||||||
Step: {step_name}
|
Step: {step_name}
|
||||||
Error: {error_message}
|
Error: {error_message}
|
||||||
|
|
||||||
Unable to spawn T-Mux session after retry.
|
Unable to spawn tmux session after retry.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
[1] Retry again
|
[1] Retry again
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@
|
||||||
See:
|
See:
|
||||||
- `escalation-messages-core.md` (Triggers 1-4)
|
- `escalation-messages-core.md` (Triggers 1-4)
|
||||||
- `escalation-messages-extended.md` (Triggers 5-7)
|
- `escalation-messages-extended.md` (Triggers 5-7)
|
||||||
|
- Trigger 8 (`final_state: "incomplete"` after `maxCycles`) has no shared template; use the workflow-specific escalation in `code-review-loop.md`
|
||||||
|
|
|
||||||
|
|
@ -22,15 +22,15 @@ When `story-automator monitor-session` fails or background monitoring task dies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# STEP 1: Check if tmux session still exists
|
# STEP 1: Check if tmux session still exists
|
||||||
sessions=$(tmux list-sessions 2>/dev/null | grep "sa-.*{story_pattern}" || true)
|
sessions=$(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "sa-.*{story_pattern}" || true)
|
||||||
|
|
||||||
# STEP 2: If session exists, check its status directly
|
# STEP 2: If session exists, check its status directly
|
||||||
if [ -n "$sessions" ]; then
|
if [ -n "$sessions" ]; then
|
||||||
for session in $sessions; do
|
while IFS= read -r session; do
|
||||||
status=$("$scripts" tmux-status-check "$session")
|
status=$("$scripts" tmux-status-check "$session")
|
||||||
session_state=$(echo "$status" | cut -d',' -f6)
|
session_state=$(echo "$status" | cut -d',' -f6)
|
||||||
# Act based on direct status
|
# Act based on direct status
|
||||||
done
|
done <<< "$sessions"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# STEP 3: ALWAYS verify source of truth regardless of session status
|
# STEP 3: ALWAYS verify source of truth regardless of session status
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ story-automator monitor-session <session_name> [options]
|
||||||
# --json Output as JSON instead of CSV
|
# --json Output as JSON instead of CSV
|
||||||
|
|
||||||
# Output (JSON):
|
# Output (JSON):
|
||||||
# {"final_state":"completed|crashed|stuck|timeout","output_file":"/tmp/...","exit_reason":"..."}
|
# {"final_state":"completed|crashed|stuck|timeout|incomplete|not_found","output_file":"/tmp/...","exit_reason":"..."}
|
||||||
```
|
```
|
||||||
|
|
||||||
### story-automator orchestrator-helper
|
### story-automator orchestrator-helper
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ Before escalating, check if story is blocking:
|
||||||
- Decision tree for poll results
|
- Decision tree for poll results
|
||||||
- Polling loop with state tracking
|
- Polling loop with state tracking
|
||||||
|
|
||||||
- **Output Parsing:** See `data/monitoring-pattern.md` (Sub-Agent Invocation section)
|
- **Output Parsing:** See `data/monitoring-pattern-parsing.md` (Sub-Agent Invocation section)
|
||||||
- NEVER parse output yourself
|
- NEVER parse output yourself
|
||||||
- ALWAYS use sub-agents (Task tool, haiku)
|
- ALWAYS use sub-agents (Task tool, haiku)
|
||||||
- Verification checkpoint before proceeding
|
- Verification checkpoint before proceeding
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,10 @@ resolve_agent_for_task() {
|
||||||
primary_agent=$(echo "$result" | jq -r '.primary')
|
primary_agent=$(echo "$result" | jq -r '.primary')
|
||||||
fallback_agent=$(echo "$result" | jq -r '.fallback')
|
fallback_agent=$(echo "$result" | jq -r '.fallback')
|
||||||
|
|
||||||
# Handle "false"/null meaning disabled
|
# Normalize disabled fallback states
|
||||||
[ "$fallback_agent" = "false" ] && fallback_agent=""
|
if [ -z "$fallback_agent" ] || [ "$fallback_agent" = "false" ] || [ "$fallback_agent" = "null" ] || [ "$fallback_agent" = "$primary_agent" ]; then
|
||||||
|
fallback_agent=""
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Usage:
|
# Usage:
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ session=$("$scripts" tmux-wrapper spawn {type} {epic} {story_id} \
|
||||||
result=$("$scripts" monitor-session "$session" --json --agent "$agent")
|
result=$("$scripts" monitor-session "$session" --json --agent "$agent")
|
||||||
|
|
||||||
# Parse output
|
# Parse output
|
||||||
parsed=$("$scripts" orchestrator-helper parse-output "$(echo $result | jq -r '.output_file')" {type})
|
parsed=$("$scripts" orchestrator-helper parse-output "$(printf '%s' "$result" | jq -r '.output_file')" {type})
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
"$scripts" tmux-wrapper kill "$session"
|
"$scripts" tmux-wrapper kill "$session"
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ The status check script automatically detects Claude vs Codex sessions:
|
||||||
|
|
||||||
**For full output (when completed/stuck):**
|
**For full output (when completed/stuck):**
|
||||||
```bash
|
```bash
|
||||||
./bin/story-automator tmux-status-check "SESSION_NAME" --full
|
{project_root}/_bmad/bmm/4-implementation/bmad-story-automator-go/bin/story-automator tmux-status-check "SESSION_NAME" --full
|
||||||
```
|
```
|
||||||
Returns: `idle,0,0,/tmp/sa-output-SESSION_NAME.txt,0,completed`
|
Returns: `idle,0,0,/tmp/sa-output-SESSION_NAME.txt,0,completed`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,9 +116,9 @@ tmux send-keys -t "SESSION" 'claude --dangerously-skip-permissions "bmad-create-
|
||||||
codex exec "Execute the BMAD create-story workflow for story STORY_ID.
|
codex exec "Execute the BMAD create-story workflow for story STORY_ID.
|
||||||
|
|
||||||
Workflow location: _bmad/bmm/4-implementation/bmad-create-story/
|
Workflow location: _bmad/bmm/4-implementation/bmad-create-story/
|
||||||
- Read workflow.yaml for the process
|
- Follow workflow.md for the process
|
||||||
- Use template.md as the output template
|
- Use template.md as the output template
|
||||||
- Follow instructions.xml for detailed steps
|
- Reference checklist.md for validation steps
|
||||||
|
|
||||||
Create story file at: _bmad-output/implementation-artifacts/STORY_PREFIX-*.md
|
Create story file at: _bmad-output/implementation-artifacts/STORY_PREFIX-*.md
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,15 @@ func cmdEnsureMarkerGitignore(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(content, entry) {
|
for _, line := range strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n") {
|
||||||
writeJSON(map[string]any{"ok": true, "changed": false, "path": gitignorePath})
|
trimmed := strings.TrimSpace(line)
|
||||||
return 0
|
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if trimmed == entry {
|
||||||
|
writeJSON(map[string]any{"ok": true, "changed": false, "path": gitignorePath})
|
||||||
|
return 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_WRONLY, 0o644)
|
f, err := os.OpenFile(gitignorePath, os.O_APPEND|os.O_WRONLY, 0o644)
|
||||||
|
|
@ -90,7 +96,11 @@ func cmdEnsureMarkerGitignore(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
if _, err := f.WriteString(entry + "\n"); err != nil {
|
prefix := ""
|
||||||
|
if content != "" && !strings.HasSuffix(content, "\n") {
|
||||||
|
prefix = "\n"
|
||||||
|
}
|
||||||
|
if _, err := f.WriteString(prefix + entry + "\n"); err != nil {
|
||||||
writeJSON(map[string]any{"ok": false, "error": "append_failed"})
|
writeJSON(map[string]any{"ok": false, "error": "append_failed"})
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +212,7 @@ func cmdEnsureStopHook(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
exists := false
|
exists := false
|
||||||
needsPathUpdate := false
|
needsUpdate := false
|
||||||
for _, entry := range stopHooks {
|
for _, entry := range stopHooks {
|
||||||
entryMap, ok := entry.(map[string]any)
|
entryMap, ok := entry.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -217,16 +227,26 @@ func cmdEnsureStopHook(args []string) int {
|
||||||
if cmd, ok := m["command"].(string); ok {
|
if cmd, ok := m["command"].(string); ok {
|
||||||
if cmd == commandPath {
|
if cmd == commandPath {
|
||||||
exists = true
|
exists = true
|
||||||
break
|
} else if strings.Contains(cmd, "story-automator") && strings.Contains(cmd, "stop-hook") {
|
||||||
}
|
|
||||||
// Flexible match: any command referencing story-automator stop-hook
|
|
||||||
// regardless of path format (relative, absolute, project-relative).
|
|
||||||
if strings.Contains(cmd, "story-automator") && strings.Contains(cmd, "stop-hook") {
|
|
||||||
exists = true
|
exists = true
|
||||||
if cmd != commandPath {
|
m["command"] = commandPath
|
||||||
// Migrate stale path to resolved absolute path in-place.
|
needsUpdate = true
|
||||||
m["command"] = commandPath
|
}
|
||||||
needsPathUpdate = true
|
if exists {
|
||||||
|
switch current := m["timeout"].(type) {
|
||||||
|
case float64:
|
||||||
|
if int(current) != timeout {
|
||||||
|
m["timeout"] = timeout
|
||||||
|
needsUpdate = true
|
||||||
|
}
|
||||||
|
case int:
|
||||||
|
if current != timeout {
|
||||||
|
m["timeout"] = timeout
|
||||||
|
needsUpdate = true
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
m["timeout"] = timeout
|
||||||
|
needsUpdate = true
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -237,21 +257,18 @@ func cmdEnsureStopHook(args []string) int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if exists && !needsPathUpdate {
|
if exists && !needsUpdate {
|
||||||
writeJSON(map[string]any{"ok": true, "changed": false, "reason": "already_configured", "path": settingsPath})
|
writeJSON(map[string]any{"ok": true, "changed": false, "reason": "already_configured", "path": settingsPath})
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if exists && needsPathUpdate {
|
if exists && needsUpdate {
|
||||||
// Path normalized to absolute — write updated settings.
|
|
||||||
// Return changed:false because the hook functionally existed;
|
|
||||||
// no session restart is needed.
|
|
||||||
b, _ := json.MarshalIndent(root, "", " ")
|
b, _ := json.MarshalIndent(root, "", " ")
|
||||||
if err := writeFileAtomic(settingsPath, b); err != nil {
|
if err := writeFileAtomic(settingsPath, b); err != nil {
|
||||||
writeJSON(map[string]any{"ok": false, "error": "write_failed", "path": settingsPath})
|
writeJSON(map[string]any{"ok": false, "error": "write_failed", "path": settingsPath})
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
writeJSON(map[string]any{"ok": true, "changed": false, "reason": "path_normalized", "path": settingsPath})
|
writeJSON(map[string]any{"ok": true, "changed": false, "reason": "hook_normalized", "path": settingsPath})
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,19 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
usage()
|
printUsage(os.Stderr)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
if isHelpFlag(os.Args[1]) {
|
||||||
|
printUsage(os.Stdout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cmd := os.Args[1]
|
cmd := os.Args[1]
|
||||||
args := os.Args[2:]
|
args := os.Args[2:]
|
||||||
|
|
@ -62,37 +67,37 @@ func main() {
|
||||||
code = cmdAgentConfig(args)
|
code = cmdAgentConfig(args)
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", cmd)
|
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", cmd)
|
||||||
usage()
|
printUsage(os.Stderr)
|
||||||
code = 1
|
code = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(code)
|
os.Exit(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func usage() {
|
func printUsage(w io.Writer) {
|
||||||
fmt.Fprintln(os.Stderr, "story-automator <command> [args]")
|
fmt.Fprintln(w, "story-automator <command> [args]")
|
||||||
fmt.Fprintln(os.Stderr, "")
|
fmt.Fprintln(w, "")
|
||||||
fmt.Fprintln(os.Stderr, "Commands:")
|
fmt.Fprintln(w, "Commands:")
|
||||||
fmt.Fprintln(os.Stderr, " derive-project-slug")
|
fmt.Fprintln(w, " derive-project-slug")
|
||||||
fmt.Fprintln(os.Stderr, " ensure-marker-gitignore")
|
fmt.Fprintln(w, " ensure-marker-gitignore")
|
||||||
fmt.Fprintln(os.Stderr, " ensure-stop-hook")
|
fmt.Fprintln(w, " ensure-stop-hook")
|
||||||
fmt.Fprintln(os.Stderr, " stop-hook")
|
fmt.Fprintln(w, " stop-hook")
|
||||||
fmt.Fprintln(os.Stderr, " build-state-doc")
|
fmt.Fprintln(w, " build-state-doc")
|
||||||
fmt.Fprintln(os.Stderr, " commit-story")
|
fmt.Fprintln(w, " commit-story")
|
||||||
fmt.Fprintln(os.Stderr, " parse-epic")
|
fmt.Fprintln(w, " parse-epic")
|
||||||
fmt.Fprintln(os.Stderr, " parse-story")
|
fmt.Fprintln(w, " parse-story")
|
||||||
fmt.Fprintln(os.Stderr, " parse-story-range")
|
fmt.Fprintln(w, " parse-story-range")
|
||||||
fmt.Fprintln(os.Stderr, " epic-complete")
|
fmt.Fprintln(w, " epic-complete")
|
||||||
fmt.Fprintln(os.Stderr, " sprint-compare")
|
fmt.Fprintln(w, " sprint-compare")
|
||||||
fmt.Fprintln(os.Stderr, " state-metrics")
|
fmt.Fprintln(w, " state-metrics")
|
||||||
fmt.Fprintln(os.Stderr, " validate-state")
|
fmt.Fprintln(w, " validate-state")
|
||||||
fmt.Fprintln(os.Stderr, " validate-story-creation")
|
fmt.Fprintln(w, " validate-story-creation")
|
||||||
fmt.Fprintln(os.Stderr, " list-sessions")
|
fmt.Fprintln(w, " list-sessions")
|
||||||
fmt.Fprintln(os.Stderr, " tmux-wrapper")
|
fmt.Fprintln(w, " tmux-wrapper")
|
||||||
fmt.Fprintln(os.Stderr, " heartbeat-check")
|
fmt.Fprintln(w, " heartbeat-check")
|
||||||
fmt.Fprintln(os.Stderr, " codex-status-check")
|
fmt.Fprintln(w, " codex-status-check")
|
||||||
fmt.Fprintln(os.Stderr, " tmux-status-check")
|
fmt.Fprintln(w, " tmux-status-check")
|
||||||
fmt.Fprintln(os.Stderr, " monitor-session")
|
fmt.Fprintln(w, " monitor-session")
|
||||||
fmt.Fprintln(os.Stderr, " orchestrator-helper")
|
fmt.Fprintln(w, " orchestrator-helper")
|
||||||
fmt.Fprintln(os.Stderr, " agent-config")
|
fmt.Fprintln(w, " agent-config")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -200,12 +200,12 @@ func orchestratorAgentsResolve(args []string) int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if stateFile == "" || storyID == "" || task == "" {
|
if storyID == "" || task == "" || (stateFile == "" && agentsPath == "") {
|
||||||
writeJSON(map[string]any{"ok": false, "error": "missing_args"})
|
writeJSON(map[string]any{"ok": false, "error": "missing_args"})
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if agentsPath == "" {
|
if agentsPath == "" && stateFile != "" {
|
||||||
agentsPath = findFrontmatterValue(stateFile, "agentsFile")
|
agentsPath = findFrontmatterValue(stateFile, "agentsFile")
|
||||||
}
|
}
|
||||||
if agentsPath == "" || !fileExists(agentsPath) {
|
if agentsPath == "" || !fileExists(agentsPath) {
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,16 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func cmdOrchestratorHelper(args []string) int {
|
func cmdOrchestratorHelper(args []string) int {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return orchestratorUsage()
|
return orchestratorUsage(os.Stderr, 1)
|
||||||
|
}
|
||||||
|
if isHelpFlag(args[0]) {
|
||||||
|
return orchestratorUsage(os.Stdout, 0)
|
||||||
}
|
}
|
||||||
action := args[0]
|
action := args[0]
|
||||||
args = args[1:]
|
args = args[1:]
|
||||||
|
|
@ -50,36 +54,36 @@ func cmdOrchestratorHelper(args []string) int {
|
||||||
case "agents-resolve":
|
case "agents-resolve":
|
||||||
return orchestratorAgentsResolve(args)
|
return orchestratorAgentsResolve(args)
|
||||||
default:
|
default:
|
||||||
return orchestratorUsage()
|
return orchestratorUsage(os.Stderr, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func orchestratorUsage() int {
|
func orchestratorUsage(w io.Writer, code int) int {
|
||||||
fmt.Fprintln(os.Stderr, "Usage: orchestrator-helper <action> [args]")
|
fmt.Fprintln(w, "Usage: orchestrator-helper <action> [args]")
|
||||||
fmt.Fprintln(os.Stderr, "")
|
fmt.Fprintln(w, "")
|
||||||
fmt.Fprintln(os.Stderr, "Actions:")
|
fmt.Fprintln(w, "Actions:")
|
||||||
fmt.Fprintln(os.Stderr, " sprint-status get <story_key>")
|
fmt.Fprintln(w, " sprint-status get <story_key>")
|
||||||
fmt.Fprintln(os.Stderr, " sprint-status exists")
|
fmt.Fprintln(w, " sprint-status exists")
|
||||||
fmt.Fprintln(os.Stderr, " sprint-status check-epic <epic>")
|
fmt.Fprintln(w, " sprint-status check-epic <epic>")
|
||||||
fmt.Fprintln(os.Stderr, " parse-output <file> <step>")
|
fmt.Fprintln(w, " parse-output <file> <step>")
|
||||||
fmt.Fprintln(os.Stderr, " marker create --epic E --story S --remaining N --state-file F")
|
fmt.Fprintln(w, " marker create --epic E --story S --remaining N --state-file F")
|
||||||
fmt.Fprintln(os.Stderr, " marker remove")
|
fmt.Fprintln(w, " marker remove")
|
||||||
fmt.Fprintln(os.Stderr, " marker check")
|
fmt.Fprintln(w, " marker check")
|
||||||
fmt.Fprintln(os.Stderr, " marker heartbeat")
|
fmt.Fprintln(w, " marker heartbeat")
|
||||||
fmt.Fprintln(os.Stderr, " state-list <folder>")
|
fmt.Fprintln(w, " state-list <folder>")
|
||||||
fmt.Fprintln(os.Stderr, " state-latest <folder> [status]")
|
fmt.Fprintln(w, " state-latest <folder> [status]")
|
||||||
fmt.Fprintln(os.Stderr, " state-latest-incomplete <folder>")
|
fmt.Fprintln(w, " state-latest-incomplete <folder>")
|
||||||
fmt.Fprintln(os.Stderr, " state-summary <file>")
|
fmt.Fprintln(w, " state-summary <file>")
|
||||||
fmt.Fprintln(os.Stderr, " state-update <file> --set k=v")
|
fmt.Fprintln(w, " state-update <file> --set k=v")
|
||||||
fmt.Fprintln(os.Stderr, " escalate <trigger> <context>")
|
fmt.Fprintln(w, " escalate <trigger> <context>")
|
||||||
fmt.Fprintln(os.Stderr, " commit-ready <story_id>")
|
fmt.Fprintln(w, " commit-ready <story_id>")
|
||||||
fmt.Fprintln(os.Stderr, " normalize-key <input> [--to id|key|prefix|json]")
|
fmt.Fprintln(w, " normalize-key <input> [--to id|key|prefix|json]")
|
||||||
fmt.Fprintln(os.Stderr, " story-file-status <story>")
|
fmt.Fprintln(w, " story-file-status <story>")
|
||||||
fmt.Fprintln(os.Stderr, " verify-code-review <story>")
|
fmt.Fprintln(w, " verify-code-review <story>")
|
||||||
fmt.Fprintln(os.Stderr, " check-epic-complete <epic> <story> [--state-file path]")
|
fmt.Fprintln(w, " check-epic-complete <epic> <story> [--state-file path]")
|
||||||
fmt.Fprintln(os.Stderr, " get-epic-stories <epic> [--state-file path]")
|
fmt.Fprintln(w, " get-epic-stories <epic> [--state-file path]")
|
||||||
fmt.Fprintln(os.Stderr, " check-blocking <story_id>")
|
fmt.Fprintln(w, " check-blocking <story_id>")
|
||||||
fmt.Fprintln(os.Stderr, " agents-build --state-file path --complexity-file path --output path --config-json '{}'")
|
fmt.Fprintln(w, " agents-build --state-file path --complexity-file path --output path --config-json '{}'")
|
||||||
fmt.Fprintln(os.Stderr, " agents-resolve --state-file path --story ID --task create|dev|auto|review [--agents-file path]")
|
fmt.Fprintln(w, " agents-resolve --state-file path --story ID --task create|dev|auto|review [--agents-file path]")
|
||||||
return 1
|
return code
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -146,10 +146,9 @@ func orchestratorCheckEpicComplete(args []string) int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
epicsDir := filepath.Join(getProjectRoot(), "_bmad-output", "implementation-artifacts")
|
epicFile := findEpicFile(getProjectRoot(), epicNumber)
|
||||||
matches, _ := filepath.Glob(filepath.Join(epicsDir, fmt.Sprintf("epic-%s-*.md", epicNumber)))
|
if epicFile != "" {
|
||||||
if len(matches) > 0 {
|
content, _ := readFile(epicFile)
|
||||||
content, _ := readFile(matches[0])
|
|
||||||
re := regexp.MustCompile(regexp.QuoteMeta(epicNumber) + `\.[0-9]+`)
|
re := regexp.MustCompile(regexp.QuoteMeta(epicNumber) + `\.[0-9]+`)
|
||||||
ids := re.FindAllString(content, -1)
|
ids := re.FindAllString(content, -1)
|
||||||
ids = uniqueSortedStories(ids)
|
ids = uniqueSortedStories(ids)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -69,20 +70,53 @@ func orchestratorMarker(args []string) int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ensureDir(filepath.Dir(markerFile))
|
|
||||||
|
|
||||||
if heartbeat == "" {
|
if heartbeat == "" {
|
||||||
heartbeat = nowUTC().Format("2006-01-02T15:04:05Z")
|
heartbeat = nowUTC().Format("2006-01-02T15:04:05Z")
|
||||||
}
|
}
|
||||||
|
|
||||||
payload := fmt.Sprintf("{\n \"epic\": %q,\n \"currentStory\": %q,\n \"storiesRemaining\": %s,\n \"stateFile\": %q,\n \"createdAt\": %q,\n \"heartbeat\": %q,\n \"pid\": %s,\n \"projectSlug\": %q\n}\n",
|
if err := ensureDir(filepath.Dir(markerFile)); err != nil {
|
||||||
epic, story, remaining, stateFile, nowUTC().Format("2006-01-02T15:04:05Z"), heartbeat, pid, projectSlug)
|
fmt.Fprintf(os.Stderr, "Failed to create marker directory %s: %v\n", filepath.Dir(markerFile), err)
|
||||||
_ = os.WriteFile(markerFile, []byte(payload), 0o644)
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingNum, err := strconv.Atoi(remaining)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Invalid --remaining value %q\n", remaining)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
pidNum, err := strconv.Atoi(pid)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Invalid --pid value %q\n", pid)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
payloadObj := map[string]any{
|
||||||
|
"epic": epic,
|
||||||
|
"currentStory": story,
|
||||||
|
"storiesRemaining": remainingNum,
|
||||||
|
"stateFile": stateFile,
|
||||||
|
"createdAt": nowUTC().Format("2006-01-02T15:04:05Z"),
|
||||||
|
"heartbeat": heartbeat,
|
||||||
|
"pid": pidNum,
|
||||||
|
"projectSlug": projectSlug,
|
||||||
|
}
|
||||||
|
payload, err := json.MarshalIndent(payloadObj, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to serialize marker %s: %v\n", markerFile, err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if err := writeFileAtomic(markerFile, append(payload, '\n')); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to write marker %s: %v\n", markerFile, err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
fmt.Printf("Marker created: %s\n", markerFile)
|
fmt.Printf("Marker created: %s\n", markerFile)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
case "remove":
|
case "remove":
|
||||||
_ = os.Remove(markerFile)
|
if err := os.Remove(markerFile); err != nil && !os.IsNotExist(err) {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to remove marker %s: %v\n", markerFile, err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
fmt.Println("Marker removed")
|
fmt.Println("Marker removed")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
@ -110,8 +144,21 @@ func orchestratorMarker(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
newHeartbeat := nowUTC().Format("2006-01-02T15:04:05Z")
|
newHeartbeat := nowUTC().Format("2006-01-02T15:04:05Z")
|
||||||
updated := regexp.MustCompile(`"heartbeat":.*$`).ReplaceAllString(content, fmt.Sprintf("\"heartbeat\": \"%s\"", newHeartbeat))
|
var marker map[string]any
|
||||||
_ = os.WriteFile(markerFile, []byte(updated), 0o644)
|
if err := json.Unmarshal([]byte(content), &marker); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Marker file is not valid JSON: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
marker["heartbeat"] = newHeartbeat
|
||||||
|
updated, err := json.MarshalIndent(marker, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to serialize marker %s: %v\n", markerFile, err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if err := writeFileAtomic(markerFile, append(updated, '\n')); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to write marker %s: %v\n", markerFile, err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
fmt.Printf("Heartbeat updated: %s\n", newHeartbeat)
|
fmt.Printf("Heartbeat updated: %s\n", newHeartbeat)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const parseOutputTimeout = 2 * time.Minute
|
||||||
|
|
||||||
func orchestratorParseOutput(args []string) int {
|
func orchestratorParseOutput(args []string) int {
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
fmt.Println("{\"status\":\"error\",\"reason\":\"output file not found or empty\"}")
|
fmt.Println("{\"status\":\"error\",\"reason\":\"output file not found or empty\"}")
|
||||||
|
|
@ -36,7 +41,10 @@ func orchestratorParseOutput(args []string) int {
|
||||||
|
|
||||||
prompt := buildParsePrompt(stepType, content)
|
prompt := buildParsePrompt(stepType, content)
|
||||||
|
|
||||||
cmd := exec.Command("claude", "-p", "--model", "haiku", prompt)
|
ctx, cancel := context.WithTimeout(context.Background(), parseOutputTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "claude", "-p", "--model", "haiku", prompt)
|
||||||
env := []string{}
|
env := []string{}
|
||||||
for _, e := range os.Environ() {
|
for _, e := range os.Environ() {
|
||||||
if !strings.HasPrefix(e, "CLAUDECODE=") {
|
if !strings.HasPrefix(e, "CLAUDECODE=") {
|
||||||
|
|
@ -46,6 +54,10 @@ func orchestratorParseOutput(args []string) int {
|
||||||
cmd.Env = append(env, "STORY_AUTOMATOR_CHILD=true")
|
cmd.Env = append(env, "STORY_AUTOMATOR_CHILD=true")
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
fmt.Println("{\"status\":\"error\",\"reason\":\"sub-agent call timed out\"}")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
fmt.Println("{\"status\":\"error\",\"reason\":\"sub-agent call failed\"}")
|
fmt.Println("{\"status\":\"error\",\"reason\":\"sub-agent call failed\"}")
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
@ -89,8 +101,11 @@ func extractJSONLine(result string) string {
|
||||||
lines := trimLines(result)
|
lines := trimLines(result)
|
||||||
jsonRe := regexp.MustCompile(`\{.*\}`)
|
jsonRe := regexp.MustCompile(`\{.*\}`)
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
if jsonRe.MatchString(line) {
|
for _, match := range jsonRe.FindAllString(line, -1) {
|
||||||
return jsonRe.FindString(line)
|
match = strings.TrimSpace(match)
|
||||||
|
if json.Valid([]byte(match)) {
|
||||||
|
return match
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
|
|
|
||||||
|
|
@ -152,9 +152,9 @@ func sprintStatusEpic(statusFile, epic string) ([]string, int) {
|
||||||
if !seen[key] {
|
if !seen[key] {
|
||||||
stories = append(stories, key)
|
stories = append(stories, key)
|
||||||
seen[key] = true
|
seen[key] = true
|
||||||
}
|
if len(status) > 0 && status[0] == "done" {
|
||||||
if len(status) > 0 && status[0] == "done" {
|
doneCount++
|
||||||
doneCount++
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -169,16 +169,27 @@ func orchestratorStateUpdate(args []string) int {
|
||||||
}
|
}
|
||||||
key := parts[0]
|
key := parts[0]
|
||||||
val := parts[1]
|
val := parts[1]
|
||||||
|
replaced := false
|
||||||
for idx, line := range lines {
|
for idx, line := range lines {
|
||||||
if strings.HasPrefix(line, key+":") {
|
if strings.HasPrefix(line, key+":") {
|
||||||
lines[idx] = fmt.Sprintf("%s: %s", key, val)
|
lines[idx] = fmt.Sprintf("%s: %s", key, val)
|
||||||
|
replaced = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updatedKeys = append(updatedKeys, key)
|
if replaced {
|
||||||
|
updatedKeys = append(updatedKeys, key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = os.WriteFile(file, []byte(strings.Join(lines, "\n")), 0o644)
|
if len(updatedKeys) == 0 {
|
||||||
|
writeJSON(map[string]any{"ok": false, "error": "keys_not_found", "updated": updatedKeys})
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(file, []byte(strings.Join(lines, "\n")), 0o644); err != nil {
|
||||||
|
writeJSON(map[string]any{"ok": false, "error": "write_failed", "updated": []string{}})
|
||||||
|
return 1
|
||||||
|
}
|
||||||
writeJSON(map[string]any{"ok": true, "updated": updatedKeys})
|
writeJSON(map[string]any{"ok": true, "updated": updatedKeys})
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
@ -199,14 +210,19 @@ func readStoryRangeFromState(stateFile string) []string {
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
if strings.HasPrefix(strings.TrimSpace(line), "storyRange:") {
|
if strings.HasPrefix(strings.TrimSpace(line), "storyRange:") {
|
||||||
inRange = true
|
inRange = true
|
||||||
|
raw := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "storyRange:"))
|
||||||
|
if parsed := parseStringListLiteral(raw); parsed != nil {
|
||||||
|
storyRange = parsed
|
||||||
|
inRange = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
if strings.HasSuffix(strings.TrimSpace(line), "[]") {
|
if strings.HasSuffix(strings.TrimSpace(line), "[]") {
|
||||||
storyRange = []string{}
|
storyRange = []string{}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if inRange && strings.HasPrefix(strings.TrimSpace(line), "-") {
|
if inRange && strings.HasPrefix(strings.TrimSpace(line), "-") {
|
||||||
val := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "-"))
|
val := unquoteScalar(strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "-")))
|
||||||
val = strings.Trim(val, "\"")
|
|
||||||
if val != "" {
|
if val != "" {
|
||||||
storyRange = append(storyRange, val)
|
storyRange = append(storyRange, val)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -145,10 +145,9 @@ func normalizeStoryKey(projectRoot, input string) (struct{ ID, Prefix, Key strin
|
||||||
statusFile := sprintStatusFile(projectRoot)
|
statusFile := sprintStatusFile(projectRoot)
|
||||||
if fileExists(statusFile) {
|
if fileExists(statusFile) {
|
||||||
content, _ := readFile(statusFile)
|
content, _ := readFile(statusFile)
|
||||||
re := regexp.MustCompile(`(?m)^\s*` + regexp.QuoteMeta(result.Prefix) + `-`) // full key
|
re := regexp.MustCompile(`(?m)^\s*(` + regexp.QuoteMeta(result.Prefix) + `-[^:\s]+)\s*:`)
|
||||||
lines := re.FindAllString(content, -1)
|
if match := re.FindStringSubmatch(content); len(match) == 2 {
|
||||||
if len(lines) > 0 {
|
result.Key = strings.TrimSpace(match[1])
|
||||||
result.Key = strings.TrimSpace(strings.SplitN(lines[0], ":", 2)[0])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,12 @@ func findFrontmatterValueCase(path, key string) string {
|
||||||
front := extractFrontmatter(content)
|
front := extractFrontmatter(content)
|
||||||
lines := trimLines(front)
|
lines := trimLines(front)
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
if strings.HasPrefix(line, key+":") {
|
idx := strings.Index(line, ":")
|
||||||
return strings.Trim(strings.TrimSpace(strings.TrimPrefix(line, key+":")), "\"")
|
if idx < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.EqualFold(strings.TrimSpace(line[:idx]), key) {
|
||||||
|
return unquoteScalar(strings.TrimSpace(line[idx+1:]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
|
|
|
||||||
|
|
@ -388,13 +388,19 @@ func cmdSprintCompare(args []string) int {
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(line, "storyRange:") {
|
if strings.HasPrefix(line, "storyRange:") {
|
||||||
key = "storyRange"
|
key = "storyRange"
|
||||||
|
raw := strings.TrimSpace(strings.TrimPrefix(line, "storyRange:"))
|
||||||
|
if parsed := parseStringListLiteral(raw); parsed != nil {
|
||||||
|
storyRange = parsed
|
||||||
|
key = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
if strings.HasSuffix(strings.TrimSpace(line), "[]") {
|
if strings.HasSuffix(strings.TrimSpace(line), "[]") {
|
||||||
storyRange = []string{}
|
storyRange = []string{}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if key == "storyRange" && strings.HasPrefix(strings.TrimSpace(line), "-") {
|
if key == "storyRange" && strings.HasPrefix(strings.TrimSpace(line), "-") {
|
||||||
storyRange = append(storyRange, strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "-")))
|
storyRange = append(storyRange, unquoteScalar(strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "-"))))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if regexp.MustCompile(`^\S+:`).MatchString(line) && !strings.HasPrefix(line, "storyRange:") {
|
if regexp.MustCompile(`^\S+:`).MatchString(line) && !strings.HasPrefix(line, "storyRange:") {
|
||||||
|
|
@ -539,11 +545,12 @@ func cmdValidateState(args []string) int {
|
||||||
if keyRe.MatchString(line) {
|
if keyRe.MatchString(line) {
|
||||||
parts := strings.SplitN(line, ":", 2)
|
parts := strings.SplitN(line, ":", 2)
|
||||||
key := strings.TrimSpace(parts[0])
|
key := strings.TrimSpace(parts[0])
|
||||||
val := strings.TrimSpace(parts[1])
|
rawVal := strings.TrimSpace(parts[1])
|
||||||
if val == "" {
|
if rawVal == "" {
|
||||||
fields[key] = []string{}
|
fields[key] = []string{}
|
||||||
currentKey = key
|
currentKey = key
|
||||||
} else {
|
} else {
|
||||||
|
val := unquoteScalar(rawVal)
|
||||||
fields[key] = val
|
fields[key] = val
|
||||||
currentKey = ""
|
currentKey = ""
|
||||||
}
|
}
|
||||||
|
|
@ -551,7 +558,7 @@ func cmdValidateState(args []string) int {
|
||||||
}
|
}
|
||||||
if currentKey != "" && strings.HasPrefix(strings.TrimSpace(line), "-") {
|
if currentKey != "" && strings.HasPrefix(strings.TrimSpace(line), "-") {
|
||||||
items, _ := fields[currentKey].([]string)
|
items, _ := fields[currentKey].([]string)
|
||||||
items = append(items, strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "-")))
|
items = append(items, unquoteScalar(strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "-"))))
|
||||||
fields[currentKey] = items
|
fields[currentKey] = items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
@ -40,17 +41,23 @@ type tmuxStatus struct {
|
||||||
|
|
||||||
func cmdTmuxWrapper(args []string) int {
|
func cmdTmuxWrapper(args []string) int {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return tmuxWrapperUsage()
|
return tmuxWrapperUsage(os.Stderr, 1)
|
||||||
|
}
|
||||||
|
if isHelpFlag(args[0]) {
|
||||||
|
return tmuxWrapperUsage(os.Stdout, 0)
|
||||||
}
|
}
|
||||||
action := args[0]
|
action := args[0]
|
||||||
args = args[1:]
|
args = args[1:]
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case "spawn":
|
case "spawn":
|
||||||
|
if len(args) > 0 && isHelpFlag(args[0]) {
|
||||||
|
return tmuxWrapperUsage(os.Stdout, 0)
|
||||||
|
}
|
||||||
return tmuxWrapperSpawn(args)
|
return tmuxWrapperSpawn(args)
|
||||||
case "name":
|
case "name":
|
||||||
if len(args) < 3 {
|
if len(args) < 3 {
|
||||||
return tmuxWrapperUsage()
|
return tmuxWrapperUsage(os.Stderr, 1)
|
||||||
}
|
}
|
||||||
step := args[0]
|
step := args[0]
|
||||||
epic := args[1]
|
epic := args[1]
|
||||||
|
|
@ -71,7 +78,7 @@ func cmdTmuxWrapper(args []string) int {
|
||||||
return 0
|
return 0
|
||||||
case "kill":
|
case "kill":
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return tmuxWrapperUsage()
|
return tmuxWrapperUsage(os.Stderr, 1)
|
||||||
}
|
}
|
||||||
tmuxKillSession(args[0])
|
tmuxKillSession(args[0])
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -88,7 +95,7 @@ func cmdTmuxWrapper(args []string) int {
|
||||||
return 0
|
return 0
|
||||||
case "exists":
|
case "exists":
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return tmuxWrapperUsage()
|
return tmuxWrapperUsage(os.Stderr, 1)
|
||||||
}
|
}
|
||||||
if tmuxHasSession(args[0]) {
|
if tmuxHasSession(args[0]) {
|
||||||
fmt.Println("true")
|
fmt.Println("true")
|
||||||
|
|
@ -97,6 +104,9 @@ func cmdTmuxWrapper(args []string) int {
|
||||||
fmt.Println("false")
|
fmt.Println("false")
|
||||||
return 1
|
return 1
|
||||||
case "build-cmd":
|
case "build-cmd":
|
||||||
|
if len(args) > 0 && isHelpFlag(args[0]) {
|
||||||
|
return tmuxWrapperUsage(os.Stdout, 0)
|
||||||
|
}
|
||||||
return tmuxWrapperBuildCmd(args)
|
return tmuxWrapperBuildCmd(args)
|
||||||
case "project-slug":
|
case "project-slug":
|
||||||
fmt.Println(getProjectSlug())
|
fmt.Println(getProjectSlug())
|
||||||
|
|
@ -106,7 +116,7 @@ func cmdTmuxWrapper(args []string) int {
|
||||||
return 0
|
return 0
|
||||||
case "story-suffix":
|
case "story-suffix":
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return tmuxWrapperUsage()
|
return tmuxWrapperUsage(os.Stderr, 1)
|
||||||
}
|
}
|
||||||
fmt.Println(strings.ReplaceAll(args[0], ".", "-"))
|
fmt.Println(strings.ReplaceAll(args[0], ".", "-"))
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -120,33 +130,33 @@ func cmdTmuxWrapper(args []string) int {
|
||||||
fmt.Println(getSkillPrefix(getAgentType()))
|
fmt.Println(getSkillPrefix(getAgentType()))
|
||||||
return 0
|
return 0
|
||||||
default:
|
default:
|
||||||
return tmuxWrapperUsage()
|
return tmuxWrapperUsage(os.Stderr, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tmuxWrapperUsage() int {
|
func tmuxWrapperUsage(w io.Writer, code int) int {
|
||||||
fmt.Fprintln(os.Stderr, "Usage: tmux-wrapper <action> [args...]")
|
fmt.Fprintln(w, "Usage: tmux-wrapper <action> [args...]")
|
||||||
fmt.Fprintln(os.Stderr, "")
|
fmt.Fprintln(w, "")
|
||||||
fmt.Fprintln(os.Stderr, "Actions:")
|
fmt.Fprintln(w, "Actions:")
|
||||||
fmt.Fprintln(os.Stderr, " spawn <step> <epic> <story_id> [--command \"...\"] [--cycle N] [--agent TYPE]")
|
fmt.Fprintln(w, " spawn <step> <epic> <story_id> [--command \"...\"] [--cycle N] [--agent TYPE]")
|
||||||
fmt.Fprintln(os.Stderr, " name <step> <epic> <story_id> [--cycle N]")
|
fmt.Fprintln(w, " name <step> <epic> <story_id> [--cycle N]")
|
||||||
fmt.Fprintln(os.Stderr, " list [--project-only]")
|
fmt.Fprintln(w, " list [--project-only]")
|
||||||
fmt.Fprintln(os.Stderr, " kill <session_name>")
|
fmt.Fprintln(w, " kill <session_name>")
|
||||||
fmt.Fprintln(os.Stderr, " kill-all [--project-only]")
|
fmt.Fprintln(w, " kill-all [--project-only]")
|
||||||
fmt.Fprintln(os.Stderr, " exists <session_name>")
|
fmt.Fprintln(w, " exists <session_name>")
|
||||||
fmt.Fprintln(os.Stderr, " build-cmd <step> <story_id> [--agent TYPE] [extra_instruction]")
|
fmt.Fprintln(w, " build-cmd <step> <story_id> [--agent TYPE] [extra_instruction]")
|
||||||
fmt.Fprintln(os.Stderr, " project-slug")
|
fmt.Fprintln(w, " project-slug")
|
||||||
fmt.Fprintln(os.Stderr, " project-hash")
|
fmt.Fprintln(w, " project-hash")
|
||||||
fmt.Fprintln(os.Stderr, " story-suffix <story_id>")
|
fmt.Fprintln(w, " story-suffix <story_id>")
|
||||||
fmt.Fprintln(os.Stderr, " agent-type")
|
fmt.Fprintln(w, " agent-type")
|
||||||
fmt.Fprintln(os.Stderr, " agent-cli")
|
fmt.Fprintln(w, " agent-cli")
|
||||||
fmt.Fprintln(os.Stderr, " skill-prefix")
|
fmt.Fprintln(w, " skill-prefix")
|
||||||
return 1
|
return code
|
||||||
}
|
}
|
||||||
|
|
||||||
func tmuxWrapperSpawn(args []string) int {
|
func tmuxWrapperSpawn(args []string) int {
|
||||||
if len(args) < 3 {
|
if len(args) < 3 {
|
||||||
return tmuxWrapperUsage()
|
return tmuxWrapperUsage(os.Stderr, 1)
|
||||||
}
|
}
|
||||||
step := args[0]
|
step := args[0]
|
||||||
epic := args[1]
|
epic := args[1]
|
||||||
|
|
@ -216,7 +226,7 @@ func tmuxWrapperSpawn(args []string) int {
|
||||||
|
|
||||||
func tmuxWrapperBuildCmd(args []string) int {
|
func tmuxWrapperBuildCmd(args []string) int {
|
||||||
if len(args) < 2 {
|
if len(args) < 2 {
|
||||||
return tmuxWrapperUsage()
|
return tmuxWrapperUsage(os.Stderr, 1)
|
||||||
}
|
}
|
||||||
step := args[0]
|
step := args[0]
|
||||||
storyID := args[1]
|
storyID := args[1]
|
||||||
|
|
@ -928,6 +938,11 @@ func cmdMonitorSession(args []string) int {
|
||||||
fmt.Fprintln(os.Stderr, "Usage: monitor-session <session_name> [options]")
|
fmt.Fprintln(os.Stderr, "Usage: monitor-session <session_name> [options]")
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
if isHelpFlag(args[0]) {
|
||||||
|
fmt.Println("Usage: monitor-session <session_name> [options]")
|
||||||
|
fmt.Println("Options: --max-polls N --initial-wait N --project-root PATH --timeout MIN --verbose --json --agent TYPE --workflow TYPE --story-key KEY")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
sessionName := ""
|
sessionName := ""
|
||||||
maxPolls := 30
|
maxPolls := 30
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -12,10 +13,16 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultCommandTimeout = 10 * time.Minute
|
||||||
|
commandTimeoutExit = 124
|
||||||
|
)
|
||||||
|
|
||||||
func mustJSON(v any) string {
|
func mustJSON(v any) string {
|
||||||
b, err := json.Marshal(v)
|
b, err := json.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -68,16 +75,25 @@ func md5Hex8(input string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCmd(name string, args ...string) (string, error) {
|
func runCmd(name string, args ...string) (string, error) {
|
||||||
cmd := exec.Command(name, args...)
|
ctx, cancel := context.WithTimeout(context.Background(), defaultCommandTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, name, args...)
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
cmd.Stdout = &out
|
cmd.Stdout = &out
|
||||||
cmd.Stderr = &out
|
cmd.Stderr = &out
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
|
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||||
|
return out.String(), ctx.Err()
|
||||||
|
}
|
||||||
return out.String(), err
|
return out.String(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCmdExit(name string, args ...string) (string, int, error) {
|
func runCmdExit(name string, args ...string) (string, int, error) {
|
||||||
cmd := exec.Command(name, args...)
|
ctx, cancel := context.WithTimeout(context.Background(), defaultCommandTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, name, args...)
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
cmd.Stdout = &out
|
cmd.Stdout = &out
|
||||||
cmd.Stderr = &out
|
cmd.Stderr = &out
|
||||||
|
|
@ -85,6 +101,9 @@ func runCmdExit(name string, args ...string) (string, int, error) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return out.String(), 0, nil
|
return out.String(), 0, nil
|
||||||
}
|
}
|
||||||
|
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||||
|
return out.String(), commandTimeoutExit, ctx.Err()
|
||||||
|
}
|
||||||
var exitErr *exec.ExitError
|
var exitErr *exec.ExitError
|
||||||
if errors.As(err, &exitErr) {
|
if errors.As(err, &exitErr) {
|
||||||
return out.String(), exitErr.ExitCode(), err
|
return out.String(), exitErr.ExitCode(), err
|
||||||
|
|
@ -98,8 +117,25 @@ func execLookPath(bin string) (string, error) {
|
||||||
|
|
||||||
func writeFileAtomic(path string, data []byte) error {
|
func writeFileAtomic(path string, data []byte) error {
|
||||||
dir := filepath.Dir(path)
|
dir := filepath.Dir(path)
|
||||||
tmp := filepath.Join(dir, fmt.Sprintf(".%s.tmp", filepath.Base(path)))
|
tmpFile, err := os.CreateTemp(dir, fmt.Sprintf(".%s.*.tmp", filepath.Base(path)))
|
||||||
if err := os.WriteFile(tmp, data, 0o644); err != nil {
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tmp := tmpFile.Name()
|
||||||
|
defer os.Remove(tmp)
|
||||||
|
if err := tmpFile.Chmod(0o644); err != nil {
|
||||||
|
_ = tmpFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tmpFile.Write(data); err != nil {
|
||||||
|
_ = tmpFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tmpFile.Sync(); err != nil {
|
||||||
|
_ = tmpFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tmpFile.Close(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return os.Rename(tmp, path)
|
return os.Rename(tmp, path)
|
||||||
|
|
@ -160,3 +196,33 @@ func clampInt(val, min, max int) int {
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isHelpFlag(arg string) bool {
|
||||||
|
return arg == "--help" || arg == "-h"
|
||||||
|
}
|
||||||
|
|
||||||
|
func unquoteScalar(val string) string {
|
||||||
|
val = strings.TrimSpace(val)
|
||||||
|
if len(val) < 2 {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
if (val[0] == '"' && val[len(val)-1] == '"') || (val[0] == '\'' && val[len(val)-1] == '\'') {
|
||||||
|
if unquoted, err := strconv.Unquote(val); err == nil {
|
||||||
|
return unquoted
|
||||||
|
}
|
||||||
|
return val[1 : len(val)-1]
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseStringListLiteral(raw string) []string {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := []string{}
|
||||||
|
if err := json.Unmarshal([]byte(raw), &out); err == nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,12 +106,22 @@ func cmdValidateStoryCreation(args []string) int {
|
||||||
switch args[i] {
|
switch args[i] {
|
||||||
case "--before":
|
case "--before":
|
||||||
if i+1 < len(args) {
|
if i+1 < len(args) {
|
||||||
before, _ = strconv.Atoi(args[i+1])
|
n, err := strconv.Atoi(args[i+1])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Invalid --before value: %s\n", args[i+1])
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
before = n
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
case "--after":
|
case "--after":
|
||||||
if i+1 < len(args) {
|
if i+1 < len(args) {
|
||||||
after, _ = strconv.Atoi(args[i+1])
|
n, err := strconv.Atoi(args[i+1])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Invalid --after value: %s\n", args[i+1])
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
after = n
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
case "--artifacts-dir":
|
case "--artifacts-dir":
|
||||||
|
|
@ -133,7 +143,14 @@ func cmdValidateStoryCreation(args []string) int {
|
||||||
fmt.Fprintln(os.Stderr, "Usage: validate-story-creation list <story_id>")
|
fmt.Fprintln(os.Stderr, "Usage: validate-story-creation list <story_id>")
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
listStoryFiles(args[0])
|
storyID := args[0]
|
||||||
|
for i := 1; i < len(args); i++ {
|
||||||
|
if args[i] == "--artifacts-dir" && i+1 < len(args) {
|
||||||
|
artifactsDir = args[i+1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listStoryFiles(storyID)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
case "prefix":
|
case "prefix":
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ compare=$(cat "$tmp_compare")
|
||||||
sessions=$(cat "$tmp_sessions")
|
sessions=$(cat "$tmp_sessions")
|
||||||
rm -f "$tmp_compare" "$tmp_sessions"
|
rm -f "$tmp_compare" "$tmp_sessions"
|
||||||
|
|
||||||
incomplete=$(echo "$compare" | jq -r '.incomplete | join(\", \")')
|
incomplete=$(echo "$compare" | jq -r '.incomplete | join(", ")')
|
||||||
session_count=$(echo "$sessions" | jq -r '.count')
|
session_count=$(echo "$sessions" | jq -r '.count')
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,11 +124,13 @@ Refer to `{complexityScoring}` for scoring criteria and thresholds.
|
||||||
# Deterministic threshold
|
# Deterministic threshold
|
||||||
if [ "$selected_count" -ge 4 ]; then
|
if [ "$selected_count" -ge 4 ]; then
|
||||||
# Parallel mode (max 4 workers)
|
# Parallel mode (max 4 workers)
|
||||||
|
tmp_story_complexity=$(mktemp)
|
||||||
printf "%s\n" $selected_ids | xargs -I{} -P 4 sh -c '
|
printf "%s\n" $selected_ids | xargs -I{} -P 4 sh -c '
|
||||||
"{parseStory}" parse-story --epic "{epic_path}" --story "{}" --rules "{complexityRules}" \
|
"{parseStory}" parse-story --epic "{epic_path}" --story "{}" --rules "{complexityRules}" \
|
||||||
| jq -c "{storyId:.storyId,title:.title,complexity:.complexity}"
|
| jq -c "{storyId:.storyId,title:.title,complexity:.complexity}"
|
||||||
' > /tmp/story-complexity.ndjson
|
' > "$tmp_story_complexity"
|
||||||
stories_json=$(jq -s '.' /tmp/story-complexity.ndjson)
|
stories_json=$(jq -s '.' "$tmp_story_complexity")
|
||||||
|
rm -f "$tmp_story_complexity"
|
||||||
else
|
else
|
||||||
# Sequential mode
|
# Sequential mode
|
||||||
stories_json='[]'
|
stories_json='[]'
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,11 @@ state_file="{outputFile}"
|
||||||
echo "- **[$(date -u +%Y-%m-%dT%H:%M:%SZ)]** Starting story {story_id}" >> "$state_file"
|
echo "- **[$(date -u +%Y-%m-%dT%H:%M:%SZ)]** Starting story {story_id}" >> "$state_file"
|
||||||
|
|
||||||
# Initialize Story Progress row
|
# Initialize Story Progress row
|
||||||
sed -i '' "/<!-- Progress rows/i\\
|
tmp_state=$(mktemp)
|
||||||
| {story_id} | - | - | - | - | - | in-progress |" "$state_file"
|
awk -v row="| {story_id} | - | - | - | - | - | in-progress |" '
|
||||||
|
/^<!-- Progress rows -->$/ { print row }
|
||||||
|
{ print }
|
||||||
|
' "$state_file" > "$tmp_state" && mv "$tmp_state" "$state_file"
|
||||||
```
|
```
|
||||||
|
|
||||||
Display: "**Story {N}/{total}: {title}**"
|
Display: "**Story {N}/{total}: {title}**"
|
||||||
|
|
@ -136,7 +139,8 @@ validation=$("$scripts" validate-story-creation check {story_id} --before $befor
|
||||||
- If `validation.valid == true`:
|
- If `validation.valid == true`:
|
||||||
```bash
|
```bash
|
||||||
# Update Story Progress: mark create-story done
|
# Update Story Progress: mark create-story done
|
||||||
sed -i '' "s/^| ${story_id} |.*$/| ${story_id} | done | - | - | - | - | in-progress |/" "$state_file"
|
tmp_state=$(mktemp)
|
||||||
|
sed "s/^| ${story_id} |.*$/| ${story_id} | done | - | - | - | - | in-progress |/" "$state_file" > "$tmp_state" && mv "$tmp_state" "$state_file"
|
||||||
```
|
```
|
||||||
→ proceed to B
|
→ proceed to B
|
||||||
- If `validation.valid == false` AND attempts < 5 → retry with next agent (see `{retryStrategy}`)
|
- If `validation.valid == false` AND attempts < 5 → retry with next agent (see `{retryStrategy}`)
|
||||||
|
|
@ -161,7 +165,7 @@ result=$("$scripts" monitor-session "$session" --json --agent "$current_agent")
|
||||||
- Return normalized schema only: `next_action`, `confidence`, `error_class`, `reasons`
|
- Return normalized schema only: `next_action`, `confidence`, `error_class`, `reasons`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
parsed=$("$scripts" orchestrator-helper parse-output "$(echo $result | jq -r '.output_file')" dev)
|
parsed=$("$scripts" orchestrator-helper parse-output "$(printf '%s' "$result" | jq -r '.output_file')" dev)
|
||||||
next_action=$(echo "$parsed" | jq -r '.next_action')
|
next_action=$(echo "$parsed" | jq -r '.next_action')
|
||||||
confidence=$(echo "$parsed" | jq -r '.confidence // 0.0')
|
confidence=$(echo "$parsed" | jq -r '.confidence // 0.0')
|
||||||
error_class=$(echo "$parsed" | jq -r '.error_class // "none"')
|
error_class=$(echo "$parsed" | jq -r '.error_class // "none"')
|
||||||
|
|
@ -171,7 +175,8 @@ reasons=$(echo "$parsed" | jq -c '.reasons // []')
|
||||||
- If `next_action == "proceed"`:
|
- If `next_action == "proceed"`:
|
||||||
```bash
|
```bash
|
||||||
# Update Story Progress: mark dev-story done
|
# Update Story Progress: mark dev-story done
|
||||||
sed -i '' "s/^| ${story_id} |.*$/| ${story_id} | done | done | - | - | - | in-progress |/" "$state_file"
|
tmp_state=$(mktemp)
|
||||||
|
sed "s/^| ${story_id} |.*$/| ${story_id} | done | done | - | - | - | in-progress |/" "$state_file" > "$tmp_state" && mv "$tmp_state" "$state_file"
|
||||||
```
|
```
|
||||||
→ proceed to C (next step)
|
→ proceed to C (next step)
|
||||||
- If `next_action == "retry"` OR `result.final_state == "crashed"`:
|
- If `next_action == "retry"` OR `result.final_state == "crashed"`:
|
||||||
|
|
|
||||||
|
|
@ -41,14 +41,16 @@ result=$("$scripts" monitor-session "$session" --json --agent "$current_agent")
|
||||||
- SUCCESS:
|
- SUCCESS:
|
||||||
```bash
|
```bash
|
||||||
# Update Story Progress: mark automate done
|
# Update Story Progress: mark automate done
|
||||||
sed -i '' "s/^| ${story_id} |.*$/| ${story_id} | done | done | done | - | - | in-progress |/" "{outputFile}"
|
tmp_state=$(mktemp)
|
||||||
|
sed "s/^| ${story_id} |.*$/| ${story_id} | done | done | done | - | - | in-progress |/" "{outputFile}" > "$tmp_state" && mv "$tmp_state" "{outputFile}"
|
||||||
```
|
```
|
||||||
Display: `[story {N}/{total}] automate -> done`
|
Display: `[story {N}/{total}] automate -> done`
|
||||||
→ proceed to D
|
→ proceed to D
|
||||||
- FAILURE → retry up to 3 attempts (non-blocking, so fewer retries), then log warning:
|
- FAILURE → retry up to 3 attempts (non-blocking, so fewer retries), then log warning:
|
||||||
```bash
|
```bash
|
||||||
# Update Story Progress: mark automate skipped
|
# Update Story Progress: mark automate skipped
|
||||||
sed -i '' "s/^| ${story_id} |.*$/| ${story_id} | done | done | skip | - | - | in-progress |/" "{outputFile}"
|
tmp_state=$(mktemp)
|
||||||
|
sed "s/^| ${story_id} |.*$/| ${story_id} | done | done | skip | - | - | in-progress |/" "{outputFile}" > "$tmp_state" && mv "$tmp_state" "{outputFile}"
|
||||||
```
|
```
|
||||||
Display: `[story {N}/{total}] automate -> skip (non-blocking)`
|
Display: `[story {N}/{total}] automate -> skip (non-blocking)`
|
||||||
→ proceed to D
|
→ proceed to D
|
||||||
|
|
@ -88,7 +90,8 @@ Key points:
|
||||||
- **States:** `completed` (verified):
|
- **States:** `completed` (verified):
|
||||||
```bash
|
```bash
|
||||||
# Update Story Progress: mark code-review done
|
# Update Story Progress: mark code-review done
|
||||||
sed -i '' "s/^| ${story_id} |.*$/| ${story_id} | done | done | done | done | - | in-progress |/" "{outputFile}"
|
tmp_state=$(mktemp)
|
||||||
|
sed "s/^| ${story_id} |.*$/| ${story_id} | done | done | done | done | - | in-progress |/" "{outputFile}" > "$tmp_state" && mv "$tmp_state" "{outputFile}"
|
||||||
```
|
```
|
||||||
Display: `[story {N}/{total}] review -> done`
|
Display: `[story {N}/{total}] review -> done`
|
||||||
→ E | `incomplete` → count as failed attempt, retry until maxCycles, then CRITICAL escalate (Trigger #8)
|
→ E | `incomplete` → count as failed attempt, retry until maxCycles, then CRITICAL escalate (Trigger #8)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ ok=$(echo "$commit" | jq -r '.ok')
|
||||||
- If `ok == true`:
|
- If `ok == true`:
|
||||||
```bash
|
```bash
|
||||||
# Update Story Progress: mark git-commit done
|
# Update Story Progress: mark git-commit done
|
||||||
sed -i '' "s/^| ${story_id} |.*$/| ${story_id} | done | done | done | done | done | in-progress |/" "{outputFile}"
|
tmp_state=$(mktemp)
|
||||||
|
sed "s/^| ${story_id} |.*$/| ${story_id} | done | done | done | done | done | in-progress |/" "{outputFile}" > "$tmp_state" && mv "$tmp_state" "{outputFile}"
|
||||||
```
|
```
|
||||||
→ proceed to F
|
→ proceed to F
|
||||||
- If `ok == false` → log warning and escalate
|
- If `ok == false` → log warning and escalate
|
||||||
|
|
@ -57,7 +58,8 @@ Display: "**✅ Story {N} complete.**"
|
||||||
echo "- **[$(date -u +%Y-%m-%dT%H:%M:%SZ)]** Story {story_id}: ✅ complete (commit + sprint-status verified)" >> "{outputFile}"
|
echo "- **[$(date -u +%Y-%m-%dT%H:%M:%SZ)]** Story {story_id}: ✅ complete (commit + sprint-status verified)" >> "{outputFile}"
|
||||||
|
|
||||||
# Update Story Progress: mark story done
|
# Update Story Progress: mark story done
|
||||||
sed -i '' "s/^| ${story_id} |.*$/| ${story_id} | done | done | done | done | done | done |/" "{outputFile}"
|
tmp_state=$(mktemp)
|
||||||
|
sed "s/^| ${story_id} |.*$/| ${story_id} | done | done | done | done | done | done |/" "{outputFile}" > "$tmp_state" && mv "$tmp_state" "{outputFile}"
|
||||||
```
|
```
|
||||||
Display: `[story {N}/{total}] finalize -> done`
|
Display: `[story {N}/{total}] finalize -> done`
|
||||||
|
|
||||||
|
|
@ -129,7 +131,9 @@ cmd=$("{scriptsDir}" tmux-wrapper build-cmd retro {epic_number} --agent "claude"
|
||||||
session=$("{scriptsDir}" tmux-wrapper spawn retro "" {epic_number} --agent "claude" --command "$cmd")
|
session=$("{scriptsDir}" tmux-wrapper spawn retro "" {epic_number} --agent "claude" --command "$cmd")
|
||||||
|
|
||||||
# Monitor with safe failure (never escalate on retro failure)
|
# Monitor with safe failure (never escalate on retro failure)
|
||||||
result=$("{scriptsDir}" monitor-session "$session" --json --agent "claude")
|
retro_timeout=60
|
||||||
|
[ "$story_count" -gt 10 ] && retro_timeout=90
|
||||||
|
result=$("{scriptsDir}" monitor-session "$session" --json --agent "claude" --timeout "$retro_timeout")
|
||||||
"{scriptsDir}" tmux-wrapper kill "$session"
|
"{scriptsDir}" tmux-wrapper kill "$session"
|
||||||
|
|
||||||
retro_status=$(echo "$result" | jq -r '.final_state')
|
retro_status=$(echo "$result" | jq -r '.final_state')
|
||||||
|
|
|
||||||
|
|
@ -91,12 +91,23 @@ tmp_sessions=$(mktemp)
|
||||||
("{validateState}" validate-state --state "{state_path}" > "$tmp_validation") &
|
("{validateState}" validate-state --state "{state_path}" > "$tmp_validation") &
|
||||||
validation_pid=$!
|
validation_pid=$!
|
||||||
|
|
||||||
project_slug=$(echo "$("{deriveProjectSlug}" derive-project-slug --project-root "{project-root}")" | jq -r '.slug')
|
project_slug_json=$("{deriveProjectSlug}" derive-project-slug --project-root "{project-root}") || {
|
||||||
|
rm -f "$tmp_validation" "$tmp_sessions"
|
||||||
|
echo "derive-project-slug failed"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
project_slug=$(printf '%s' "$project_slug_json" | jq -r '.slug')
|
||||||
("{listSessions}" list-sessions --slug "$project_slug" > "$tmp_sessions") &
|
("{listSessions}" list-sessions --slug "$project_slug" > "$tmp_sessions") &
|
||||||
sessions_pid=$!
|
sessions_pid=$!
|
||||||
|
|
||||||
wait "$validation_pid"
|
wait "$validation_pid"; validation_status=$?
|
||||||
wait "$sessions_pid"
|
wait "$sessions_pid"; sessions_status=$?
|
||||||
|
|
||||||
|
if [ "$validation_status" -ne 0 ] || [ "$sessions_status" -ne 0 ]; then
|
||||||
|
rm -f "$tmp_validation" "$tmp_sessions"
|
||||||
|
echo "state validation or session inventory failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
validation=$(cat "$tmp_validation")
|
validation=$(cat "$tmp_validation")
|
||||||
sessions=$(cat "$tmp_sessions")
|
sessions=$(cat "$tmp_sessions")
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ Use carried-forward context:
|
||||||
- `untracked_live`
|
- `untracked_live`
|
||||||
- Prior structure/session issues
|
- Prior structure/session issues
|
||||||
|
|
||||||
Load the selected state document again (resolved as `{outputFile}` for this run) to verify progress details.
|
Load the selected state document again (resolved as `{state_path}` for this run) to verify progress details.
|
||||||
|
|
||||||
### 2. Validate Story Progress Thoroughly
|
### 2. Validate Story Progress Thoroughly
|
||||||
|
|
||||||
|
|
@ -64,8 +64,8 @@ If `story_count >= 4`, run per-story consistency checks in parallel and return c
|
||||||
```bash
|
```bash
|
||||||
story_ids=$(echo "$validation" | jq -r '.storyRange[]?')
|
story_ids=$(echo "$validation" | jq -r '.storyRange[]?')
|
||||||
tmp_progress=$(mktemp)
|
tmp_progress=$(mktemp)
|
||||||
printf "%s\n" $story_ids | xargs -I{} -P 4 sh -c \
|
printf "%s\n" "$story_ids" | xargs -I{} -P 4 sh -c \
|
||||||
'rg -n "^[[:space:]]*\\|[[:space:]]*{}[[:space:]]*\\|" "$0" | head -n 1 | sed "s/^/{}|/"' "$state_path" \
|
'id="$1"; file="$2"; rg -n -F "| ${id} |" "$file" | head -n 1 | sed "s/^/${id}|/"' _ "{}" "$state_path" \
|
||||||
> "$tmp_progress"
|
> "$tmp_progress"
|
||||||
progress_rows=$(wc -l < "$tmp_progress" | tr -d ' ')
|
progress_rows=$(wc -l < "$tmp_progress" | tr -d ' ')
|
||||||
rm -f "$tmp_progress"
|
rm -f "$tmp_progress"
|
||||||
|
|
|
||||||
|
|
@ -94,16 +94,8 @@
|
||||||
</action>
|
</action>
|
||||||
|
|
||||||
<check if="total_issues_found lt 3">
|
<check if="total_issues_found lt 3">
|
||||||
<critical>NOT LOOKING HARD ENOUGH - Find more problems!</critical>
|
<action>Re-examine the review surface for missed issues, but report only verified findings.</action>
|
||||||
<action>Re-examine code for:
|
<action>If no additional issues are found, continue with the smaller confirmed set.</action>
|
||||||
- Edge cases and null handling
|
|
||||||
- Architecture violations
|
|
||||||
- Documentation gaps
|
|
||||||
- Integration issues
|
|
||||||
- Dependency problems
|
|
||||||
- Git commit message quality (if applicable)
|
|
||||||
</action>
|
|
||||||
<action>Find at least 3 more specific, actionable issues</action>
|
|
||||||
</check>
|
</check>
|
||||||
</step>
|
</step>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue