@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,190 @@
1
+ #!/usr/bin/env bash
2
+ # spawn-agent.sh — Module 2: Create worktree + tmux + launch coding agent
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source "${SCRIPT_DIR}/../lib/common.sh"
7
+
8
+ # ── Help ───────────────────────────────────────────────────────────────
9
+ usage() {
10
+ cat <<EOF
11
+ Usage: spawn-agent.sh --repo <path> --branch <name> --task <description> [options]
12
+
13
+ Options:
14
+ --repo <path> Path to the git repository (required)
15
+ --branch <name> Branch name to create (required)
16
+ --task <description> Task description for the agent (required)
17
+ --agent <name> Agent to use: claude or codex (default: auto-detect)
18
+ --model <model> Model override
19
+ --effort <level> Effort level: high, medium, low (default: high)
20
+ --dry-run Do everything except launch the agent
21
+ --help Show this help
22
+ EOF
23
+ }
24
+
25
+ # ── Parse args ─────────────────────────────────────────────────────────
26
+ REPO="" BRANCH="" TASK="" AGENT="" MODEL="" EFFORT="" DRY_RUN=false
27
+
28
+ while [[ $# -gt 0 ]]; do
29
+ case "$1" in
30
+ --repo) REPO="$2"; shift 2 ;;
31
+ --branch) BRANCH="$2"; shift 2 ;;
32
+ --task) TASK="$2"; shift 2 ;;
33
+ --agent) AGENT="$2"; shift 2 ;;
34
+ --model) MODEL="$2"; shift 2 ;;
35
+ --effort) EFFORT="$2"; shift 2 ;;
36
+ --dry-run) DRY_RUN=true; shift ;;
37
+ --help|-h) usage; exit 0 ;;
38
+ *) log_error "Unknown option: $1"; usage; exit 1 ;;
39
+ esac
40
+ done
41
+
42
+ # ── Validate ───────────────────────────────────────────────────────────
43
+ [[ -z "$REPO" ]] && { log_error "--repo is required"; usage; exit 1; }
44
+ [[ -z "$BRANCH" ]] && { log_error "--branch is required"; usage; exit 1; }
45
+ [[ -z "$TASK" ]] && { log_error "--task is required"; usage; exit 1; }
46
+ [[ -d "$REPO/.git" ]] || [[ -f "$REPO/.git" ]] || { log_error "Not a git repo: $REPO"; exit 1; }
47
+
48
+ # ── Resolve settings ──────────────────────────────────────────────────
49
+ RESOLVED_AGENT=$(detect_agent "${AGENT:-}")
50
+ EFFORT="${EFFORT:-$(config_get default_effort high)}"
51
+
52
+ if [[ -z "$MODEL" ]]; then
53
+ if [[ "$RESOLVED_AGENT" == "claude" ]]; then
54
+ MODEL=$(config_get default_model_claude "claude-sonnet-4-5")
55
+ else
56
+ MODEL=$(config_get default_model_codex "gpt-5.3-codex")
57
+ fi
58
+ fi
59
+
60
+ SAFE_BRANCH=$(sanitize_branch "$BRANCH")
61
+ WORKTREE_BASE=$(config_get worktree_base "../worktrees")
62
+ REPO_ABS=$(cd "$REPO" && pwd)
63
+
64
+ # Resolve worktree path relative to repo
65
+ if [[ "$WORKTREE_BASE" == ../* ]]; then
66
+ WORKTREE_DIR="$(cd "$REPO_ABS" && cd .. && pwd)/worktrees/${SAFE_BRANCH}"
67
+ else
68
+ WORKTREE_DIR="${WORKTREE_BASE}/${SAFE_BRANCH}"
69
+ fi
70
+
71
+ TMUX_SESSION="agent-${SAFE_BRANCH}"
72
+ MAX_RETRIES=$(config_get max_retries 3)
73
+
74
+ log_info "Spawning agent: $RESOLVED_AGENT ($MODEL)"
75
+ log_info "Repo: $REPO_ABS"
76
+ log_info "Branch: $BRANCH → worktree: $WORKTREE_DIR"
77
+ log_info "tmux session: $TMUX_SESSION"
78
+
79
+ # ── Step 1: Create worktree ───────────────────────────────────────────
80
+ if [[ -d "$WORKTREE_DIR" ]]; then
81
+ log_warn "Worktree already exists: $WORKTREE_DIR"
82
+ else
83
+ log_info "Creating worktree..."
84
+ mkdir -p "$(dirname "$WORKTREE_DIR")"
85
+ git -C "$REPO_ABS" worktree add "$WORKTREE_DIR" -b "$BRANCH" 2>/dev/null || \
86
+ git -C "$REPO_ABS" worktree add "$WORKTREE_DIR" "$BRANCH" 2>/dev/null || \
87
+ git -C "$REPO_ABS" worktree add "$WORKTREE_DIR" -B "$BRANCH"
88
+ fi
89
+
90
+ # ── Step 2: Install deps ──────────────────────────────────────────────
91
+ if [[ -f "$WORKTREE_DIR/package.json" ]]; then
92
+ log_info "Installing dependencies..."
93
+ if ! $DRY_RUN; then
94
+ if [[ -f "$WORKTREE_DIR/pnpm-lock.yaml" ]]; then
95
+ (cd "$WORKTREE_DIR" && pnpm install --frozen-lockfile 2>/dev/null || pnpm install) || true
96
+ else
97
+ (cd "$WORKTREE_DIR" && npm install) || true
98
+ fi
99
+ else
100
+ log_info "[dry-run] Would install deps in $WORKTREE_DIR"
101
+ fi
102
+ fi
103
+
104
+ # ── Step 3: Build prompt (with memory injection) ─────────────────────
105
+ MEMORY_SECTION=""
106
+ MEMORY_BASE="$HOME/.clawforge/memory"
107
+ REPO_NAME=$(basename "$REPO_ABS")
108
+ # Try git remote for repo name
109
+ REMOTE_URL=$(git -C "$REPO_ABS" config --get remote.origin.url 2>/dev/null || true)
110
+ [[ -n "$REMOTE_URL" ]] && REPO_NAME=$(basename "$REMOTE_URL" .git)
111
+ MEMORY_FILE="${MEMORY_BASE}/${REPO_NAME}.jsonl"
112
+
113
+ if [[ -f "$MEMORY_FILE" ]] && [[ -s "$MEMORY_FILE" ]]; then
114
+ MEMORIES=$(tail -20 "$MEMORY_FILE" | jq -r '.text' 2>/dev/null || true)
115
+ if [[ -n "$MEMORIES" ]]; then
116
+ MEMORY_SECTION="
117
+ ## Project Notes
118
+ $MEMORIES
119
+ "
120
+ log_info "Injected $(echo "$MEMORIES" | wc -l | tr -d ' ') memories into prompt"
121
+ fi
122
+ fi
123
+
124
+ FULL_PROMPT="${MEMORY_SECTION}$TASK
125
+
126
+ When complete:
127
+ 1. Commit your changes with a descriptive message
128
+ 2. Push the branch: git push origin $BRANCH
129
+ 3. Create a PR: gh pr create --fill --base main"
130
+
131
+ # ── Step 4: Register task ─────────────────────────────────────────────
132
+ NOW=$(epoch_ms)
133
+ TASK_JSON=$(jq -n \
134
+ --arg id "$SAFE_BRANCH" \
135
+ --arg tmux "$TMUX_SESSION" \
136
+ --arg agent "$RESOLVED_AGENT" \
137
+ --arg model "$MODEL" \
138
+ --arg desc "$TASK" \
139
+ --arg repo "$REPO_ABS" \
140
+ --arg wt "$WORKTREE_DIR" \
141
+ --arg branch "$BRANCH" \
142
+ --argjson started "$NOW" \
143
+ --argjson maxRetries "$MAX_RETRIES" \
144
+ '{
145
+ id: $id,
146
+ tmuxSession: $tmux,
147
+ agent: $agent,
148
+ model: $model,
149
+ description: $desc,
150
+ repo: $repo,
151
+ worktree: $wt,
152
+ branch: $branch,
153
+ startedAt: $started,
154
+ status: "spawned",
155
+ retries: 0,
156
+ maxRetries: $maxRetries,
157
+ pr: null,
158
+ checks: {},
159
+ completedAt: null,
160
+ note: null
161
+ }')
162
+
163
+ registry_add "$TASK_JSON"
164
+
165
+ # ── Step 5: Launch agent in tmux ──────────────────────────────────────
166
+ if $DRY_RUN; then
167
+ log_info "[dry-run] Would create tmux session '$TMUX_SESSION' and launch $RESOLVED_AGENT"
168
+ log_info "[dry-run] Working directory: $WORKTREE_DIR"
169
+ log_info "[dry-run] Prompt: $(echo "$FULL_PROMPT" | head -1)..."
170
+ registry_update "$SAFE_BRANCH" "status" '"running"'
171
+ echo "$TASK_JSON"
172
+ exit 0
173
+ fi
174
+
175
+ # Kill existing session if present (idempotent)
176
+ tmux kill-session -t "$TMUX_SESSION" 2>/dev/null || true
177
+
178
+ # Build agent command
179
+ if [[ "$RESOLVED_AGENT" == "claude" ]]; then
180
+ AGENT_CMD="claude --model ${MODEL} --dangerously-skip-permissions -p \"$(echo "$FULL_PROMPT" | sed 's/"/\\"/g')\""
181
+ elif [[ "$RESOLVED_AGENT" == "codex" ]]; then
182
+ AGENT_CMD="codex --model ${MODEL} --dangerously-bypass-approvals-and-sandbox \"$(echo "$FULL_PROMPT" | sed 's/"/\\"/g')\""
183
+ fi
184
+
185
+ # Create tmux session and launch
186
+ tmux new-session -d -s "$TMUX_SESSION" -c "$WORKTREE_DIR" "$AGENT_CMD"
187
+ registry_update "$SAFE_BRANCH" "status" '"running"'
188
+
189
+ log_info "Agent spawned successfully in tmux session: $TMUX_SESSION"
190
+ echo "$TASK_JSON"
package/bin/sprint.sh ADDED
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env bash
2
+ # sprint.sh — Sprint mode: single agent, full dev cycle
3
+ # Usage: clawforge sprint [repo] "<task>" [flags]
4
+ set -euo pipefail
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ source "${SCRIPT_DIR}/../lib/common.sh"
8
+ source "${SCRIPT_DIR}/routing.sh"
9
+
10
+ # ── Help ───────────────────────────────────────────────────────────────
11
+ usage() {
12
+ cat <<EOF
13
+ Usage: clawforge sprint [repo] "<task>" [flags]
14
+
15
+ The workhorse. Single agent, full dev cycle.
16
+
17
+ Arguments:
18
+ [repo] Path to git repository (default: auto-detect from cwd)
19
+ "<task>" Task description (required)
20
+
21
+ Flags:
22
+ --quick Patch mode: auto-branch, auto-merge, skip review, targeted tests
23
+ --branch <name> Override auto-generated branch name
24
+ --agent <name> Agent to use: claude or codex (default: auto-detect)
25
+ --model <model> Model override
26
+ --routing <strategy> Model routing: auto, cheap, or quality (--model overrides)
27
+ --auto-merge Merge automatically if CI + review pass
28
+ --template <name> Apply a task template (overrides defaults, CLI flags override template)
29
+ --ci-loop Enable CI auto-fix feedback loop
30
+ --max-ci-retries <N> Max CI auto-fix retries (default: 3)
31
+ --budget <dollars> Kill agent if cost exceeds budget
32
+ --json Output structured JSON
33
+ --notify Send OpenClaw event on completion
34
+ --webhook <url> POST completion payload to URL
35
+ --auto-clean Clean up worktree+session on completion
36
+ --timeout <minutes> Kill agent after N minutes
37
+ --dry-run Preview what would happen
38
+ --help Show this help
39
+
40
+ Examples:
41
+ clawforge sprint "Add JWT authentication middleware"
42
+ clawforge sprint ~/github/api "Fix null pointer in UserService" --quick
43
+ clawforge sprint "Add rate limiter" --branch feat/rate-limit --agent codex
44
+ clawforge sprint --template refactor "Refactor auth module"
45
+ clawforge sprint "Fix login bug" --ci-loop --budget 5.00
46
+ EOF
47
+ }
48
+
49
+ # ── Parse args ────────────────────────────────────────────────────────
50
+ REPO="" TASK="" BRANCH="" AGENT="" MODEL="" QUICK=false AUTO_MERGE=false DRY_RUN=false
51
+ TEMPLATE="" CI_LOOP=false MAX_CI_RETRIES=3 BUDGET="" JSON_OUTPUT=false NOTIFY=false WEBHOOK="" ROUTING=""
52
+ AUTO_CLEAN=false
53
+ TIMEOUT_MIN=""
54
+ POSITIONAL=()
55
+
56
+ while [[ $# -gt 0 ]]; do
57
+ case "$1" in
58
+ --quick) QUICK=true; shift ;;
59
+ --branch) BRANCH="$2"; shift 2 ;;
60
+ --agent) AGENT="$2"; shift 2 ;;
61
+ --model) MODEL="$2"; shift 2 ;;
62
+ --routing) ROUTING="$2"; shift 2 ;;
63
+ --auto-clean) AUTO_CLEAN=true; shift ;;
64
+ --timeout) TIMEOUT_MIN="$2"; shift 2 ;;
65
+ --auto-merge) AUTO_MERGE=true; shift ;;
66
+ --template) TEMPLATE="$2"; shift 2 ;;
67
+ --ci-loop) CI_LOOP=true; shift ;;
68
+ --max-ci-retries) MAX_CI_RETRIES="$2"; shift 2 ;;
69
+ --budget) BUDGET="$2"; shift 2 ;;
70
+ --json) JSON_OUTPUT=true; shift ;;
71
+ --notify) NOTIFY=true; shift ;;
72
+ --webhook) WEBHOOK="$2"; shift 2 ;;
73
+ --dry-run) DRY_RUN=true; shift ;;
74
+ --help|-h) usage; exit 0 ;;
75
+ --*) log_error "Unknown option: $1"; usage; exit 1 ;;
76
+ *) POSITIONAL+=("$1"); shift ;;
77
+ esac
78
+ done
79
+
80
+ # ── Apply template (template < CLI flags) ─────────────────────────────
81
+ if [[ -n "$TEMPLATE" ]]; then
82
+ TMPL_FILE=""
83
+ if [[ -f "${CLAWFORGE_DIR}/lib/templates/${TEMPLATE}.json" ]]; then
84
+ TMPL_FILE="${CLAWFORGE_DIR}/lib/templates/${TEMPLATE}.json"
85
+ elif [[ -f "${HOME}/.clawforge/templates/${TEMPLATE}.json" ]]; then
86
+ TMPL_FILE="${HOME}/.clawforge/templates/${TEMPLATE}.json"
87
+ else
88
+ log_error "Template '$TEMPLATE' not found"; exit 1
89
+ fi
90
+ log_info "Applying template: $TEMPLATE"
91
+ # Template sets defaults; CLI flags override
92
+ TMPL_AUTO_MERGE=$(jq -r '.autoMerge // false' "$TMPL_FILE")
93
+ TMPL_CI_LOOP=$(jq -r '.ciLoop // false' "$TMPL_FILE")
94
+ TMPL_QUICK=$(jq -r '.quick // false' "$TMPL_FILE")
95
+ # Only apply template values if CLI didn't set them explicitly
96
+ [[ "$TMPL_AUTO_MERGE" == "true" ]] && AUTO_MERGE=true
97
+ [[ "$TMPL_CI_LOOP" == "true" ]] && CI_LOOP=true
98
+ [[ "$TMPL_QUICK" == "true" ]] && QUICK=true
99
+ fi
100
+
101
+ # Parse positional args: [repo] "<task>"
102
+ case ${#POSITIONAL[@]} in
103
+ 0) log_error "Task description is required"; usage; exit 1 ;;
104
+ 1) TASK="${POSITIONAL[0]}" ;;
105
+ 2) REPO="${POSITIONAL[0]}"; TASK="${POSITIONAL[1]}" ;;
106
+ *) log_error "Too many positional arguments"; usage; exit 1 ;;
107
+ esac
108
+
109
+ # ── Resolve repo ──────────────────────────────────────────────────────
110
+ if [[ -z "$REPO" ]]; then
111
+ REPO=$(detect_repo) || { log_error "No --repo and no git repo found from cwd"; exit 1; }
112
+ fi
113
+ REPO_ABS=$(cd "$REPO" && pwd)
114
+
115
+ # ── Resolve branch ────────────────────────────────────────────────────
116
+ if [[ -z "$BRANCH" ]]; then
117
+ if $QUICK; then
118
+ BRANCH=$(auto_branch_name "quick" "$TASK" "$REPO_ABS")
119
+ else
120
+ BRANCH=$(auto_branch_name "sprint" "$TASK" "$REPO_ABS")
121
+ fi
122
+ fi
123
+
124
+ # ── Quick mode overrides ─────────────────────────────────────────────
125
+ if $QUICK; then
126
+ AUTO_MERGE=true
127
+ fi
128
+
129
+ # ── Resolve agent + model ────────────────────────────────────────────
130
+ RESOLVED_AGENT=$(detect_agent "${AGENT:-}")
131
+ MODEL_OVERRIDE="$MODEL" # preserve explicit --model
132
+ if [[ -z "$MODEL" ]]; then
133
+ if [[ "$RESOLVED_AGENT" == "claude" ]]; then
134
+ MODEL=$(config_get default_model_claude "claude-sonnet-4-5")
135
+ else
136
+ MODEL=$(config_get default_model_codex "gpt-5.3-codex")
137
+ fi
138
+ fi
139
+
140
+ # ── Load routing ────────────────────────────────────────────────────
141
+ if [[ -n "$ROUTING" ]]; then
142
+ load_routing "$ROUTING"
143
+ log_info "Routing: strategy=$ROUTING"
144
+ fi
145
+
146
+ # ── Assign short ID ──────────────────────────────────────────────────
147
+ SHORT_ID=$(_next_short_id)
148
+ SAFE_BRANCH=$(sanitize_branch "$BRANCH")
149
+ MODE="sprint"
150
+ $QUICK && MODE="quick"
151
+
152
+ # ── Log intent ────────────────────────────────────────────────────────
153
+ log_info "Sprint mode ($MODE): $TASK"
154
+ log_info "Repo: $REPO_ABS"
155
+ log_info "Branch: $BRANCH (short ID: #$SHORT_ID)"
156
+ log_info "Agent: $RESOLVED_AGENT ($MODEL)"
157
+ $QUICK && log_info "Quick mode: auto-merge=true, skip-review=true"
158
+ $AUTO_MERGE && log_info "Auto-merge enabled"
159
+
160
+ # ── Dry-run ───────────────────────────────────────────────────────────
161
+ if $DRY_RUN; then
162
+ echo "=== Sprint Dry Run ==="
163
+ echo " Mode: $MODE"
164
+ echo " Task: $TASK"
165
+ echo " Repo: $REPO_ABS"
166
+ echo " Branch: $BRANCH"
167
+ echo " Agent: $RESOLVED_AGENT ($MODEL)"
168
+ echo " Short ID: #$SHORT_ID"
169
+ echo " Auto-merge: $AUTO_MERGE"
170
+ echo " Quick: $QUICK"
171
+ [[ -n "$ROUTING" ]] && echo " Routing: $ROUTING"
172
+ echo ""
173
+ echo "Would execute:"
174
+ echo " 1. Scope task"
175
+ echo " 2. Create worktree + spawn agent"
176
+ if $QUICK; then
177
+ echo " 3. Auto-merge on CI pass (skip review)"
178
+ else
179
+ echo " 3. Wait for PR → review → merge"
180
+ fi
181
+ exit 0
182
+ fi
183
+
184
+ # ── Escalation check ──────────────────────────────────────────────────
185
+ # Quick mode: detect if task looks too complex for a patch
186
+ if $QUICK; then
187
+ WORD_COUNT=$(echo "$TASK" | wc -w | tr -d ' ')
188
+ if [[ "$WORD_COUNT" -gt 20 ]]; then
189
+ log_warn "This task description is long for --quick mode."
190
+ echo " Tip: This looks bigger than a patch. Consider running as full sprint:"
191
+ echo " clawforge sprint $(printf '%q' "$TASK")"
192
+ fi
193
+ fi
194
+
195
+ # ── Step 1: Scope ─────────────────────────────────────────────────────
196
+ log_info "Step 1: Scoping task..."
197
+ # Apply routing model for scope phase (--model overrides routing)
198
+ if [[ -n "$ROUTING" && -z "$MODEL_OVERRIDE" ]]; then
199
+ SCOPE_MODEL=$(get_model_for_phase "scope")
200
+ [[ -n "$SCOPE_MODEL" ]] && log_info "Routing: scope → $SCOPE_MODEL"
201
+ fi
202
+ PROMPT=$("${SCRIPT_DIR}/scope-task.sh" --task "$TASK" 2>/dev/null || echo "$TASK")
203
+
204
+ # ── Step 2: Spawn ─────────────────────────────────────────────────────
205
+ # Disk space check
206
+ disk_check "$REPO_ABS" || { log_error "Aborting due to low disk space"; exit 1; }
207
+
208
+ log_info "Step 2: Spawning agent..."
209
+ # Apply routing model for implement phase (--model overrides routing)
210
+ SPAWN_MODEL="$MODEL"
211
+ if [[ -n "$ROUTING" && -z "$MODEL_OVERRIDE" ]]; then
212
+ IMPL_MODEL=$(get_model_for_phase "implement")
213
+ [[ -n "$IMPL_MODEL" ]] && SPAWN_MODEL="$IMPL_MODEL" && log_info "Routing: implement → $IMPL_MODEL"
214
+ fi
215
+ SPAWN_ARGS=(--repo "$REPO_ABS" --branch "$BRANCH" --task "$PROMPT")
216
+ [[ -n "${AGENT:-}" ]] && SPAWN_ARGS+=(--agent "$AGENT")
217
+ [[ -n "${SPAWN_MODEL:-}" ]] && SPAWN_ARGS+=(--model "$SPAWN_MODEL")
218
+
219
+ TASK_JSON=$("${SCRIPT_DIR}/spawn-agent.sh" "${SPAWN_ARGS[@]}" 2>/dev/null || true)
220
+
221
+ # ── Step 3: Enhance registry with mode data ───────────────────────────
222
+ registry_update "$SAFE_BRANCH" "short_id" "$SHORT_ID"
223
+ registry_update "$SAFE_BRANCH" "mode" "\"$MODE\""
224
+ registry_update "$SAFE_BRANCH" "files_touched" '[]'
225
+ registry_update "$SAFE_BRANCH" "ci_retries" '0'
226
+ registry_update "$SAFE_BRANCH" "max_ci_retries" "$MAX_CI_RETRIES"
227
+ $AUTO_MERGE && registry_update "$SAFE_BRANCH" "auto_merge" 'true'
228
+ $QUICK && registry_update "$SAFE_BRANCH" "skip_review" 'true'
229
+ $CI_LOOP && registry_update "$SAFE_BRANCH" "ci_loop" 'true'
230
+ [[ -n "$BUDGET" ]] && registry_update "$SAFE_BRANCH" "budget" "$BUDGET"
231
+
232
+
233
+ # ── Signal trap cleanup ──────────────────────────────────────────────
234
+ WATCHDOG_PID=""
235
+ _sprint_cleanup() {
236
+ log_warn "Sprint interrupted. Cleaning up..."
237
+ registry_update "$SAFE_BRANCH" "status" '"cancelled"'
238
+ if tmux has-session -t "agent-${SAFE_BRANCH}" 2>/dev/null; then
239
+ tmux kill-session -t "agent-${SAFE_BRANCH}" 2>/dev/null || true
240
+ fi
241
+ [[ -n "$WATCHDOG_PID" ]] && kill "$WATCHDOG_PID" 2>/dev/null || true
242
+ echo "Task #${SHORT_ID} cancelled."
243
+ exit 130
244
+ }
245
+ trap _sprint_cleanup SIGINT SIGTERM
246
+
247
+ # ── Watchdog timeout ─────────────────────────────────────────────────
248
+ if [[ -n "$TIMEOUT_MIN" ]]; then
249
+ log_info "Watchdog: will kill agent after ${TIMEOUT_MIN} minutes"
250
+ (
251
+ sleep $((TIMEOUT_MIN * 60))
252
+ log_warn "Timeout reached (${TIMEOUT_MIN}m). Stopping agent..."
253
+ registry_update "$SAFE_BRANCH" "status" '"timeout"'
254
+ tmux kill-session -t "agent-${SAFE_BRANCH}" 2>/dev/null || true
255
+ ) &
256
+ WATCHDOG_PID=$!
257
+ fi
258
+
259
+ # ── Step 4: Notify ────────────────────────────────────────────────────
260
+ "${SCRIPT_DIR}/notify.sh" --type task-started --description "$TASK" --dry-run 2>/dev/null || true
261
+
262
+ # ── OpenClaw notify ──────────────────────────────────────────────────
263
+ if $NOTIFY; then
264
+ openclaw system event --text "ClawForge: sprint started — $TASK (#$SHORT_ID)" --mode now 2>/dev/null || true
265
+ fi
266
+
267
+ # ── Webhook ──────────────────────────────────────────────────────────
268
+ if [[ -n "$WEBHOOK" ]]; then
269
+ local payload
270
+ payload=$(jq -cn \
271
+ --arg taskId "$SAFE_BRANCH" \
272
+ --argjson shortId "$SHORT_ID" \
273
+ --arg mode "$MODE" \
274
+ --arg status "spawned" \
275
+ --arg branch "$BRANCH" \
276
+ --arg description "$TASK" \
277
+ --arg repo "$REPO_ABS" \
278
+ '{taskId: $taskId, shortId: $shortId, mode: $mode, status: $status, branch: $branch, description: $description, repo: $repo}')
279
+ curl -s -X POST -H "Content-Type: application/json" -d "$payload" "$WEBHOOK" >/dev/null 2>&1 || log_warn "Webhook POST failed"
280
+ fi
281
+
282
+ # ── Output ────────────────────────────────────────────────────────────
283
+ if $JSON_OUTPUT; then
284
+ jq -cn \
285
+ --arg taskId "$SAFE_BRANCH" \
286
+ --argjson shortId "$SHORT_ID" \
287
+ --arg mode "$MODE" \
288
+ --arg status "spawned" \
289
+ --arg branch "$BRANCH" \
290
+ --arg description "$TASK" \
291
+ --arg repo "$REPO_ABS" \
292
+ --arg agent "$RESOLVED_AGENT" \
293
+ --arg model "$MODEL" \
294
+ --argjson autoMerge "$AUTO_MERGE" \
295
+ --argjson ciLoop "$CI_LOOP" \
296
+ '{taskId: $taskId, shortId: $shortId, mode: $mode, status: $status, branch: $branch, description: $description, repo: $repo, agent: $agent, model: $model, autoMerge: $autoMerge, ciLoop: $ciLoop}'
297
+ else
298
+ echo ""
299
+ echo " #${SHORT_ID} ${MODE} spawned $(basename "$REPO_ABS") \"$(echo "$TASK" | head -c 50)\""
300
+ echo ""
301
+ echo " Agent running in tmux session: agent-${SAFE_BRANCH}"
302
+ echo " Attach: clawforge attach $SHORT_ID"
303
+ echo " Steer: clawforge steer $SHORT_ID \"<message>\""
304
+ echo " Status: clawforge status"
305
+ echo ""
306
+ $CI_LOOP && echo " CI feedback loop: enabled (max retries: $MAX_CI_RETRIES)"
307
+ [[ -n "$BUDGET" ]] && echo " Budget cap: \$$BUDGET"
308
+ echo " Tip: Run 'clawforge watch --daemon' in another pane for auto-monitoring"
309
+ fi
310
+
311
+ # ── Auto-clean on completion ─────────────────────────────────────────
312
+ if $AUTO_CLEAN; then
313
+ log_info "Auto-clean enabled. Will clean on task completion."
314
+ # Note: actual cleanup happens when task reaches "done" status
315
+ registry_update "$SAFE_BRANCH" "auto_clean" 'true'
316
+ fi
317
+
318
+ # Kill watchdog if still running
319
+ [[ -n "$WATCHDOG_PID" ]] && kill "$WATCHDOG_PID" 2>/dev/null || true
320
+ trap - SIGINT SIGTERM
package/bin/steer.sh ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bash
2
+ # steer.sh — Send course correction to a running agent
3
+ # Usage: clawforge steer <id> "<message>"
4
+ set -euo pipefail
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ source "${SCRIPT_DIR}/../lib/common.sh"
8
+
9
+ # ── Help ───────────────────────────────────────────────────────────────
10
+ usage() {
11
+ cat <<EOF
12
+ Usage: clawforge steer <id> "<message>"
13
+
14
+ Send a course correction to a running agent via tmux.
15
+
16
+ Arguments:
17
+ <id> Task short ID (#1), full ID, or sub-agent ID (3.2)
18
+ "<message>" Course correction message
19
+
20
+ Examples:
21
+ clawforge steer 1 "Use bcrypt instead of md5 for password hashing"
22
+ clawforge steer 3.2 "Skip the legacy migration files"
23
+ clawforge steer myapp-auth "Add rate limiting too"
24
+ EOF
25
+ }
26
+
27
+ # ── Parse args ────────────────────────────────────────────────────────
28
+ if [[ $# -lt 1 ]] || [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then
29
+ usage
30
+ [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]] && exit 0 || exit 1
31
+ fi
32
+
33
+ TASK_REF="$1"
34
+ shift
35
+
36
+ MESSAGE="${*:-}"
37
+ if [[ -z "$MESSAGE" ]]; then
38
+ log_error "Message is required"
39
+ usage
40
+ exit 1
41
+ fi
42
+
43
+ # ── Resolve task ──────────────────────────────────────────────────────
44
+ TASK_ID=$(resolve_task_id "$TASK_REF")
45
+ if [[ -z "$TASK_ID" ]]; then
46
+ log_error "Could not resolve task: $TASK_REF"
47
+ exit 1
48
+ fi
49
+
50
+ TASK=$(registry_get "$TASK_ID")
51
+ if [[ -z "$TASK" ]]; then
52
+ log_error "Task not found: $TASK_ID"
53
+ exit 1
54
+ fi
55
+
56
+ STATUS=$(echo "$TASK" | jq -r '.status')
57
+ TMUX_SESSION=$(echo "$TASK" | jq -r '.tmuxSession')
58
+ DESCRIPTION=$(echo "$TASK" | jq -r '.description' | head -c 50)
59
+
60
+ # ── Check task state ──────────────────────────────────────────────────
61
+ case "$STATUS" in
62
+ done|archived)
63
+ log_warn "Task $TASK_REF is already $STATUS."
64
+ echo " Tip: Run 'clawforge review --pr <num>' instead."
65
+ exit 0
66
+ ;;
67
+ failed)
68
+ log_warn "Task $TASK_REF has failed. Consider restarting it."
69
+ exit 1
70
+ ;;
71
+ stopped)
72
+ log_warn "Task $TASK_REF is stopped. Start it first."
73
+ exit 1
74
+ ;;
75
+ esac
76
+
77
+ # ── Check tmux session ────────────────────────────────────────────────
78
+ if [[ -z "$TMUX_SESSION" ]] || ! tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then
79
+ log_error "tmux session not found: ${TMUX_SESSION:-none}"
80
+ echo " The agent may have finished or crashed."
81
+ echo " Use 'clawforge status' to check."
82
+ exit 1
83
+ fi
84
+
85
+ # ── Send message ──────────────────────────────────────────────────────
86
+ # For long messages (>200 chars), use tmux load-buffer to avoid truncation
87
+ if [[ ${#MESSAGE} -gt 200 ]]; then
88
+ TMPFILE=$(mktemp)
89
+ echo "$MESSAGE" > "$TMPFILE"
90
+ tmux load-buffer "$TMPFILE"
91
+ tmux paste-buffer -t "$TMUX_SESSION"
92
+ tmux send-keys -t "$TMUX_SESSION" Enter
93
+ rm -f "$TMPFILE"
94
+ else
95
+ tmux send-keys -t "$TMUX_SESSION" "$MESSAGE" Enter
96
+ fi
97
+
98
+ # ── Log steer event in registry ───────────────────────────────────────
99
+ NOW=$(epoch_ms)
100
+ STEER_LOG=$(registry_get "$TASK_ID" | jq -r '.steer_log // "[]"')
101
+ STEER_ENTRY=$(jq -n --argjson ts "$NOW" --arg msg "$MESSAGE" '{timestamp: $ts, message: $msg}')
102
+ NEW_LOG=$(echo "$STEER_LOG" | jq --argjson entry "$STEER_ENTRY" '. += [$entry]')
103
+ registry_update "$TASK_ID" "steer_log" "$NEW_LOG" 2>/dev/null || true
104
+
105
+ log_info "Steered task $TASK_REF ($DESCRIPTION): $(echo "$MESSAGE" | head -c 60)..."
106
+ echo " Sent to: $TMUX_SESSION"
107
+ echo " Attach: clawforge attach $TASK_REF"