@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.
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/VERSION +1 -0
- package/bin/attach.sh +98 -0
- package/bin/check-agents.sh +343 -0
- package/bin/clawforge +257 -0
- package/bin/clawforge-dashboard +0 -0
- package/bin/clean.sh +257 -0
- package/bin/config.sh +111 -0
- package/bin/conflicts.sh +224 -0
- package/bin/cost.sh +273 -0
- package/bin/dashboard.sh +557 -0
- package/bin/diff.sh +109 -0
- package/bin/doctor.sh +196 -0
- package/bin/eval.sh +217 -0
- package/bin/history.sh +91 -0
- package/bin/init.sh +182 -0
- package/bin/learn.sh +230 -0
- package/bin/logs.sh +126 -0
- package/bin/memory.sh +207 -0
- package/bin/merge-helper.sh +174 -0
- package/bin/multi-review.sh +215 -0
- package/bin/notify.sh +93 -0
- package/bin/on-complete.sh +149 -0
- package/bin/parse-cost.sh +205 -0
- package/bin/pr.sh +167 -0
- package/bin/resume.sh +183 -0
- package/bin/review-mode.sh +163 -0
- package/bin/review-pr.sh +145 -0
- package/bin/routing.sh +88 -0
- package/bin/scope-task.sh +169 -0
- package/bin/spawn-agent.sh +190 -0
- package/bin/sprint.sh +320 -0
- package/bin/steer.sh +107 -0
- package/bin/stop.sh +136 -0
- package/bin/summary.sh +182 -0
- package/bin/swarm.sh +525 -0
- package/bin/templates.sh +244 -0
- package/lib/common.sh +302 -0
- package/lib/templates/bugfix.json +6 -0
- package/lib/templates/migration.json +7 -0
- package/lib/templates/refactor.json +6 -0
- package/lib/templates/security-audit.json +5 -0
- package/lib/templates/test-coverage.json +6 -0
- package/package.json +31 -0
- package/registry/conflicts.jsonl +0 -0
- package/registry/costs.jsonl +0 -0
- package/tui/PRD.md +106 -0
- package/tui/agent.go +266 -0
- package/tui/animation.go +192 -0
- package/tui/dashboard.go +219 -0
- package/tui/filter.go +68 -0
- package/tui/go.mod +25 -0
- package/tui/go.sum +46 -0
- package/tui/keybindings.go +229 -0
- package/tui/main.go +61 -0
- package/tui/model.go +166 -0
- package/tui/steer.go +69 -0
- 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"
|