diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/artifacts/story-automator/bin/darwin-amd64/story-automator b/src/bmm-skills/4-implementation/bmad-story-automator-go/artifacts/story-automator/bin/darwin-amd64/story-automator index 054c2d931..10b80f384 100755 Binary files a/src/bmm-skills/4-implementation/bmad-story-automator-go/artifacts/story-automator/bin/darwin-amd64/story-automator and b/src/bmm-skills/4-implementation/bmad-story-automator-go/artifacts/story-automator/bin/darwin-amd64/story-automator differ diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/artifacts/story-automator/bin/darwin-arm64/story-automator b/src/bmm-skills/4-implementation/bmad-story-automator-go/artifacts/story-automator/bin/darwin-arm64/story-automator index b3e468196..8330275e3 100755 Binary files a/src/bmm-skills/4-implementation/bmad-story-automator-go/artifacts/story-automator/bin/darwin-arm64/story-automator and b/src/bmm-skills/4-implementation/bmad-story-automator-go/artifacts/story-automator/bin/darwin-arm64/story-automator differ diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/artifacts/story-automator/bin/linux-amd64/story-automator b/src/bmm-skills/4-implementation/bmad-story-automator-go/artifacts/story-automator/bin/linux-amd64/story-automator index 79699aaaa..0b21543c2 100755 Binary files a/src/bmm-skills/4-implementation/bmad-story-automator-go/artifacts/story-automator/bin/linux-amd64/story-automator and b/src/bmm-skills/4-implementation/bmad-story-automator-go/artifacts/story-automator/bin/linux-amd64/story-automator differ diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/artifacts/story-automator/bin/linux-arm64/story-automator b/src/bmm-skills/4-implementation/bmad-story-automator-go/artifacts/story-automator/bin/linux-arm64/story-automator index 68e2dabbb..4082a73f0 100755 Binary files a/src/bmm-skills/4-implementation/bmad-story-automator-go/artifacts/story-automator/bin/linux-arm64/story-automator and b/src/bmm-skills/4-implementation/bmad-story-automator-go/artifacts/story-automator/bin/linux-arm64/story-automator differ diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/adaptive-retry.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/adaptive-retry.md index 6420b6995..d5ab89462 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/adaptive-retry.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/adaptive-retry.md @@ -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) | | 4 | FAILURE, plateau confirmed across agents | **DEFER** story (complexity/context limit hit) | | 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, plateau confirmed (and progress > 0) | **DEFER** story | | 5 | FAILURE, variable but incomplete | **ESCALATE** (all retries exhausted) | --- diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/agent-fallback.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/agent-fallback.md index f7d7442b1..fea888443 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/agent-fallback.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/agent-fallback.md @@ -64,12 +64,13 @@ session=$("$scripts" tmux-wrapper spawn dev {epic} {story_id} \ ## 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) - **Different activity detection:** Uses output freshness + heartbeat (no marker reliance) - **Output staleness window:** `CODEX_OUTPUT_STALE_SECONDS` (default: 300) - **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 ## ⚠️ Codex Code-Review Limitations (v1.5.0) diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/code-review-loop.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/code-review-loop.md index 6109f7966..4bcd06e8f 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/code-review-loop.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/code-review-loop.md @@ -101,13 +101,13 @@ is_done=$(echo "$status" | jq -r '.done') **IF final_state == "incomplete":** (v2.2 - Codex-specific) - Session idle but sprint-status NOT updated - Cleanup: `"$scripts" tmux-wrapper kill "$session_name"` -- Count this as a failed attempt and **retry** until `reviewCycle == maxCycles` -- **After maxCycles exhausted:** Escalate with CRITICAL priority (Trigger #8) -- Present options: +- Increment `reviewCycle` +- If `reviewCycle <= maxCycles`: count this as a failed attempt and **CONTINUE** with a retry +- If `reviewCycle > maxCycles`: Escalate with CRITICAL priority (Trigger #8), then present options: 1. **[1] Manual Fix** - Update sprint-status.yaml yourself 2. **[2] Run with Claude** - Re-run code-review with Claude agent 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":** - Log "Review session failed: $final_state" diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/escalation-messages-core.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/escalation-messages-core.md index 2454b4d18..a0a32fb8d 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/escalation-messages-core.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/escalation-messages-core.md @@ -27,6 +27,9 @@ tests_pass=$([[ "$test_result" != *"FAIL"* ]] && echo "true" || echo "false") Story: {story_name} Story ID: {story_id} +``` + +--- ## 2. Cannot Parse Session Output @@ -64,7 +67,7 @@ Story: {story_name} Step: {step_name} Error: {error_message} -Unable to spawn T-Mux session after retry. +Unable to spawn tmux session after retry. Options: [1] Retry again diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/escalation-messages.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/escalation-messages.md index 6e62dda39..af931f383 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/escalation-messages.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/escalation-messages.md @@ -3,3 +3,4 @@ See: - `escalation-messages-core.md` (Triggers 1-4) - `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` diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/monitoring-fallback.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/monitoring-fallback.md index 6ff081a01..acaf8cc70 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/monitoring-fallback.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/monitoring-fallback.md @@ -22,15 +22,15 @@ When `story-automator monitor-session` fails or background monitoring task dies: ```bash # 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 if [ -n "$sessions" ]; then - for session in $sessions; do + while IFS= read -r session; do status=$("$scripts" tmux-status-check "$session") session_state=$(echo "$status" | cut -d',' -f6) # Act based on direct status - done + done <<< "$sessions" fi # STEP 3: ALWAYS verify source of truth regardless of session status diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/monitoring-pattern.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/monitoring-pattern.md index e80d2b527..380c6aad3 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/monitoring-pattern.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/monitoring-pattern.md @@ -84,7 +84,7 @@ story-automator monitor-session [options] # --json Output as JSON instead of CSV # 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 diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/orchestrator-rules-appendix.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/orchestrator-rules-appendix.md index eb80b2b3f..c831cca79 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/orchestrator-rules-appendix.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/orchestrator-rules-appendix.md @@ -76,7 +76,7 @@ Before escalating, check if story is blocking: - Decision tree for poll results - 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 - ALWAYS use sub-agents (Task tool, haiku) - Verification checkpoint before proceeding diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/retry-fallback-strategy.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/retry-fallback-strategy.md index cfc63a3c1..6f7599c7b 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/retry-fallback-strategy.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/retry-fallback-strategy.md @@ -36,8 +36,10 @@ resolve_agent_for_task() { primary_agent=$(echo "$result" | jq -r '.primary') fallback_agent=$(echo "$result" | jq -r '.fallback') - # Handle "false"/null meaning disabled - [ "$fallback_agent" = "false" ] && fallback_agent="" + # Normalize disabled fallback states + if [ -z "$fallback_agent" ] || [ "$fallback_agent" = "false" ] || [ "$fallback_agent" = "null" ] || [ "$fallback_agent" = "$primary_agent" ]; then + fallback_agent="" + fi } # Usage: diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/scripts-reference.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/scripts-reference.md index 92a0c908e..70229be64 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/scripts-reference.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/scripts-reference.md @@ -36,7 +36,7 @@ session=$("$scripts" tmux-wrapper spawn {type} {epic} {story_id} \ result=$("$scripts" monitor-session "$session" --json --agent "$agent") # 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 "$scripts" tmux-wrapper kill "$session" diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/tmux-commands.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/tmux-commands.md index b2ccd2290..cb737fcd4 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/tmux-commands.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/tmux-commands.md @@ -113,7 +113,7 @@ The status check script automatically detects Claude vs Codex sessions: **For full output (when completed/stuck):** ```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` diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/workflow-commands.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/workflow-commands.md index 6b172bd49..37fcf69f4 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/data/workflow-commands.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/data/workflow-commands.md @@ -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. 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 -- Follow instructions.xml for detailed steps +- Reference checklist.md for validation steps Create story file at: _bmad-output/implementation-artifacts/STORY_PREFIX-*.md diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/basic_cmds.go b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/basic_cmds.go index a708bea11..2a1d96937 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/basic_cmds.go +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/basic_cmds.go @@ -79,9 +79,15 @@ func cmdEnsureMarkerGitignore(args []string) int { return 1 } - if strings.Contains(content, entry) { - writeJSON(map[string]any{"ok": true, "changed": false, "path": gitignorePath}) - return 0 + for _, line := range strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n") { + trimmed := strings.TrimSpace(line) + 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) @@ -90,7 +96,11 @@ func cmdEnsureMarkerGitignore(args []string) int { return 1 } 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"}) return 1 } @@ -202,7 +212,7 @@ func cmdEnsureStopHook(args []string) int { } exists := false - needsPathUpdate := false + needsUpdate := false for _, entry := range stopHooks { entryMap, ok := entry.(map[string]any) if !ok { @@ -217,16 +227,26 @@ func cmdEnsureStopHook(args []string) int { if cmd, ok := m["command"].(string); ok { if cmd == commandPath { exists = true - break - } - // 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") { + } else if strings.Contains(cmd, "story-automator") && strings.Contains(cmd, "stop-hook") { exists = true - if cmd != commandPath { - // Migrate stale path to resolved absolute path in-place. - m["command"] = commandPath - needsPathUpdate = true + m["command"] = commandPath + needsUpdate = 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 } @@ -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}) return 0 } - if exists && needsPathUpdate { - // Path normalized to absolute — write updated settings. - // Return changed:false because the hook functionally existed; - // no session restart is needed. + if exists && needsUpdate { b, _ := json.MarshalIndent(root, "", " ") if err := writeFileAtomic(settingsPath, b); err != nil { writeJSON(map[string]any{"ok": false, "error": "write_failed", "path": settingsPath}) 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 } diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/main.go b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/main.go index 473ed07dd..5091a4d3d 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/main.go +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/main.go @@ -2,14 +2,19 @@ package main import ( "fmt" + "io" "os" ) func main() { if len(os.Args) < 2 { - usage() + printUsage(os.Stderr) os.Exit(1) } + if isHelpFlag(os.Args[1]) { + printUsage(os.Stdout) + return + } cmd := os.Args[1] args := os.Args[2:] @@ -62,37 +67,37 @@ func main() { code = cmdAgentConfig(args) default: fmt.Fprintf(os.Stderr, "Unknown command: %s\n", cmd) - usage() + printUsage(os.Stderr) code = 1 } os.Exit(code) } -func usage() { - fmt.Fprintln(os.Stderr, "story-automator [args]") - fmt.Fprintln(os.Stderr, "") - fmt.Fprintln(os.Stderr, "Commands:") - fmt.Fprintln(os.Stderr, " derive-project-slug") - fmt.Fprintln(os.Stderr, " ensure-marker-gitignore") - fmt.Fprintln(os.Stderr, " ensure-stop-hook") - fmt.Fprintln(os.Stderr, " stop-hook") - fmt.Fprintln(os.Stderr, " build-state-doc") - fmt.Fprintln(os.Stderr, " commit-story") - fmt.Fprintln(os.Stderr, " parse-epic") - fmt.Fprintln(os.Stderr, " parse-story") - fmt.Fprintln(os.Stderr, " parse-story-range") - fmt.Fprintln(os.Stderr, " epic-complete") - fmt.Fprintln(os.Stderr, " sprint-compare") - fmt.Fprintln(os.Stderr, " state-metrics") - fmt.Fprintln(os.Stderr, " validate-state") - fmt.Fprintln(os.Stderr, " validate-story-creation") - fmt.Fprintln(os.Stderr, " list-sessions") - fmt.Fprintln(os.Stderr, " tmux-wrapper") - fmt.Fprintln(os.Stderr, " heartbeat-check") - fmt.Fprintln(os.Stderr, " codex-status-check") - fmt.Fprintln(os.Stderr, " tmux-status-check") - fmt.Fprintln(os.Stderr, " monitor-session") - fmt.Fprintln(os.Stderr, " orchestrator-helper") - fmt.Fprintln(os.Stderr, " agent-config") +func printUsage(w io.Writer) { + fmt.Fprintln(w, "story-automator [args]") + fmt.Fprintln(w, "") + fmt.Fprintln(w, "Commands:") + fmt.Fprintln(w, " derive-project-slug") + fmt.Fprintln(w, " ensure-marker-gitignore") + fmt.Fprintln(w, " ensure-stop-hook") + fmt.Fprintln(w, " stop-hook") + fmt.Fprintln(w, " build-state-doc") + fmt.Fprintln(w, " commit-story") + fmt.Fprintln(w, " parse-epic") + fmt.Fprintln(w, " parse-story") + fmt.Fprintln(w, " parse-story-range") + fmt.Fprintln(w, " epic-complete") + fmt.Fprintln(w, " sprint-compare") + fmt.Fprintln(w, " state-metrics") + fmt.Fprintln(w, " validate-state") + fmt.Fprintln(w, " validate-story-creation") + fmt.Fprintln(w, " list-sessions") + fmt.Fprintln(w, " tmux-wrapper") + fmt.Fprintln(w, " heartbeat-check") + fmt.Fprintln(w, " codex-status-check") + fmt.Fprintln(w, " tmux-status-check") + fmt.Fprintln(w, " monitor-session") + fmt.Fprintln(w, " orchestrator-helper") + fmt.Fprintln(w, " agent-config") } diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_agents.go b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_agents.go index 36ba3c28a..96e77acbc 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_agents.go +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_agents.go @@ -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"}) return 1 } - if agentsPath == "" { + if agentsPath == "" && stateFile != "" { agentsPath = findFrontmatterValue(stateFile, "agentsFile") } if agentsPath == "" || !fileExists(agentsPath) { diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_cmds.go b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_cmds.go index 753433463..c7b7752a6 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_cmds.go +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_cmds.go @@ -2,12 +2,16 @@ package main import ( "fmt" + "io" "os" ) func cmdOrchestratorHelper(args []string) int { if len(args) == 0 { - return orchestratorUsage() + return orchestratorUsage(os.Stderr, 1) + } + if isHelpFlag(args[0]) { + return orchestratorUsage(os.Stdout, 0) } action := args[0] args = args[1:] @@ -50,36 +54,36 @@ func cmdOrchestratorHelper(args []string) int { case "agents-resolve": return orchestratorAgentsResolve(args) default: - return orchestratorUsage() + return orchestratorUsage(os.Stderr, 1) } } -func orchestratorUsage() int { - fmt.Fprintln(os.Stderr, "Usage: orchestrator-helper [args]") - fmt.Fprintln(os.Stderr, "") - fmt.Fprintln(os.Stderr, "Actions:") - fmt.Fprintln(os.Stderr, " sprint-status get ") - fmt.Fprintln(os.Stderr, " sprint-status exists") - fmt.Fprintln(os.Stderr, " sprint-status check-epic ") - fmt.Fprintln(os.Stderr, " parse-output ") - fmt.Fprintln(os.Stderr, " marker create --epic E --story S --remaining N --state-file F") - fmt.Fprintln(os.Stderr, " marker remove") - fmt.Fprintln(os.Stderr, " marker check") - fmt.Fprintln(os.Stderr, " marker heartbeat") - fmt.Fprintln(os.Stderr, " state-list ") - fmt.Fprintln(os.Stderr, " state-latest [status]") - fmt.Fprintln(os.Stderr, " state-latest-incomplete ") - fmt.Fprintln(os.Stderr, " state-summary ") - fmt.Fprintln(os.Stderr, " state-update --set k=v") - fmt.Fprintln(os.Stderr, " escalate ") - fmt.Fprintln(os.Stderr, " commit-ready ") - fmt.Fprintln(os.Stderr, " normalize-key [--to id|key|prefix|json]") - fmt.Fprintln(os.Stderr, " story-file-status ") - fmt.Fprintln(os.Stderr, " verify-code-review ") - fmt.Fprintln(os.Stderr, " check-epic-complete [--state-file path]") - fmt.Fprintln(os.Stderr, " get-epic-stories [--state-file path]") - fmt.Fprintln(os.Stderr, " check-blocking ") - fmt.Fprintln(os.Stderr, " 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]") - return 1 +func orchestratorUsage(w io.Writer, code int) int { + fmt.Fprintln(w, "Usage: orchestrator-helper [args]") + fmt.Fprintln(w, "") + fmt.Fprintln(w, "Actions:") + fmt.Fprintln(w, " sprint-status get ") + fmt.Fprintln(w, " sprint-status exists") + fmt.Fprintln(w, " sprint-status check-epic ") + fmt.Fprintln(w, " parse-output ") + fmt.Fprintln(w, " marker create --epic E --story S --remaining N --state-file F") + fmt.Fprintln(w, " marker remove") + fmt.Fprintln(w, " marker check") + fmt.Fprintln(w, " marker heartbeat") + fmt.Fprintln(w, " state-list ") + fmt.Fprintln(w, " state-latest [status]") + fmt.Fprintln(w, " state-latest-incomplete ") + fmt.Fprintln(w, " state-summary ") + fmt.Fprintln(w, " state-update --set k=v") + fmt.Fprintln(w, " escalate ") + fmt.Fprintln(w, " commit-ready ") + fmt.Fprintln(w, " normalize-key [--to id|key|prefix|json]") + fmt.Fprintln(w, " story-file-status ") + fmt.Fprintln(w, " verify-code-review ") + fmt.Fprintln(w, " check-epic-complete [--state-file path]") + fmt.Fprintln(w, " get-epic-stories [--state-file path]") + fmt.Fprintln(w, " check-blocking ") + fmt.Fprintln(w, " agents-build --state-file path --complexity-file path --output path --config-json '{}'") + fmt.Fprintln(w, " agents-resolve --state-file path --story ID --task create|dev|auto|review [--agents-file path]") + return code } diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_epic.go b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_epic.go index ef8a75215..d22d07e65 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_epic.go +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_epic.go @@ -146,10 +146,9 @@ func orchestratorCheckEpicComplete(args []string) int { } } - epicsDir := filepath.Join(getProjectRoot(), "_bmad-output", "implementation-artifacts") - matches, _ := filepath.Glob(filepath.Join(epicsDir, fmt.Sprintf("epic-%s-*.md", epicNumber))) - if len(matches) > 0 { - content, _ := readFile(matches[0]) + epicFile := findEpicFile(getProjectRoot(), epicNumber) + if epicFile != "" { + content, _ := readFile(epicFile) re := regexp.MustCompile(regexp.QuoteMeta(epicNumber) + `\.[0-9]+`) ids := re.FindAllString(content, -1) ids = uniqueSortedStories(ids) diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_marker.go b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_marker.go index 3790b0b45..46d59050d 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_marker.go +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_marker.go @@ -1,10 +1,11 @@ package main import ( + "encoding/json" "fmt" "os" "path/filepath" - "regexp" + "strconv" "strings" ) @@ -69,20 +70,53 @@ func orchestratorMarker(args []string) int { } } - _ = ensureDir(filepath.Dir(markerFile)) - if heartbeat == "" { 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", - epic, story, remaining, stateFile, nowUTC().Format("2006-01-02T15:04:05Z"), heartbeat, pid, projectSlug) - _ = os.WriteFile(markerFile, []byte(payload), 0o644) + if err := ensureDir(filepath.Dir(markerFile)); err != nil { + fmt.Fprintf(os.Stderr, "Failed to create marker directory %s: %v\n", filepath.Dir(markerFile), err) + 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) return 0 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") return 0 @@ -110,8 +144,21 @@ func orchestratorMarker(args []string) int { return 1 } newHeartbeat := nowUTC().Format("2006-01-02T15:04:05Z") - updated := regexp.MustCompile(`"heartbeat":.*$`).ReplaceAllString(content, fmt.Sprintf("\"heartbeat\": \"%s\"", newHeartbeat)) - _ = os.WriteFile(markerFile, []byte(updated), 0o644) + var marker map[string]any + 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) return 0 diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_parse.go b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_parse.go index ae470249b..8cfd1da04 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_parse.go +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_parse.go @@ -1,13 +1,18 @@ package main import ( + "context" + "encoding/json" "fmt" "os" "os/exec" "regexp" "strings" + "time" ) +const parseOutputTimeout = 2 * time.Minute + func orchestratorParseOutput(args []string) int { if len(args) < 2 { fmt.Println("{\"status\":\"error\",\"reason\":\"output file not found or empty\"}") @@ -36,7 +41,10 @@ func orchestratorParseOutput(args []string) int { 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{} for _, e := range os.Environ() { if !strings.HasPrefix(e, "CLAUDECODE=") { @@ -46,6 +54,10 @@ func orchestratorParseOutput(args []string) int { cmd.Env = append(env, "STORY_AUTOMATOR_CHILD=true") out, err := cmd.CombinedOutput() 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\"}") return 1 } @@ -89,8 +101,11 @@ func extractJSONLine(result string) string { lines := trimLines(result) jsonRe := regexp.MustCompile(`\{.*\}`) for _, line := range lines { - if jsonRe.MatchString(line) { - return jsonRe.FindString(line) + for _, match := range jsonRe.FindAllString(line, -1) { + match = strings.TrimSpace(match) + if json.Valid([]byte(match)) { + return match + } } } return "" diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_sprint.go b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_sprint.go index 37428e0a5..f13ef24de 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_sprint.go +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_sprint.go @@ -152,9 +152,9 @@ func sprintStatusEpic(statusFile, epic string) ([]string, int) { if !seen[key] { stories = append(stories, key) seen[key] = true - } - if len(status) > 0 && status[0] == "done" { - doneCount++ + if len(status) > 0 && status[0] == "done" { + doneCount++ + } } } } diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_state.go b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_state.go index 0c7f3a557..3e22bf39b 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_state.go +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_state.go @@ -169,16 +169,27 @@ func orchestratorStateUpdate(args []string) int { } key := parts[0] val := parts[1] + replaced := false for idx, line := range lines { if strings.HasPrefix(line, key+":") { 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}) return 0 } @@ -199,14 +210,19 @@ func readStoryRangeFromState(stateFile string) []string { for _, line := range lines { if strings.HasPrefix(strings.TrimSpace(line), "storyRange:") { 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), "[]") { storyRange = []string{} } continue } if inRange && strings.HasPrefix(strings.TrimSpace(line), "-") { - val := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "-")) - val = strings.Trim(val, "\"") + val := unquoteScalar(strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "-"))) if val != "" { storyRange = append(storyRange, val) } diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_story.go b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_story.go index 1455f803d..a8f7d205b 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_story.go +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_story.go @@ -145,10 +145,9 @@ func normalizeStoryKey(projectRoot, input string) (struct{ ID, Prefix, Key strin statusFile := sprintStatusFile(projectRoot) if fileExists(statusFile) { content, _ := readFile(statusFile) - re := regexp.MustCompile(`(?m)^\s*` + regexp.QuoteMeta(result.Prefix) + `-`) // full key - lines := re.FindAllString(content, -1) - if len(lines) > 0 { - result.Key = strings.TrimSpace(strings.SplitN(lines[0], ":", 2)[0]) + re := regexp.MustCompile(`(?m)^\s*(` + regexp.QuoteMeta(result.Prefix) + `-[^:\s]+)\s*:`) + if match := re.FindStringSubmatch(content); len(match) == 2 { + result.Key = strings.TrimSpace(match[1]) } } } diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_util.go b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_util.go index 2597d43c6..262fcbf06 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_util.go +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/orchestrator_util.go @@ -28,8 +28,12 @@ func findFrontmatterValueCase(path, key string) string { front := extractFrontmatter(content) lines := trimLines(front) for _, line := range lines { - if strings.HasPrefix(line, key+":") { - return strings.Trim(strings.TrimSpace(strings.TrimPrefix(line, key+":")), "\"") + idx := strings.Index(line, ":") + if idx < 0 { + continue + } + if strings.EqualFold(strings.TrimSpace(line[:idx]), key) { + return unquoteScalar(strings.TrimSpace(line[idx+1:])) } } return "" diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/state_cmds.go b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/state_cmds.go index d4cdcda6d..79ada95c8 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/state_cmds.go +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/state_cmds.go @@ -388,13 +388,19 @@ func cmdSprintCompare(args []string) int { } if strings.HasPrefix(line, "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), "[]") { storyRange = []string{} } continue } 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 } if regexp.MustCompile(`^\S+:`).MatchString(line) && !strings.HasPrefix(line, "storyRange:") { @@ -539,11 +545,12 @@ func cmdValidateState(args []string) int { if keyRe.MatchString(line) { parts := strings.SplitN(line, ":", 2) key := strings.TrimSpace(parts[0]) - val := strings.TrimSpace(parts[1]) - if val == "" { + rawVal := strings.TrimSpace(parts[1]) + if rawVal == "" { fields[key] = []string{} currentKey = key } else { + val := unquoteScalar(rawVal) fields[key] = val currentKey = "" } @@ -551,7 +558,7 @@ func cmdValidateState(args []string) int { } if currentKey != "" && strings.HasPrefix(strings.TrimSpace(line), "-") { 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 } } diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/tmux_cmds.go b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/tmux_cmds.go index 1c3baa298..59a1c7f5e 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/tmux_cmds.go +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/tmux_cmds.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "os" "path/filepath" "regexp" @@ -40,17 +41,23 @@ type tmuxStatus struct { func cmdTmuxWrapper(args []string) int { if len(args) == 0 { - return tmuxWrapperUsage() + return tmuxWrapperUsage(os.Stderr, 1) + } + if isHelpFlag(args[0]) { + return tmuxWrapperUsage(os.Stdout, 0) } action := args[0] args = args[1:] switch action { case "spawn": + if len(args) > 0 && isHelpFlag(args[0]) { + return tmuxWrapperUsage(os.Stdout, 0) + } return tmuxWrapperSpawn(args) case "name": if len(args) < 3 { - return tmuxWrapperUsage() + return tmuxWrapperUsage(os.Stderr, 1) } step := args[0] epic := args[1] @@ -71,7 +78,7 @@ func cmdTmuxWrapper(args []string) int { return 0 case "kill": if len(args) < 1 { - return tmuxWrapperUsage() + return tmuxWrapperUsage(os.Stderr, 1) } tmuxKillSession(args[0]) return 0 @@ -88,7 +95,7 @@ func cmdTmuxWrapper(args []string) int { return 0 case "exists": if len(args) < 1 { - return tmuxWrapperUsage() + return tmuxWrapperUsage(os.Stderr, 1) } if tmuxHasSession(args[0]) { fmt.Println("true") @@ -97,6 +104,9 @@ func cmdTmuxWrapper(args []string) int { fmt.Println("false") return 1 case "build-cmd": + if len(args) > 0 && isHelpFlag(args[0]) { + return tmuxWrapperUsage(os.Stdout, 0) + } return tmuxWrapperBuildCmd(args) case "project-slug": fmt.Println(getProjectSlug()) @@ -106,7 +116,7 @@ func cmdTmuxWrapper(args []string) int { return 0 case "story-suffix": if len(args) < 1 { - return tmuxWrapperUsage() + return tmuxWrapperUsage(os.Stderr, 1) } fmt.Println(strings.ReplaceAll(args[0], ".", "-")) return 0 @@ -120,33 +130,33 @@ func cmdTmuxWrapper(args []string) int { fmt.Println(getSkillPrefix(getAgentType())) return 0 default: - return tmuxWrapperUsage() + return tmuxWrapperUsage(os.Stderr, 1) } } -func tmuxWrapperUsage() int { - fmt.Fprintln(os.Stderr, "Usage: tmux-wrapper [args...]") - fmt.Fprintln(os.Stderr, "") - fmt.Fprintln(os.Stderr, "Actions:") - fmt.Fprintln(os.Stderr, " spawn [--command \"...\"] [--cycle N] [--agent TYPE]") - fmt.Fprintln(os.Stderr, " name [--cycle N]") - fmt.Fprintln(os.Stderr, " list [--project-only]") - fmt.Fprintln(os.Stderr, " kill ") - fmt.Fprintln(os.Stderr, " kill-all [--project-only]") - fmt.Fprintln(os.Stderr, " exists ") - fmt.Fprintln(os.Stderr, " build-cmd [--agent TYPE] [extra_instruction]") - fmt.Fprintln(os.Stderr, " project-slug") - fmt.Fprintln(os.Stderr, " project-hash") - fmt.Fprintln(os.Stderr, " story-suffix ") - fmt.Fprintln(os.Stderr, " agent-type") - fmt.Fprintln(os.Stderr, " agent-cli") - fmt.Fprintln(os.Stderr, " skill-prefix") - return 1 +func tmuxWrapperUsage(w io.Writer, code int) int { + fmt.Fprintln(w, "Usage: tmux-wrapper [args...]") + fmt.Fprintln(w, "") + fmt.Fprintln(w, "Actions:") + fmt.Fprintln(w, " spawn [--command \"...\"] [--cycle N] [--agent TYPE]") + fmt.Fprintln(w, " name [--cycle N]") + fmt.Fprintln(w, " list [--project-only]") + fmt.Fprintln(w, " kill ") + fmt.Fprintln(w, " kill-all [--project-only]") + fmt.Fprintln(w, " exists ") + fmt.Fprintln(w, " build-cmd [--agent TYPE] [extra_instruction]") + fmt.Fprintln(w, " project-slug") + fmt.Fprintln(w, " project-hash") + fmt.Fprintln(w, " story-suffix ") + fmt.Fprintln(w, " agent-type") + fmt.Fprintln(w, " agent-cli") + fmt.Fprintln(w, " skill-prefix") + return code } func tmuxWrapperSpawn(args []string) int { if len(args) < 3 { - return tmuxWrapperUsage() + return tmuxWrapperUsage(os.Stderr, 1) } step := args[0] epic := args[1] @@ -216,7 +226,7 @@ func tmuxWrapperSpawn(args []string) int { func tmuxWrapperBuildCmd(args []string) int { if len(args) < 2 { - return tmuxWrapperUsage() + return tmuxWrapperUsage(os.Stderr, 1) } step := args[0] storyID := args[1] @@ -928,6 +938,11 @@ func cmdMonitorSession(args []string) int { fmt.Fprintln(os.Stderr, "Usage: monitor-session [options]") return 1 } + if isHelpFlag(args[0]) { + fmt.Println("Usage: monitor-session [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 := "" maxPolls := 30 diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/util.go b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/util.go index 019f99d9e..4c9049321 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/util.go +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/util.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "context" "crypto/md5" "encoding/hex" "encoding/json" @@ -12,10 +13,16 @@ import ( "os/exec" "path/filepath" "regexp" + "strconv" "strings" "time" ) +const ( + defaultCommandTimeout = 10 * time.Minute + commandTimeoutExit = 124 +) + func mustJSON(v any) string { b, err := json.Marshal(v) if err != nil { @@ -68,16 +75,25 @@ func md5Hex8(input string) string { } 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 cmd.Stdout = &out cmd.Stderr = &out err := cmd.Run() + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return out.String(), ctx.Err() + } return out.String(), err } 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 cmd.Stdout = &out cmd.Stderr = &out @@ -85,6 +101,9 @@ func runCmdExit(name string, args ...string) (string, int, error) { if err == nil { return out.String(), 0, nil } + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return out.String(), commandTimeoutExit, ctx.Err() + } var exitErr *exec.ExitError if errors.As(err, &exitErr) { return out.String(), exitErr.ExitCode(), err @@ -98,8 +117,25 @@ func execLookPath(bin string) (string, error) { func writeFileAtomic(path string, data []byte) error { dir := filepath.Dir(path) - tmp := filepath.Join(dir, fmt.Sprintf(".%s.tmp", filepath.Base(path))) - if err := os.WriteFile(tmp, data, 0o644); err != nil { + tmpFile, err := os.CreateTemp(dir, fmt.Sprintf(".%s.*.tmp", filepath.Base(path))) + 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 os.Rename(tmp, path) @@ -160,3 +196,33 @@ func clampInt(val, min, max int) int { } 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 +} diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/validate_cmds.go b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/validate_cmds.go index 989c71ec6..ce5285a71 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/validate_cmds.go +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/source/cmd/story-automator/validate_cmds.go @@ -106,12 +106,22 @@ func cmdValidateStoryCreation(args []string) int { switch args[i] { case "--before": 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++ } case "--after": 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++ } case "--artifacts-dir": @@ -133,7 +143,14 @@ func cmdValidateStoryCreation(args []string) int { fmt.Fprintln(os.Stderr, "Usage: validate-story-creation list ") 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 case "prefix": diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-01b-continue.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-01b-continue.md index 1d7fbb9ec..59e53bf8a 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-01b-continue.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-01b-continue.md @@ -88,7 +88,7 @@ compare=$(cat "$tmp_compare") sessions=$(cat "$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') ``` diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-02-preflight.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-02-preflight.md index 8c300aaf5..c32029427 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-02-preflight.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-02-preflight.md @@ -124,11 +124,13 @@ Refer to `{complexityScoring}` for scoring criteria and thresholds. # Deterministic threshold if [ "$selected_count" -ge 4 ]; then # Parallel mode (max 4 workers) + tmp_story_complexity=$(mktemp) printf "%s\n" $selected_ids | xargs -I{} -P 4 sh -c ' "{parseStory}" parse-story --epic "{epic_path}" --story "{}" --rules "{complexityRules}" \ | jq -c "{storyId:.storyId,title:.title,complexity:.complexity}" - ' > /tmp/story-complexity.ndjson - stories_json=$(jq -s '.' /tmp/story-complexity.ndjson) + ' > "$tmp_story_complexity" + stories_json=$(jq -s '.' "$tmp_story_complexity") + rm -f "$tmp_story_complexity" else # Sequential mode stories_json='[]' diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-03-execute.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-03-execute.md index e1a524034..529a3e543 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-03-execute.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-03-execute.md @@ -91,8 +91,11 @@ state_file="{outputFile}" echo "- **[$(date -u +%Y-%m-%dT%H:%M:%SZ)]** Starting story {story_id}" >> "$state_file" # Initialize Story Progress row -sed -i '' "/$/ { print row } + { print } +' "$state_file" > "$tmp_state" && mv "$tmp_state" "$state_file" ``` Display: "**Story {N}/{total}: {title}**" @@ -136,7 +139,8 @@ validation=$("$scripts" validate-story-creation check {story_id} --before $befor - If `validation.valid == true`: ```bash # 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 - 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` ```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') confidence=$(echo "$parsed" | jq -r '.confidence // 0.0') error_class=$(echo "$parsed" | jq -r '.error_class // "none"') @@ -171,7 +175,8 @@ reasons=$(echo "$parsed" | jq -c '.reasons // []') - If `next_action == "proceed"`: ```bash # 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) - If `next_action == "retry"` OR `result.final_state == "crashed"`: diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-03a-execute-review.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-03a-execute-review.md index 55c1e1bfd..4985f7311 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-03a-execute-review.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-03a-execute-review.md @@ -41,14 +41,16 @@ result=$("$scripts" monitor-session "$session" --json --agent "$current_agent") - SUCCESS: ```bash # 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` → proceed to D - FAILURE → retry up to 3 attempts (non-blocking, so fewer retries), then log warning: ```bash # 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)` → proceed to D @@ -88,7 +90,8 @@ Key points: - **States:** `completed` (verified): ```bash # 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` → E | `incomplete` → count as failed attempt, retry until maxCycles, then CRITICAL escalate (Trigger #8) diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-03b-execute-finish.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-03b-execute-finish.md index 466057e54..eaaccf23c 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-03b-execute-finish.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-c/step-03b-execute-finish.md @@ -25,7 +25,8 @@ ok=$(echo "$commit" | jq -r '.ok') - If `ok == true`: ```bash # 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 - 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}" # 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` @@ -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") # 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" retro_status=$(echo "$result" | jq -r '.final_state') diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-v/step-v-01-check.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-v/step-v-01-check.md index 07fb8adc1..a83da35dd 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-v/step-v-01-check.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-v/step-v-01-check.md @@ -91,12 +91,23 @@ tmp_sessions=$(mktemp) ("{validateState}" validate-state --state "{state_path}" > "$tmp_validation") & 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") & sessions_pid=$! -wait "$validation_pid" -wait "$sessions_pid" +wait "$validation_pid"; validation_status=$? +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") sessions=$(cat "$tmp_sessions") diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-v/step-v-02-report.md b/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-v/step-v-02-report.md index 4d5170620..e00cc31a6 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-v/step-v-02-report.md +++ b/src/bmm-skills/4-implementation/bmad-story-automator-go/steps-v/step-v-02-report.md @@ -27,7 +27,7 @@ Use carried-forward context: - `untracked_live` - 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 @@ -64,8 +64,8 @@ If `story_count >= 4`, run per-story consistency checks in parallel and return c ```bash story_ids=$(echo "$validation" | jq -r '.storyRange[]?') tmp_progress=$(mktemp) -printf "%s\n" $story_ids | xargs -I{} -P 4 sh -c \ - 'rg -n "^[[:space:]]*\\|[[:space:]]*{}[[:space:]]*\\|" "$0" | head -n 1 | sed "s/^/{}|/"' "$state_path" \ +printf "%s\n" "$story_ids" | xargs -I{} -P 4 sh -c \ + 'id="$1"; file="$2"; rg -n -F "| ${id} |" "$file" | head -n 1 | sed "s/^/${id}|/"' _ "{}" "$state_path" \ > "$tmp_progress" progress_rows=$(wc -l < "$tmp_progress" | tr -d ' ') rm -f "$tmp_progress" diff --git a/src/bmm-skills/4-implementation/bmad-story-automator-review/instructions.xml b/src/bmm-skills/4-implementation/bmad-story-automator-review/instructions.xml index e657a8722..e48ccdd8e 100644 --- a/src/bmm-skills/4-implementation/bmad-story-automator-review/instructions.xml +++ b/src/bmm-skills/4-implementation/bmad-story-automator-review/instructions.xml @@ -94,16 +94,8 @@ - NOT LOOKING HARD ENOUGH - Find more problems! - Re-examine code for: - - Edge cases and null handling - - Architecture violations - - Documentation gaps - - Integration issues - - Dependency problems - - Git commit message quality (if applicable) - - Find at least 3 more specific, actionable issues + Re-examine the review surface for missed issues, but report only verified findings. + If no additional issues are found, continue with the smaller confirmed set.