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) |
|
||||
| 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) |
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ story-automator monitor-session <session_name> [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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <command> [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 <command> [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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 <action> [args]")
|
||||
fmt.Fprintln(os.Stderr, "")
|
||||
fmt.Fprintln(os.Stderr, "Actions:")
|
||||
fmt.Fprintln(os.Stderr, " sprint-status get <story_key>")
|
||||
fmt.Fprintln(os.Stderr, " sprint-status exists")
|
||||
fmt.Fprintln(os.Stderr, " sprint-status check-epic <epic>")
|
||||
fmt.Fprintln(os.Stderr, " parse-output <file> <step>")
|
||||
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 <folder>")
|
||||
fmt.Fprintln(os.Stderr, " state-latest <folder> [status]")
|
||||
fmt.Fprintln(os.Stderr, " state-latest-incomplete <folder>")
|
||||
fmt.Fprintln(os.Stderr, " state-summary <file>")
|
||||
fmt.Fprintln(os.Stderr, " state-update <file> --set k=v")
|
||||
fmt.Fprintln(os.Stderr, " escalate <trigger> <context>")
|
||||
fmt.Fprintln(os.Stderr, " commit-ready <story_id>")
|
||||
fmt.Fprintln(os.Stderr, " normalize-key <input> [--to id|key|prefix|json]")
|
||||
fmt.Fprintln(os.Stderr, " story-file-status <story>")
|
||||
fmt.Fprintln(os.Stderr, " verify-code-review <story>")
|
||||
fmt.Fprintln(os.Stderr, " check-epic-complete <epic> <story> [--state-file path]")
|
||||
fmt.Fprintln(os.Stderr, " get-epic-stories <epic> [--state-file path]")
|
||||
fmt.Fprintln(os.Stderr, " check-blocking <story_id>")
|
||||
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 <action> [args]")
|
||||
fmt.Fprintln(w, "")
|
||||
fmt.Fprintln(w, "Actions:")
|
||||
fmt.Fprintln(w, " sprint-status get <story_key>")
|
||||
fmt.Fprintln(w, " sprint-status exists")
|
||||
fmt.Fprintln(w, " sprint-status check-epic <epic>")
|
||||
fmt.Fprintln(w, " parse-output <file> <step>")
|
||||
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 <folder>")
|
||||
fmt.Fprintln(w, " state-latest <folder> [status]")
|
||||
fmt.Fprintln(w, " state-latest-incomplete <folder>")
|
||||
fmt.Fprintln(w, " state-summary <file>")
|
||||
fmt.Fprintln(w, " state-update <file> --set k=v")
|
||||
fmt.Fprintln(w, " escalate <trigger> <context>")
|
||||
fmt.Fprintln(w, " commit-ready <story_id>")
|
||||
fmt.Fprintln(w, " normalize-key <input> [--to id|key|prefix|json]")
|
||||
fmt.Fprintln(w, " story-file-status <story>")
|
||||
fmt.Fprintln(w, " verify-code-review <story>")
|
||||
fmt.Fprintln(w, " check-epic-complete <epic> <story> [--state-file path]")
|
||||
fmt.Fprintln(w, " get-epic-stories <epic> [--state-file path]")
|
||||
fmt.Fprintln(w, " check-blocking <story_id>")
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <action> [args...]")
|
||||
fmt.Fprintln(os.Stderr, "")
|
||||
fmt.Fprintln(os.Stderr, "Actions:")
|
||||
fmt.Fprintln(os.Stderr, " spawn <step> <epic> <story_id> [--command \"...\"] [--cycle N] [--agent TYPE]")
|
||||
fmt.Fprintln(os.Stderr, " name <step> <epic> <story_id> [--cycle N]")
|
||||
fmt.Fprintln(os.Stderr, " list [--project-only]")
|
||||
fmt.Fprintln(os.Stderr, " kill <session_name>")
|
||||
fmt.Fprintln(os.Stderr, " kill-all [--project-only]")
|
||||
fmt.Fprintln(os.Stderr, " exists <session_name>")
|
||||
fmt.Fprintln(os.Stderr, " build-cmd <step> <story_id> [--agent TYPE] [extra_instruction]")
|
||||
fmt.Fprintln(os.Stderr, " project-slug")
|
||||
fmt.Fprintln(os.Stderr, " project-hash")
|
||||
fmt.Fprintln(os.Stderr, " story-suffix <story_id>")
|
||||
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 <action> [args...]")
|
||||
fmt.Fprintln(w, "")
|
||||
fmt.Fprintln(w, "Actions:")
|
||||
fmt.Fprintln(w, " spawn <step> <epic> <story_id> [--command \"...\"] [--cycle N] [--agent TYPE]")
|
||||
fmt.Fprintln(w, " name <step> <epic> <story_id> [--cycle N]")
|
||||
fmt.Fprintln(w, " list [--project-only]")
|
||||
fmt.Fprintln(w, " kill <session_name>")
|
||||
fmt.Fprintln(w, " kill-all [--project-only]")
|
||||
fmt.Fprintln(w, " exists <session_name>")
|
||||
fmt.Fprintln(w, " build-cmd <step> <story_id> [--agent TYPE] [extra_instruction]")
|
||||
fmt.Fprintln(w, " project-slug")
|
||||
fmt.Fprintln(w, " project-hash")
|
||||
fmt.Fprintln(w, " story-suffix <story_id>")
|
||||
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 <session_name> [options]")
|
||||
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 := ""
|
||||
maxPolls := 30
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <story_id>")
|
||||
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":
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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='[]'
|
||||
|
|
|
|||
|
|
@ -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 '' "/<!-- Progress rows/i\\
|
||||
| {story_id} | - | - | - | - | - | in-progress |" "$state_file"
|
||||
tmp_state=$(mktemp)
|
||||
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}**"
|
||||
|
|
@ -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"`:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -94,16 +94,8 @@
|
|||
</action>
|
||||
|
||||
<check if="total_issues_found lt 3">
|
||||
<critical>NOT LOOKING HARD ENOUGH - Find more problems!</critical>
|
||||
<action>Re-examine code for:
|
||||
- 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>
|
||||
<action>Re-examine the review surface for missed issues, but report only verified findings.</action>
|
||||
<action>If no additional issues are found, continue with the smaller confirmed set.</action>
|
||||
</check>
|
||||
</step>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue