@cyperx/clawforge 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/VERSION +1 -0
  4. package/bin/attach.sh +98 -0
  5. package/bin/check-agents.sh +343 -0
  6. package/bin/clawforge +257 -0
  7. package/bin/clawforge-dashboard +0 -0
  8. package/bin/clean.sh +257 -0
  9. package/bin/config.sh +111 -0
  10. package/bin/conflicts.sh +224 -0
  11. package/bin/cost.sh +273 -0
  12. package/bin/dashboard.sh +557 -0
  13. package/bin/diff.sh +109 -0
  14. package/bin/doctor.sh +196 -0
  15. package/bin/eval.sh +217 -0
  16. package/bin/history.sh +91 -0
  17. package/bin/init.sh +182 -0
  18. package/bin/learn.sh +230 -0
  19. package/bin/logs.sh +126 -0
  20. package/bin/memory.sh +207 -0
  21. package/bin/merge-helper.sh +174 -0
  22. package/bin/multi-review.sh +215 -0
  23. package/bin/notify.sh +93 -0
  24. package/bin/on-complete.sh +149 -0
  25. package/bin/parse-cost.sh +205 -0
  26. package/bin/pr.sh +167 -0
  27. package/bin/resume.sh +183 -0
  28. package/bin/review-mode.sh +163 -0
  29. package/bin/review-pr.sh +145 -0
  30. package/bin/routing.sh +88 -0
  31. package/bin/scope-task.sh +169 -0
  32. package/bin/spawn-agent.sh +190 -0
  33. package/bin/sprint.sh +320 -0
  34. package/bin/steer.sh +107 -0
  35. package/bin/stop.sh +136 -0
  36. package/bin/summary.sh +182 -0
  37. package/bin/swarm.sh +525 -0
  38. package/bin/templates.sh +244 -0
  39. package/lib/common.sh +302 -0
  40. package/lib/templates/bugfix.json +6 -0
  41. package/lib/templates/migration.json +7 -0
  42. package/lib/templates/refactor.json +6 -0
  43. package/lib/templates/security-audit.json +5 -0
  44. package/lib/templates/test-coverage.json +6 -0
  45. package/package.json +31 -0
  46. package/registry/conflicts.jsonl +0 -0
  47. package/registry/costs.jsonl +0 -0
  48. package/tui/PRD.md +106 -0
  49. package/tui/agent.go +266 -0
  50. package/tui/animation.go +192 -0
  51. package/tui/dashboard.go +219 -0
  52. package/tui/filter.go +68 -0
  53. package/tui/go.mod +25 -0
  54. package/tui/go.sum +46 -0
  55. package/tui/keybindings.go +229 -0
  56. package/tui/main.go +61 -0
  57. package/tui/model.go +166 -0
  58. package/tui/steer.go +69 -0
  59. package/tui/styles.go +69 -0
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env bash
2
+ # parse-cost.sh — Parse real cost/token data from agent output
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source "${SCRIPT_DIR}/../lib/common.sh"
7
+
8
+ usage() {
9
+ cat <<EOF
10
+ Usage: clawforge parse-cost <id> [options]
11
+
12
+ Parse token usage and cost from a running or completed agent's tmux output.
13
+ Supports Claude Code and Codex output formats.
14
+
15
+ Arguments:
16
+ <id> Task ID or short ID (or "all" for all running tasks)
17
+
18
+ Options:
19
+ --lines <N> Lines to scan from tmux (default: 200)
20
+ --update Write parsed cost to registry costs.jsonl
21
+ --json Output as JSON
22
+ --help Show this help
23
+
24
+ Examples:
25
+ clawforge parse-cost 1
26
+ clawforge parse-cost all --update
27
+ clawforge parse-cost 1 --json
28
+ EOF
29
+ }
30
+
31
+ TASK_REF="" LINES=200 UPDATE=false JSON_OUTPUT=false
32
+
33
+ while [[ $# -gt 0 ]]; do
34
+ case "$1" in
35
+ --lines) LINES="$2"; shift 2 ;;
36
+ --update) UPDATE=true; shift ;;
37
+ --json) JSON_OUTPUT=true; shift ;;
38
+ --help|-h) usage; exit 0 ;;
39
+ --*) log_error "Unknown option: $1"; usage; exit 1 ;;
40
+ *) TASK_REF="$1"; shift ;;
41
+ esac
42
+ done
43
+
44
+ [[ -z "$TASK_REF" ]] && { log_error "Task ID required"; usage; exit 1; }
45
+
46
+ _ensure_registry
47
+ COSTS_FILE="${CLAWFORGE_DIR}/registry/costs.jsonl"
48
+
49
+ # Parse cost from captured output
50
+ parse_cost_from_output() {
51
+ local output="$1"
52
+ local task_id="$2"
53
+
54
+ local total_cost=0
55
+ local input_tokens=0
56
+ local output_tokens=0
57
+ local total_tokens=0
58
+ local found=false
59
+
60
+ # Claude Code patterns:
61
+ # "Total cost: $1.23"
62
+ # "Cost: $0.45"
63
+ # "Input tokens: 12345"
64
+ # "Output tokens: 6789"
65
+ # "> Total cost: $X.XX"
66
+ # "Total input tokens: X"
67
+ local cost_match
68
+ cost_match=$(echo "$output" | grep -ioE '(total )?cost:?\s*\$[0-9]+\.[0-9]+' | tail -1 || true)
69
+ if [[ -n "$cost_match" ]]; then
70
+ total_cost=$(echo "$cost_match" | grep -oE '[0-9]+\.[0-9]+' | tail -1)
71
+ found=true
72
+ fi
73
+
74
+ local input_match
75
+ input_match=$(echo "$output" | grep -ioE '(total )?input tokens?:?\s*[0-9,]+' | tail -1 || true)
76
+ if [[ -n "$input_match" ]]; then
77
+ input_tokens=$(echo "$input_match" | grep -oE '[0-9,]+' | tail -1 | tr -d ',')
78
+ found=true
79
+ fi
80
+
81
+ local output_match
82
+ output_match=$(echo "$output" | grep -ioE '(total )?output tokens?:?\s*[0-9,]+' | tail -1 || true)
83
+ if [[ -n "$output_match" ]]; then
84
+ output_tokens=$(echo "$output_match" | grep -oE '[0-9,]+' | tail -1 | tr -d ',')
85
+ found=true
86
+ fi
87
+
88
+ # Codex patterns:
89
+ # "Tokens used: 12345"
90
+ # "API cost: $1.23"
91
+ if ! $found; then
92
+ local codex_cost
93
+ codex_cost=$(echo "$output" | grep -ioE 'api cost:?\s*\$[0-9]+\.[0-9]+' | tail -1 || true)
94
+ if [[ -n "$codex_cost" ]]; then
95
+ total_cost=$(echo "$codex_cost" | grep -oE '[0-9]+\.[0-9]+' | tail -1)
96
+ found=true
97
+ fi
98
+
99
+ local codex_tokens
100
+ codex_tokens=$(echo "$output" | grep -ioE 'tokens? used:?\s*[0-9,]+' | tail -1 || true)
101
+ if [[ -n "$codex_tokens" ]]; then
102
+ total_tokens=$(echo "$codex_tokens" | grep -oE '[0-9,]+' | tail -1 | tr -d ',')
103
+ found=true
104
+ fi
105
+ fi
106
+
107
+ # Calculate total tokens
108
+ if [[ "$total_tokens" -eq 0 ]]; then
109
+ total_tokens=$((input_tokens + output_tokens))
110
+ fi
111
+
112
+ if $found; then
113
+ jq -cn \
114
+ --arg id "$task_id" \
115
+ --argjson cost "$total_cost" \
116
+ --argjson input "$input_tokens" \
117
+ --argjson output "$output_tokens" \
118
+ --argjson total "$total_tokens" \
119
+ --argjson ts "$(epoch_ms)" \
120
+ '{taskId:$id, totalCost:$cost, inputTokens:$input, outputTokens:$output, totalTokens:$total, timestamp:$ts, source:"parsed"}'
121
+ else
122
+ echo ""
123
+ fi
124
+ }
125
+
126
+ # Process single task
127
+ process_task() {
128
+ local task_ref="$1"
129
+ local task_data=""
130
+
131
+ if [[ "$task_ref" =~ ^[0-9]+$ ]]; then
132
+ task_data=$(jq -r --argjson sid "$task_ref" '.tasks[] | select(.short_id == $sid)' "$REGISTRY_FILE" 2>/dev/null || true)
133
+ fi
134
+ if [[ -z "$task_data" ]]; then
135
+ task_data=$(registry_get "$task_ref" 2>/dev/null || true)
136
+ fi
137
+ if [[ -z "$task_data" ]]; then
138
+ log_warn "Task '$task_ref' not found"
139
+ return 1
140
+ fi
141
+
142
+ local id=$(echo "$task_data" | jq -r '.id')
143
+ local sid=$(echo "$task_data" | jq -r '.short_id // 0')
144
+ local tmux_session=$(echo "$task_data" | jq -r '.tmuxSession // empty')
145
+ [[ -z "$tmux_session" ]] && tmux_session="agent-${id}"
146
+
147
+ # Capture tmux output
148
+ local output=""
149
+ if tmux has-session -t "$tmux_session" 2>/dev/null; then
150
+ output=$(tmux capture-pane -t "$tmux_session" -p -S "-${LINES}" 2>/dev/null || true)
151
+ fi
152
+
153
+ if [[ -z "$output" ]]; then
154
+ if $JSON_OUTPUT; then
155
+ jq -cn --arg id "$id" --argjson sid "$sid" '{taskId:$id, shortId:$sid, status:"no_output"}'
156
+ else
157
+ echo " #${sid} ($id): no output available"
158
+ fi
159
+ return 0
160
+ fi
161
+
162
+ # Parse
163
+ local result
164
+ result=$(parse_cost_from_output "$output" "$id")
165
+
166
+ if [[ -z "$result" ]]; then
167
+ if $JSON_OUTPUT; then
168
+ jq -cn --arg id "$id" --argjson sid "$sid" '{taskId:$id, shortId:$sid, status:"no_cost_found"}'
169
+ else
170
+ echo " #${sid} ($id): no cost data found in output"
171
+ fi
172
+ return 0
173
+ fi
174
+
175
+ # Update registry
176
+ if $UPDATE; then
177
+ echo "$result" >> "$COSTS_FILE"
178
+ local cost=$(echo "$result" | jq -r '.totalCost')
179
+ registry_update "$id" "cost" "\"$cost\"" 2>/dev/null || true
180
+ log_info "Updated cost for #${sid}: \$${cost}"
181
+ fi
182
+
183
+ if $JSON_OUTPUT; then
184
+ echo "$result"
185
+ else
186
+ local cost=$(echo "$result" | jq -r '.totalCost')
187
+ local tokens=$(echo "$result" | jq -r '.totalTokens')
188
+ echo " #${sid} ($id): \$${cost} | ${tokens} tokens"
189
+ fi
190
+ }
191
+
192
+ # Process all or single
193
+ if [[ "$TASK_REF" == "all" ]]; then
194
+ echo "── Parsing costs for all running tasks ──"
195
+ IDS=$(jq -r '.tasks[] | select(.status == "running" or .status == "spawned") | .id' "$REGISTRY_FILE" 2>/dev/null || true)
196
+ if [[ -z "$IDS" ]]; then
197
+ echo "No running tasks."
198
+ exit 0
199
+ fi
200
+ while IFS= read -r id; do
201
+ process_task "$id" || true
202
+ done <<< "$IDS"
203
+ else
204
+ process_task "$TASK_REF"
205
+ fi
package/bin/pr.sh ADDED
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env bash
2
+ # pr.sh — Create a PR from a task's branch
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source "${SCRIPT_DIR}/../lib/common.sh"
7
+
8
+ usage() {
9
+ cat <<EOF
10
+ Usage: clawforge pr <id> [options]
11
+
12
+ Create a GitHub PR from a task's branch. Auto-fills title and body from task data.
13
+
14
+ Arguments:
15
+ <id> Task ID or short ID
16
+
17
+ Options:
18
+ --title <text> Override PR title (default: task description)
19
+ --body <text> Override PR body
20
+ --draft Create as draft PR
21
+ --base <branch> Base branch (default: main)
22
+ --reviewers <list> Comma-separated reviewer list
23
+ --labels <list> Comma-separated label list
24
+ --dry-run Show what would be created
25
+ --help Show this help
26
+
27
+ Examples:
28
+ clawforge pr 1
29
+ clawforge pr 1 --draft
30
+ clawforge pr 1 --reviewers alice,bob --labels enhancement
31
+ EOF
32
+ }
33
+
34
+ TASK_REF="" TITLE="" BODY="" DRAFT=false BASE="" REVIEWERS="" LABELS="" DRY_RUN=false
35
+
36
+ while [[ $# -gt 0 ]]; do
37
+ case "$1" in
38
+ --title) TITLE="$2"; shift 2 ;;
39
+ --body) BODY="$2"; shift 2 ;;
40
+ --draft) DRAFT=true; shift ;;
41
+ --base) BASE="$2"; shift 2 ;;
42
+ --reviewers) REVIEWERS="$2"; shift 2 ;;
43
+ --labels) LABELS="$2"; shift 2 ;;
44
+ --dry-run) DRY_RUN=true; shift ;;
45
+ --help|-h) usage; exit 0 ;;
46
+ --*) log_error "Unknown option: $1"; usage; exit 1 ;;
47
+ *) TASK_REF="$1"; shift ;;
48
+ esac
49
+ done
50
+
51
+ [[ -z "$TASK_REF" ]] && { log_error "Task ID required"; usage; exit 1; }
52
+
53
+ _ensure_registry
54
+
55
+ # Resolve task
56
+ TASK_DATA=""
57
+ if [[ "$TASK_REF" =~ ^[0-9]+$ ]]; then
58
+ TASK_DATA=$(jq -r --argjson sid "$TASK_REF" '.tasks[] | select(.short_id == $sid)' "$REGISTRY_FILE" 2>/dev/null || true)
59
+ fi
60
+ if [[ -z "$TASK_DATA" ]]; then
61
+ TASK_DATA=$(registry_get "$TASK_REF" 2>/dev/null || true)
62
+ fi
63
+ if [[ -z "$TASK_DATA" ]]; then
64
+ log_error "Task '$TASK_REF' not found"
65
+ exit 1
66
+ fi
67
+
68
+ TASK_ID=$(echo "$TASK_DATA" | jq -r '.id')
69
+ DESC=$(echo "$TASK_DATA" | jq -r '.description // "—"')
70
+ BRANCH=$(echo "$TASK_DATA" | jq -r '.branch // empty')
71
+ REPO=$(echo "$TASK_DATA" | jq -r '.repo // empty')
72
+ WORKTREE=$(echo "$TASK_DATA" | jq -r '.worktree // empty')
73
+ MODE=$(echo "$TASK_DATA" | jq -r '.mode // "sprint"')
74
+ SHORT_ID=$(echo "$TASK_DATA" | jq -r '.short_id // 0')
75
+ EXISTING_PR=$(echo "$TASK_DATA" | jq -r '.pr // empty')
76
+
77
+ # Check for existing PR
78
+ if [[ -n "$EXISTING_PR" && "$EXISTING_PR" != "null" ]]; then
79
+ log_warn "Task #${SHORT_ID} already has PR #${EXISTING_PR}"
80
+ echo "View: gh pr view $EXISTING_PR --repo $REPO"
81
+ exit 0
82
+ fi
83
+
84
+ if [[ -z "$BRANCH" ]]; then
85
+ log_error "No branch found for task #${SHORT_ID}"
86
+ exit 1
87
+ fi
88
+
89
+ # Use worktree or repo for git operations
90
+ GIT_DIR=""
91
+ if [[ -n "$WORKTREE" && -d "$WORKTREE" ]]; then
92
+ GIT_DIR="$WORKTREE"
93
+ elif [[ -n "$REPO" && -d "$REPO" ]]; then
94
+ GIT_DIR="$REPO"
95
+ else
96
+ log_error "Neither worktree nor repo directory found"
97
+ exit 1
98
+ fi
99
+
100
+ # Default title from task description
101
+ [[ -z "$TITLE" ]] && TITLE="$DESC"
102
+
103
+ # Default body
104
+ if [[ -z "$BODY" ]]; then
105
+ BODY="## Task
106
+ ${DESC}
107
+
108
+ ## Details
109
+ - Mode: ${MODE}
110
+ - Task ID: #${SHORT_ID} (${TASK_ID})
111
+ - Branch: ${BRANCH}
112
+
113
+ _Created by ClawForge_"
114
+ fi
115
+
116
+ # Default base branch
117
+ if [[ -z "$BASE" ]]; then
118
+ BASE=$(git -C "$GIT_DIR" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||' || echo "main")
119
+ fi
120
+
121
+ # Ensure branch is pushed
122
+ if ! git -C "$GIT_DIR" ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q "$BRANCH"; then
123
+ if $DRY_RUN; then
124
+ echo "[dry-run] Would push branch: $BRANCH"
125
+ else
126
+ log_info "Pushing branch $BRANCH..."
127
+ git -C "$GIT_DIR" push -u origin "$BRANCH" 2>/dev/null || {
128
+ log_error "Failed to push branch. Check if there are commits."
129
+ exit 1
130
+ }
131
+ fi
132
+ fi
133
+
134
+ # Build gh pr create args
135
+ PR_ARGS=(--title "$TITLE" --body "$BODY" --base "$BASE" --head "$BRANCH")
136
+ $DRAFT && PR_ARGS+=(--draft)
137
+ [[ -n "$REVIEWERS" ]] && PR_ARGS+=(--reviewer "$REVIEWERS")
138
+ [[ -n "$LABELS" ]] && PR_ARGS+=(--label "$LABELS")
139
+
140
+ if $DRY_RUN; then
141
+ echo "=== PR Dry Run ==="
142
+ echo " Task: #${SHORT_ID}"
143
+ echo " Title: $TITLE"
144
+ echo " Branch: $BRANCH → $BASE"
145
+ echo " Repo: $GIT_DIR"
146
+ $DRAFT && echo " Draft: yes"
147
+ [[ -n "$REVIEWERS" ]] && echo " Reviewers: $REVIEWERS"
148
+ [[ -n "$LABELS" ]] && echo " Labels: $LABELS"
149
+ echo ""
150
+ echo "Body:"
151
+ echo "$BODY"
152
+ exit 0
153
+ fi
154
+
155
+ # Create PR
156
+ log_info "Creating PR..."
157
+ PR_URL=$(gh pr create "${PR_ARGS[@]}" --repo "$REPO" 2>&1)
158
+ PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$' || true)
159
+
160
+ if [[ -n "$PR_NUMBER" ]]; then
161
+ registry_update "$TASK_ID" "pr" "$PR_NUMBER"
162
+ log_info "PR created: $PR_URL"
163
+ fi
164
+
165
+ echo ""
166
+ echo " #${SHORT_ID} PR created: $PR_URL"
167
+ echo ""
package/bin/resume.sh ADDED
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env bash
2
+ # resume.sh — Resume a failed/timeout/cancelled agent from where it left off
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source "${SCRIPT_DIR}/../lib/common.sh"
7
+
8
+ usage() {
9
+ cat <<EOF
10
+ Usage: clawforge resume <id> [options]
11
+
12
+ Resume a failed/timeout/cancelled task. Reuses the same worktree and branch,
13
+ spawns a fresh tmux session, and injects last N lines of output as context.
14
+
15
+ Arguments:
16
+ <id> Task ID or short ID
17
+
18
+ Options:
19
+ --context-lines <N> Lines of previous output to inject (default: 30)
20
+ --agent <name> Override agent (claude/codex)
21
+ --model <model> Override model
22
+ --message <msg> Additional instructions for the resumed agent
23
+ --dry-run Show what would happen
24
+ --help Show this help
25
+
26
+ Examples:
27
+ clawforge resume 1
28
+ clawforge resume 1 --message "Focus on fixing the test failures"
29
+ clawforge resume sprint-jwt --agent codex
30
+ EOF
31
+ }
32
+
33
+ TASK_REF="" CONTEXT_LINES=30 AGENT_OVERRIDE="" MODEL_OVERRIDE="" MESSAGE="" DRY_RUN=false
34
+
35
+ while [[ $# -gt 0 ]]; do
36
+ case "$1" in
37
+ --context-lines) CONTEXT_LINES="$2"; shift 2 ;;
38
+ --agent) AGENT_OVERRIDE="$2"; shift 2 ;;
39
+ --model) MODEL_OVERRIDE="$2"; shift 2 ;;
40
+ --message) MESSAGE="$2"; shift 2 ;;
41
+ --dry-run) DRY_RUN=true; shift ;;
42
+ --help|-h) usage; exit 0 ;;
43
+ --*) log_error "Unknown option: $1"; usage; exit 1 ;;
44
+ *) TASK_REF="$1"; shift ;;
45
+ esac
46
+ done
47
+
48
+ [[ -z "$TASK_REF" ]] && { log_error "Task ID required"; usage; exit 1; }
49
+
50
+ _ensure_registry
51
+
52
+ # Resolve task
53
+ TASK_DATA=""
54
+ if [[ "$TASK_REF" =~ ^[0-9]+$ ]]; then
55
+ TASK_DATA=$(jq -r --argjson sid "$TASK_REF" '.tasks[] | select(.short_id == $sid)' "$REGISTRY_FILE" 2>/dev/null || true)
56
+ fi
57
+ if [[ -z "$TASK_DATA" ]]; then
58
+ TASK_DATA=$(registry_get "$TASK_REF" 2>/dev/null || true)
59
+ fi
60
+ if [[ -z "$TASK_DATA" ]]; then
61
+ log_error "Task '$TASK_REF' not found"
62
+ exit 1
63
+ fi
64
+
65
+ TASK_ID=$(echo "$TASK_DATA" | jq -r '.id')
66
+ STATUS=$(echo "$TASK_DATA" | jq -r '.status')
67
+ DESC=$(echo "$TASK_DATA" | jq -r '.description')
68
+ WORKTREE=$(echo "$TASK_DATA" | jq -r '.worktree // empty')
69
+ BRANCH=$(echo "$TASK_DATA" | jq -r '.branch // empty')
70
+ REPO=$(echo "$TASK_DATA" | jq -r '.repo // empty')
71
+ AGENT=$(echo "$TASK_DATA" | jq -r '.agent // "claude"')
72
+ MODEL=$(echo "$TASK_DATA" | jq -r '.model // "claude-sonnet-4-5"')
73
+ TMUX_SESSION=$(echo "$TASK_DATA" | jq -r '.tmuxSession // empty')
74
+ SHORT_ID=$(echo "$TASK_DATA" | jq -r '.short_id // 0')
75
+
76
+ # Validate status is resumable
77
+ case "$STATUS" in
78
+ failed|timeout|cancelled) ;;
79
+ running|spawned)
80
+ log_error "Task #${SHORT_ID} is still $STATUS. Use 'steer' instead."
81
+ exit 1 ;;
82
+ done)
83
+ log_error "Task #${SHORT_ID} is already done."
84
+ exit 1 ;;
85
+ *)
86
+ log_error "Task #${SHORT_ID} has status '$STATUS' — not resumable."
87
+ exit 1 ;;
88
+ esac
89
+
90
+ # Apply overrides
91
+ [[ -n "$AGENT_OVERRIDE" ]] && AGENT="$AGENT_OVERRIDE"
92
+ [[ -n "$MODEL_OVERRIDE" ]] && MODEL="$MODEL_OVERRIDE"
93
+
94
+ # Check worktree still exists
95
+ if [[ -z "$WORKTREE" || ! -d "$WORKTREE" ]]; then
96
+ log_error "Worktree not found: ${WORKTREE:-'(none)'}. Cannot resume — worktree was cleaned."
97
+ echo "Tip: Re-run as a fresh sprint instead."
98
+ exit 1
99
+ fi
100
+
101
+ # Kill old tmux session if lingering
102
+ if [[ -n "$TMUX_SESSION" ]]; then
103
+ tmux kill-session -t "$TMUX_SESSION" 2>/dev/null || true
104
+ fi
105
+ [[ -z "$TMUX_SESSION" ]] && TMUX_SESSION="agent-${TASK_ID}"
106
+
107
+ # Capture previous output for context
108
+ PREV_OUTPUT=""
109
+ if tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then
110
+ PREV_OUTPUT=$(tmux capture-pane -t "$TMUX_SESSION" -p -S "-${CONTEXT_LINES}" 2>/dev/null || true)
111
+ fi
112
+
113
+ # Build resume prompt
114
+ RESUME_PROMPT="You are resuming a previously ${STATUS} task.
115
+
116
+ Original task: ${DESC}
117
+ Branch: ${BRANCH}
118
+ "
119
+
120
+ if [[ -n "$PREV_OUTPUT" ]]; then
121
+ RESUME_PROMPT+="
122
+ Previous session output (last ${CONTEXT_LINES} lines):
123
+ \`\`\`
124
+ ${PREV_OUTPUT}
125
+ \`\`\`
126
+ "
127
+ fi
128
+
129
+ if [[ -n "$MESSAGE" ]]; then
130
+ RESUME_PROMPT+="
131
+ Additional instructions: ${MESSAGE}
132
+ "
133
+ fi
134
+
135
+ RESUME_PROMPT+="
136
+ Continue from where the previous agent left off. Check git status and recent changes first.
137
+
138
+ When complete:
139
+ 1. Commit your changes with a descriptive message
140
+ 2. Push the branch: git push origin ${BRANCH}
141
+ 3. Create a PR: gh pr create --fill --base main"
142
+
143
+ # Dry run
144
+ if $DRY_RUN; then
145
+ echo "=== Resume Dry Run ==="
146
+ echo " Task: #${SHORT_ID} ($TASK_ID)"
147
+ echo " Status: $STATUS → running"
148
+ echo " Worktree: $WORKTREE"
149
+ echo " Branch: $BRANCH"
150
+ echo " Agent: $AGENT ($MODEL)"
151
+ echo " tmux: $TMUX_SESSION"
152
+ echo " Context: ${CONTEXT_LINES} lines"
153
+ [[ -n "$MESSAGE" ]] && echo " Message: $MESSAGE"
154
+ echo ""
155
+ echo "Resume prompt preview:"
156
+ echo "$RESUME_PROMPT" | head -20
157
+ exit 0
158
+ fi
159
+
160
+ # Spawn fresh agent in existing worktree
161
+ log_info "Resuming #${SHORT_ID} in $WORKTREE..."
162
+
163
+ if [[ "$AGENT" == "claude" ]]; then
164
+ AGENT_CMD="claude --model ${MODEL} --dangerously-skip-permissions -p \"$(echo "$RESUME_PROMPT" | sed 's/"/\\"/g')\""
165
+ else
166
+ AGENT_CMD="codex --model ${MODEL} --dangerously-bypass-approvals-and-sandbox \"$(echo "$RESUME_PROMPT" | sed 's/"/\\"/g')\""
167
+ fi
168
+
169
+ tmux new-session -d -s "$TMUX_SESSION" -c "$WORKTREE" "$AGENT_CMD"
170
+
171
+ # Update registry
172
+ registry_update "$TASK_ID" "status" '"running"'
173
+ registry_update "$TASK_ID" "resumedAt" "$(epoch_ms)"
174
+ RETRIES=$(echo "$TASK_DATA" | jq -r '.retries // 0')
175
+ registry_update "$TASK_ID" "retries" "$((RETRIES + 1))"
176
+
177
+ echo ""
178
+ echo " #${SHORT_ID} resumed $(basename "$REPO") \"$(echo "$DESC" | head -c 50)\""
179
+ echo ""
180
+ echo " Agent running in tmux: $TMUX_SESSION"
181
+ echo " Attach: clawforge attach $SHORT_ID"
182
+ echo " Steer: clawforge steer $SHORT_ID \"<message>\""
183
+ echo " Logs: clawforge logs $SHORT_ID"