@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,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"
|