@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,163 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# review-mode.sh — Review mode: quality gate on an existing PR
|
|
3
|
+
# Usage: clawforge review [repo] --pr <num> [flags]
|
|
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 review [repo] --pr <num> [flags]
|
|
13
|
+
|
|
14
|
+
Quality gate on an existing PR. No agent spawned — analysis only.
|
|
15
|
+
|
|
16
|
+
Arguments:
|
|
17
|
+
[repo] Path to git repository (default: auto-detect from cwd)
|
|
18
|
+
|
|
19
|
+
Flags:
|
|
20
|
+
--pr <num> PR number to review (required)
|
|
21
|
+
--fix Escalate: spawn agent to fix issues found
|
|
22
|
+
--reviewers <list> Comma-separated reviewer models (default: claude,gemini)
|
|
23
|
+
--dry-run Show review without posting comments
|
|
24
|
+
--help Show this help
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
clawforge review --pr 42
|
|
28
|
+
clawforge review ~/github/api --pr 42 --fix
|
|
29
|
+
clawforge review --pr 42 --reviewers claude,gemini,codex
|
|
30
|
+
EOF
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# ── Parse args ────────────────────────────────────────────────────────
|
|
34
|
+
REPO="" PR="" FIX=false REVIEWERS="" DRY_RUN=false
|
|
35
|
+
POSITIONAL=()
|
|
36
|
+
|
|
37
|
+
while [[ $# -gt 0 ]]; do
|
|
38
|
+
case "$1" in
|
|
39
|
+
--pr) PR="$2"; shift 2 ;;
|
|
40
|
+
--fix) FIX=true; shift ;;
|
|
41
|
+
--reviewers) REVIEWERS="$2"; shift 2 ;;
|
|
42
|
+
--dry-run) DRY_RUN=true; shift ;;
|
|
43
|
+
--help|-h) usage; exit 0 ;;
|
|
44
|
+
--*) log_error "Unknown option: $1"; usage; exit 1 ;;
|
|
45
|
+
*) POSITIONAL+=("$1"); shift ;;
|
|
46
|
+
esac
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
# Positional: optional repo
|
|
50
|
+
if [[ ${#POSITIONAL[@]} -gt 0 ]]; then
|
|
51
|
+
REPO="${POSITIONAL[0]}"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# ── Validate ──────────────────────────────────────────────────────────
|
|
55
|
+
[[ -z "$PR" ]] && { log_error "--pr is required"; usage; exit 1; }
|
|
56
|
+
|
|
57
|
+
# ── Resolve repo ──────────────────────────────────────────────────────
|
|
58
|
+
if [[ -z "$REPO" ]]; then
|
|
59
|
+
REPO=$(detect_repo) || { log_error "No --repo and no git repo found from cwd"; exit 1; }
|
|
60
|
+
fi
|
|
61
|
+
REPO_ABS=$(cd "$REPO" && pwd)
|
|
62
|
+
|
|
63
|
+
# ── Resolve reviewers ────────────────────────────────────────────────
|
|
64
|
+
if [[ -z "$REVIEWERS" ]]; then
|
|
65
|
+
REVIEWERS=$(config_get reviewers "claude,gemini" | jq -r 'if type == "array" then join(",") else . end' 2>/dev/null || echo "claude,gemini")
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# ── Assign short ID ──────────────────────────────────────────────────
|
|
69
|
+
SHORT_ID=$(_next_short_id)
|
|
70
|
+
|
|
71
|
+
# ── Register in registry ─────────────────────────────────────────────
|
|
72
|
+
NOW=$(epoch_ms)
|
|
73
|
+
TASK_JSON=$(jq -n \
|
|
74
|
+
--arg id "review-pr-${PR}" \
|
|
75
|
+
--argjson sid "$SHORT_ID" \
|
|
76
|
+
--arg desc "Review PR #${PR}" \
|
|
77
|
+
--arg repo "$REPO_ABS" \
|
|
78
|
+
--argjson pr "$PR" \
|
|
79
|
+
--argjson started "$NOW" \
|
|
80
|
+
'{
|
|
81
|
+
id: $id,
|
|
82
|
+
short_id: $sid,
|
|
83
|
+
mode: "review",
|
|
84
|
+
tmuxSession: "",
|
|
85
|
+
agent: "multi",
|
|
86
|
+
model: "multi",
|
|
87
|
+
description: $desc,
|
|
88
|
+
repo: $repo,
|
|
89
|
+
worktree: "",
|
|
90
|
+
branch: "",
|
|
91
|
+
startedAt: $started,
|
|
92
|
+
status: "reviewing",
|
|
93
|
+
retries: 0,
|
|
94
|
+
maxRetries: 0,
|
|
95
|
+
pr: $pr,
|
|
96
|
+
checks: {},
|
|
97
|
+
completedAt: null,
|
|
98
|
+
note: null,
|
|
99
|
+
files_touched: [],
|
|
100
|
+
ci_retries: 0
|
|
101
|
+
}')
|
|
102
|
+
registry_add "$TASK_JSON"
|
|
103
|
+
|
|
104
|
+
log_info "Review mode: PR #$PR in $REPO_ABS"
|
|
105
|
+
log_info "Reviewers: $REVIEWERS"
|
|
106
|
+
log_info "Short ID: #$SHORT_ID"
|
|
107
|
+
|
|
108
|
+
# ── Run review ────────────────────────────────────────────────────────
|
|
109
|
+
REVIEW_ARGS=(--repo "$REPO_ABS" --pr "$PR" --reviewers "$REVIEWERS")
|
|
110
|
+
$DRY_RUN && REVIEW_ARGS+=(--dry-run)
|
|
111
|
+
|
|
112
|
+
echo ""
|
|
113
|
+
echo " #${SHORT_ID} review reviewing $(basename "$REPO_ABS") \"Review PR #${PR}\""
|
|
114
|
+
echo ""
|
|
115
|
+
|
|
116
|
+
REVIEW_OUTPUT=$("${SCRIPT_DIR}/review-pr.sh" "${REVIEW_ARGS[@]}" 2>/dev/null || echo "[]")
|
|
117
|
+
|
|
118
|
+
# Store review results
|
|
119
|
+
registry_update "review-pr-${PR}" "checks" "$REVIEW_OUTPUT" 2>/dev/null || true
|
|
120
|
+
|
|
121
|
+
# ── Fix mode: spawn agent to fix issues ───────────────────────────────
|
|
122
|
+
if $FIX && ! $DRY_RUN; then
|
|
123
|
+
log_info "Fix mode: spawning agent to fix issues found in PR #$PR..."
|
|
124
|
+
|
|
125
|
+
# Get PR branch HEAD
|
|
126
|
+
PR_BRANCH=$(gh pr view "$PR" --repo "$REPO_ABS" --json headRefName -q '.headRefName' 2>/dev/null || echo "")
|
|
127
|
+
if [[ -z "$PR_BRANCH" ]]; then
|
|
128
|
+
log_error "Could not determine PR branch for --fix"
|
|
129
|
+
exit 1
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# Build fix prompt from review output
|
|
133
|
+
FIX_PROMPT="Fix the issues found in PR #${PR} code review:
|
|
134
|
+
|
|
135
|
+
${REVIEW_OUTPUT}
|
|
136
|
+
|
|
137
|
+
When complete:
|
|
138
|
+
1. Commit your fixes
|
|
139
|
+
2. Push to the same branch: git push origin ${PR_BRANCH}"
|
|
140
|
+
|
|
141
|
+
FIX_BRANCH="$PR_BRANCH"
|
|
142
|
+
RESOLVED_AGENT=$(detect_agent "")
|
|
143
|
+
|
|
144
|
+
"${SCRIPT_DIR}/spawn-agent.sh" \
|
|
145
|
+
--repo "$REPO_ABS" \
|
|
146
|
+
--branch "$FIX_BRANCH" \
|
|
147
|
+
--task "$FIX_PROMPT" \
|
|
148
|
+
--agent "$RESOLVED_AGENT" 2>/dev/null || true
|
|
149
|
+
|
|
150
|
+
echo " Agent spawned to fix issues on branch: $FIX_BRANCH"
|
|
151
|
+
echo " Attach: clawforge attach $SHORT_ID"
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# ── Mark complete ─────────────────────────────────────────────────────
|
|
155
|
+
if ! $FIX; then
|
|
156
|
+
NOW=$(epoch_ms)
|
|
157
|
+
registry_update "review-pr-${PR}" "status" '"done"'
|
|
158
|
+
registry_update "review-pr-${PR}" "completedAt" "$NOW"
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
echo ""
|
|
162
|
+
echo " Review complete. Results stored in registry."
|
|
163
|
+
echo "$REVIEW_OUTPUT"
|
package/bin/review-pr.sh
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# review-pr.sh — Module 5: Multi-model code review
|
|
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: review-pr.sh --repo <path> --pr <number> [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--repo <path> Path to the git repository (required)
|
|
15
|
+
--pr <number> PR number to review (required)
|
|
16
|
+
--reviewers <list> Comma-separated reviewer models (default: claude)
|
|
17
|
+
--dry-run Show what would happen without executing
|
|
18
|
+
--help Show this help
|
|
19
|
+
EOF
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
REPO="" PR_NUMBER="" REVIEWERS="" DRY_RUN=false
|
|
23
|
+
|
|
24
|
+
while [[ $# -gt 0 ]]; do
|
|
25
|
+
case "$1" in
|
|
26
|
+
--repo) REPO="$2"; shift 2 ;;
|
|
27
|
+
--pr) PR_NUMBER="$2"; shift 2 ;;
|
|
28
|
+
--reviewers) REVIEWERS="$2"; shift 2 ;;
|
|
29
|
+
--dry-run) DRY_RUN=true; shift ;;
|
|
30
|
+
--help|-h) usage; exit 0 ;;
|
|
31
|
+
*) log_error "Unknown option: $1"; usage; exit 1 ;;
|
|
32
|
+
esac
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
[[ -z "$REPO" ]] && { log_error "--repo is required"; usage; exit 1; }
|
|
36
|
+
[[ -z "$PR_NUMBER" ]] && { log_error "--pr is required"; usage; exit 1; }
|
|
37
|
+
|
|
38
|
+
REPO_ABS=$(cd "$REPO" && pwd)
|
|
39
|
+
|
|
40
|
+
# Default reviewers from config
|
|
41
|
+
if [[ -z "$REVIEWERS" ]]; then
|
|
42
|
+
REVIEWERS=$(config_get reviewers "claude" | jq -r 'if type == "array" then join(",") else . end' 2>/dev/null || echo "claude")
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
REVIEW_PROMPT=$(config_get review_prompt "Review this pull request for:
|
|
46
|
+
1. Bugs and logic errors
|
|
47
|
+
2. Edge cases and error handling
|
|
48
|
+
3. Security issues
|
|
49
|
+
4. Performance concerns
|
|
50
|
+
5. Code quality and conventions
|
|
51
|
+
|
|
52
|
+
Respond with: APPROVE, REQUEST_CHANGES, or COMMENT. Then list findings.")
|
|
53
|
+
|
|
54
|
+
log_info "Reviewing PR #${PR_NUMBER} in ${REPO_ABS}"
|
|
55
|
+
log_info "Reviewers: ${REVIEWERS}"
|
|
56
|
+
|
|
57
|
+
# ── Get PR diff ───────────────────────────────────────────────────────
|
|
58
|
+
if $DRY_RUN; then
|
|
59
|
+
log_info "[dry-run] Would fetch diff for PR #${PR_NUMBER}"
|
|
60
|
+
DIFF="[dry-run: diff would be fetched here]"
|
|
61
|
+
else
|
|
62
|
+
DIFF=$(gh pr diff "$PR_NUMBER" --repo "$REPO_ABS" 2>/dev/null || true)
|
|
63
|
+
if [[ -z "$DIFF" ]]; then
|
|
64
|
+
log_error "Could not get diff for PR #${PR_NUMBER}"
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# ── Get PR info ───────────────────────────────────────────────────────
|
|
70
|
+
PR_TITLE=""
|
|
71
|
+
PR_BODY=""
|
|
72
|
+
if ! $DRY_RUN; then
|
|
73
|
+
PR_INFO=$(gh pr view "$PR_NUMBER" --repo "$REPO_ABS" --json title,body 2>/dev/null || echo '{}')
|
|
74
|
+
PR_TITLE=$(echo "$PR_INFO" | jq -r '.title // ""')
|
|
75
|
+
PR_BODY=$(echo "$PR_INFO" | jq -r '.body // ""')
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# ── Review with each model ────────────────────────────────────────────
|
|
79
|
+
IFS=',' read -ra REVIEWER_LIST <<< "$REVIEWERS"
|
|
80
|
+
REVIEWS="[]"
|
|
81
|
+
|
|
82
|
+
for reviewer in "${REVIEWER_LIST[@]}"; do
|
|
83
|
+
reviewer=$(echo "$reviewer" | xargs) # trim whitespace
|
|
84
|
+
log_info "Getting review from: $reviewer"
|
|
85
|
+
|
|
86
|
+
FULL_REVIEW_PROMPT="${REVIEW_PROMPT}
|
|
87
|
+
|
|
88
|
+
PR Title: ${PR_TITLE}
|
|
89
|
+
PR Description: ${PR_BODY}
|
|
90
|
+
|
|
91
|
+
Diff:
|
|
92
|
+
${DIFF}"
|
|
93
|
+
|
|
94
|
+
if $DRY_RUN; then
|
|
95
|
+
log_info "[dry-run] Would send diff to $reviewer for review"
|
|
96
|
+
log_info "[dry-run] Prompt starts with: $(echo "$REVIEW_PROMPT" | head -3)"
|
|
97
|
+
REVIEW_RESULT="[dry-run] Review from $reviewer would appear here"
|
|
98
|
+
else
|
|
99
|
+
# Use claude for all reviews (with model flag for different models)
|
|
100
|
+
case "$reviewer" in
|
|
101
|
+
claude)
|
|
102
|
+
REVIEW_RESULT=$(claude --model claude-sonnet-4-5 --dangerously-skip-permissions -p "$FULL_REVIEW_PROMPT" 2>/dev/null || echo "Review failed for $reviewer")
|
|
103
|
+
;;
|
|
104
|
+
codex)
|
|
105
|
+
REVIEW_RESULT=$(codex --model gpt-5.3-codex -q "$FULL_REVIEW_PROMPT" 2>/dev/null || echo "Review failed for $reviewer")
|
|
106
|
+
;;
|
|
107
|
+
gemini)
|
|
108
|
+
REVIEW_RESULT=$(claude --model gemini-2.5-pro -p "$FULL_REVIEW_PROMPT" 2>/dev/null || echo "Review failed for $reviewer")
|
|
109
|
+
;;
|
|
110
|
+
*)
|
|
111
|
+
REVIEW_RESULT=$(claude --model "$reviewer" --dangerously-skip-permissions -p "$FULL_REVIEW_PROMPT" 2>/dev/null || echo "Review failed for $reviewer")
|
|
112
|
+
;;
|
|
113
|
+
esac
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Store review
|
|
117
|
+
review_entry=$(jq -n --arg r "$reviewer" --arg body "$REVIEW_RESULT" '{reviewer: $r, review: $body}')
|
|
118
|
+
REVIEWS=$(echo "$REVIEWS" | jq --argjson entry "$review_entry" '. += [$entry]')
|
|
119
|
+
|
|
120
|
+
# Post review comment
|
|
121
|
+
if ! $DRY_RUN; then
|
|
122
|
+
COMMENT_BODY="## 🤖 Review by \`${reviewer}\`
|
|
123
|
+
|
|
124
|
+
${REVIEW_RESULT}"
|
|
125
|
+
gh pr review "$PR_NUMBER" --repo "$REPO_ABS" --comment --body "$COMMENT_BODY" 2>/dev/null || \
|
|
126
|
+
log_warn "Failed to post review comment for $reviewer"
|
|
127
|
+
log_info "Posted review from $reviewer"
|
|
128
|
+
fi
|
|
129
|
+
done
|
|
130
|
+
|
|
131
|
+
# ── Update registry ───────────────────────────────────────────────────
|
|
132
|
+
# Find task by PR number and update
|
|
133
|
+
if ! $DRY_RUN; then
|
|
134
|
+
_ensure_registry
|
|
135
|
+
TASK_ID=$(jq -r --argjson pr "$PR_NUMBER" '.tasks[] | select(.pr == $pr) | .id' "$REGISTRY_FILE" 2>/dev/null || true)
|
|
136
|
+
if [[ -n "$TASK_ID" ]]; then
|
|
137
|
+
CHECKS_JSON=$(echo "$REVIEWS" | jq 'reduce .[] as $r ({}; .[$r.reviewer] = ($r.review | split("\n")[0]))')
|
|
138
|
+
registry_update "$TASK_ID" "checks" "$CHECKS_JSON"
|
|
139
|
+
registry_update "$TASK_ID" "status" '"reviewing"'
|
|
140
|
+
log_info "Updated registry for task $TASK_ID"
|
|
141
|
+
fi
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
# ── Output ─────────────────────────────────────────────────────────────
|
|
145
|
+
echo "$REVIEWS" | jq '.'
|
package/bin/routing.sh
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# routing.sh — Model routing: pick the right model for each phase
|
|
3
|
+
# Usage: source bin/routing.sh; load_routing "auto"; get_model_for_phase "scope"
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
source "${SCRIPT_DIR}/../lib/common.sh"
|
|
8
|
+
|
|
9
|
+
# ── Routing config ────────────────────────────────────────────────────
|
|
10
|
+
ROUTING_DEFAULTS="${CLAWFORGE_DIR}/config/routing-defaults.json"
|
|
11
|
+
ROUTING_USER="${HOME}/.clawforge/routing.json"
|
|
12
|
+
|
|
13
|
+
# Internal state — set by load_routing
|
|
14
|
+
_ROUTING_STRATEGY=""
|
|
15
|
+
_ROUTING_CONFIG=""
|
|
16
|
+
|
|
17
|
+
# ── Model aliases ─────────────────────────────────────────────────────
|
|
18
|
+
_resolve_model_alias() {
|
|
19
|
+
local alias="$1"
|
|
20
|
+
case "$alias" in
|
|
21
|
+
haiku) echo "claude-haiku-4-5" ;;
|
|
22
|
+
sonnet) echo "claude-sonnet-4-5" ;;
|
|
23
|
+
opus) echo "claude-opus-4-6" ;;
|
|
24
|
+
*) echo "$alias" ;; # pass through full model IDs
|
|
25
|
+
esac
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# ── load_routing(strategy) ────────────────────────────────────────────
|
|
29
|
+
# strategy: auto | cheap | quality | "" (disabled)
|
|
30
|
+
load_routing() {
|
|
31
|
+
local strategy="${1:-}"
|
|
32
|
+
_ROUTING_STRATEGY="$strategy"
|
|
33
|
+
|
|
34
|
+
case "$strategy" in
|
|
35
|
+
auto)
|
|
36
|
+
# User config > defaults
|
|
37
|
+
if [[ -f "$ROUTING_USER" ]]; then
|
|
38
|
+
_ROUTING_CONFIG=$(cat "$ROUTING_USER")
|
|
39
|
+
log_debug "Routing: loaded user config from $ROUTING_USER"
|
|
40
|
+
elif [[ -f "$ROUTING_DEFAULTS" ]]; then
|
|
41
|
+
_ROUTING_CONFIG=$(cat "$ROUTING_DEFAULTS")
|
|
42
|
+
log_debug "Routing: loaded defaults from $ROUTING_DEFAULTS"
|
|
43
|
+
else
|
|
44
|
+
log_warn "Routing: no config found, falling back to agent defaults"
|
|
45
|
+
_ROUTING_STRATEGY=""
|
|
46
|
+
fi
|
|
47
|
+
;;
|
|
48
|
+
cheap)
|
|
49
|
+
_ROUTING_CONFIG='{"scope":"haiku","implement":"haiku","review":"haiku","ci-fix":"haiku"}'
|
|
50
|
+
log_debug "Routing: cheap mode — haiku for all phases"
|
|
51
|
+
;;
|
|
52
|
+
quality)
|
|
53
|
+
_ROUTING_CONFIG='{"scope":"opus","implement":"opus","review":"opus","ci-fix":"opus"}'
|
|
54
|
+
log_debug "Routing: quality mode — opus for all phases"
|
|
55
|
+
;;
|
|
56
|
+
"")
|
|
57
|
+
_ROUTING_CONFIG=""
|
|
58
|
+
;;
|
|
59
|
+
*)
|
|
60
|
+
log_error "Unknown routing strategy: $strategy (expected: auto, cheap, quality)"
|
|
61
|
+
return 1
|
|
62
|
+
;;
|
|
63
|
+
esac
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# ── get_model_for_phase(phase) ────────────────────────────────────────
|
|
67
|
+
# phase: scope | implement | review | ci-fix
|
|
68
|
+
# Returns: full model ID string, or empty if no routing active
|
|
69
|
+
get_model_for_phase() {
|
|
70
|
+
local phase="$1"
|
|
71
|
+
|
|
72
|
+
# No routing loaded — return empty (caller uses its own default)
|
|
73
|
+
if [[ -z "$_ROUTING_STRATEGY" || -z "$_ROUTING_CONFIG" ]]; then
|
|
74
|
+
echo ""
|
|
75
|
+
return
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
local alias
|
|
79
|
+
alias=$(echo "$_ROUTING_CONFIG" | jq -r --arg p "$phase" '.[$p] // empty' 2>/dev/null)
|
|
80
|
+
|
|
81
|
+
if [[ -z "$alias" ]]; then
|
|
82
|
+
log_debug "Routing: no mapping for phase '$phase', using default"
|
|
83
|
+
echo ""
|
|
84
|
+
return
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
_resolve_model_alias "$alias"
|
|
88
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scope-task.sh — Module 1: Assemble a comprehensive prompt from task + context
|
|
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: scope-task.sh --task <description> [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--task <description> Task description (required)
|
|
15
|
+
--vault-query <search> Search Obsidian vault for relevant context
|
|
16
|
+
--prd <path> Path to PRD or spec file
|
|
17
|
+
--context <file> Additional context file (repeatable)
|
|
18
|
+
--template <name> Prompt template name (default: default)
|
|
19
|
+
--output prompt|json Output format (default: prompt)
|
|
20
|
+
--dry-run Show what would be included without assembling
|
|
21
|
+
--help Show this help
|
|
22
|
+
EOF
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# ── Parse args ─────────────────────────────────────────────────────────
|
|
26
|
+
TASK="" VAULT_QUERY="" PRD="" OUTPUT="prompt" TEMPLATE="default" DRY_RUN=false
|
|
27
|
+
CONTEXT_FILES=()
|
|
28
|
+
|
|
29
|
+
while [[ $# -gt 0 ]]; do
|
|
30
|
+
case "$1" in
|
|
31
|
+
--task) TASK="$2"; shift 2 ;;
|
|
32
|
+
--vault-query) VAULT_QUERY="$2"; shift 2 ;;
|
|
33
|
+
--prd) PRD="$2"; shift 2 ;;
|
|
34
|
+
--context) CONTEXT_FILES+=("$2"); shift 2 ;;
|
|
35
|
+
--template) TEMPLATE="$2"; shift 2 ;;
|
|
36
|
+
--output) OUTPUT="$2"; shift 2 ;;
|
|
37
|
+
--dry-run) DRY_RUN=true; shift ;;
|
|
38
|
+
--help|-h) usage; exit 0 ;;
|
|
39
|
+
*) log_error "Unknown option: $1"; usage; exit 1 ;;
|
|
40
|
+
esac
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
[[ -z "$TASK" ]] && { log_error "--task is required"; usage; exit 1; }
|
|
44
|
+
|
|
45
|
+
# ── Resolve paths ─────────────────────────────────────────────────────
|
|
46
|
+
VAULT_PATH=$(config_get vault_path "/Users/cyperx/Library/Mobile Documents/iCloud~md~obsidian/Documents/cyperx")
|
|
47
|
+
VAULT_MAX_LINES=$(config_get vault_max_lines 2000)
|
|
48
|
+
TEMPLATE_DIR="${CLAWFORGE_DIR}/config/prompt-templates"
|
|
49
|
+
TEMPLATE_FILE="${TEMPLATE_DIR}/${TEMPLATE}.md"
|
|
50
|
+
|
|
51
|
+
if [[ ! -f "$TEMPLATE_FILE" ]]; then
|
|
52
|
+
log_warn "Template '${TEMPLATE}' not found, using inline default"
|
|
53
|
+
TEMPLATE_FILE=""
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# ── Vault search ──────────────────────────────────────────────────────
|
|
57
|
+
VAULT_CONTEXT=""
|
|
58
|
+
if [[ -n "$VAULT_QUERY" ]]; then
|
|
59
|
+
log_info "Searching vault for: $VAULT_QUERY"
|
|
60
|
+
if [[ -d "$VAULT_PATH" ]] && command -v rg &>/dev/null; then
|
|
61
|
+
VAULT_RESULTS=$(rg -l --type md -i "$VAULT_QUERY" "$VAULT_PATH" 2>/dev/null | head -10 || true)
|
|
62
|
+
if [[ -n "$VAULT_RESULTS" ]]; then
|
|
63
|
+
VAULT_CONTEXT="## Vault Context (matching: ${VAULT_QUERY})"$'\n\n'
|
|
64
|
+
while IFS= read -r file; do
|
|
65
|
+
fname=$(basename "$file")
|
|
66
|
+
VAULT_CONTEXT+="### ${fname}"$'\n'
|
|
67
|
+
VAULT_CONTEXT+=$(head -100 "$file")$'\n\n'
|
|
68
|
+
done <<< "$VAULT_RESULTS"
|
|
69
|
+
# Truncate
|
|
70
|
+
line_count=$(echo "$VAULT_CONTEXT" | wc -l)
|
|
71
|
+
if [[ "$line_count" -gt "$VAULT_MAX_LINES" ]]; then
|
|
72
|
+
VAULT_CONTEXT=$(echo "$VAULT_CONTEXT" | head -"$VAULT_MAX_LINES")
|
|
73
|
+
VAULT_CONTEXT+=$'\n[...truncated to '"$VAULT_MAX_LINES"' lines]'
|
|
74
|
+
log_info "Vault context truncated to $VAULT_MAX_LINES lines"
|
|
75
|
+
fi
|
|
76
|
+
log_info "Found $(echo "$VAULT_RESULTS" | wc -l | tr -d ' ') matching files"
|
|
77
|
+
else
|
|
78
|
+
log_info "No vault matches for: $VAULT_QUERY"
|
|
79
|
+
fi
|
|
80
|
+
elif [[ ! -d "$VAULT_PATH" ]]; then
|
|
81
|
+
log_warn "Vault path not found: $VAULT_PATH"
|
|
82
|
+
elif ! command -v rg &>/dev/null; then
|
|
83
|
+
log_warn "ripgrep (rg) not found, skipping vault search"
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# ── PRD content ───────────────────────────────────────────────────────
|
|
88
|
+
PRD_CONTENT=""
|
|
89
|
+
if [[ -n "$PRD" ]]; then
|
|
90
|
+
if [[ -f "$PRD" ]]; then
|
|
91
|
+
PRD_CONTENT="## PRD / Specification"$'\n\n'
|
|
92
|
+
PRD_CONTENT+=$(cat "$PRD")
|
|
93
|
+
log_info "Included PRD: $PRD"
|
|
94
|
+
else
|
|
95
|
+
log_error "PRD file not found: $PRD"
|
|
96
|
+
exit 1
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# ── Extra context ─────────────────────────────────────────────────────
|
|
101
|
+
EXTRA_CONTEXT=""
|
|
102
|
+
if [[ ${#CONTEXT_FILES[@]} -gt 0 ]]; then
|
|
103
|
+
EXTRA_CONTEXT="## Additional Context"$'\n\n'
|
|
104
|
+
for ctx_file in "${CONTEXT_FILES[@]}"; do
|
|
105
|
+
if [[ -f "$ctx_file" ]]; then
|
|
106
|
+
fname=$(basename "$ctx_file")
|
|
107
|
+
EXTRA_CONTEXT+="### ${fname}"$'\n'
|
|
108
|
+
EXTRA_CONTEXT+=$(cat "$ctx_file")$'\n\n'
|
|
109
|
+
log_info "Included context: $ctx_file"
|
|
110
|
+
else
|
|
111
|
+
log_warn "Context file not found: $ctx_file"
|
|
112
|
+
fi
|
|
113
|
+
done
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# ── Dry run ───────────────────────────────────────────────────────────
|
|
117
|
+
if $DRY_RUN; then
|
|
118
|
+
echo "=== Scope Dry Run ==="
|
|
119
|
+
echo "Task: $TASK"
|
|
120
|
+
echo "Template: ${TEMPLATE_FILE:-inline}"
|
|
121
|
+
echo "Vault query: ${VAULT_QUERY:-none}"
|
|
122
|
+
echo "Vault matches: $(echo "$VAULT_CONTEXT" | grep -c '^### ' 2>/dev/null || echo 0)"
|
|
123
|
+
echo "PRD: ${PRD:-none}"
|
|
124
|
+
echo "Context files: ${#CONTEXT_FILES[@]}"
|
|
125
|
+
[[ -n "$VAULT_CONTEXT" ]] && echo "Vault context lines: $(echo "$VAULT_CONTEXT" | wc -l | tr -d ' ')"
|
|
126
|
+
[[ -n "$PRD_CONTENT" ]] && echo "PRD lines: $(echo "$PRD_CONTENT" | wc -l | tr -d ' ')"
|
|
127
|
+
exit 0
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# ── Assemble prompt ──────────────────────────────────────────────────
|
|
131
|
+
if [[ -n "$TEMPLATE_FILE" ]]; then
|
|
132
|
+
PROMPT=$(cat "$TEMPLATE_FILE")
|
|
133
|
+
PROMPT="${PROMPT//\{\{TASK_DESCRIPTION\}\}/$TASK}"
|
|
134
|
+
PROMPT="${PROMPT//\{\{VAULT_CONTEXT\}\}/$VAULT_CONTEXT}"
|
|
135
|
+
PROMPT="${PROMPT//\{\{PRD_CONTENT\}\}/$PRD_CONTENT}"
|
|
136
|
+
PROMPT="${PROMPT//\{\{EXTRA_CONTEXT\}\}/$EXTRA_CONTEXT}"
|
|
137
|
+
else
|
|
138
|
+
PROMPT="# Task"$'\n\n'"$TASK"$'\n\n'
|
|
139
|
+
[[ -n "$VAULT_CONTEXT" ]] && PROMPT+="$VAULT_CONTEXT"$'\n\n'
|
|
140
|
+
[[ -n "$PRD_CONTENT" ]] && PROMPT+="$PRD_CONTENT"$'\n\n'
|
|
141
|
+
[[ -n "$EXTRA_CONTEXT" ]] && PROMPT+="$EXTRA_CONTEXT"$'\n\n'
|
|
142
|
+
PROMPT+="# Instructions"$'\n\n'
|
|
143
|
+
PROMPT+="- Follow existing code conventions and style"$'\n'
|
|
144
|
+
PROMPT+="- Create small, atomic commits with imperative messages"$'\n'
|
|
145
|
+
PROMPT+="- When complete, push the branch and create a PR against main"
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# ── Output ────────────────────────────────────────────────────────────
|
|
149
|
+
if [[ "$OUTPUT" == "json" ]]; then
|
|
150
|
+
jq -n \
|
|
151
|
+
--arg task "$TASK" \
|
|
152
|
+
--arg vault_query "${VAULT_QUERY:-}" \
|
|
153
|
+
--arg prd "${PRD:-}" \
|
|
154
|
+
--arg template "$TEMPLATE" \
|
|
155
|
+
--arg prompt "$PROMPT" \
|
|
156
|
+
--argjson context_count "${#CONTEXT_FILES[@]}" \
|
|
157
|
+
--argjson timestamp "$(epoch_ms)" \
|
|
158
|
+
'{
|
|
159
|
+
timestamp: $timestamp,
|
|
160
|
+
task: $task,
|
|
161
|
+
vaultQuery: (if $vault_query == "" then null else $vault_query end),
|
|
162
|
+
prd: (if $prd == "" then null else $prd end),
|
|
163
|
+
template: $template,
|
|
164
|
+
contextFiles: $context_count,
|
|
165
|
+
prompt: $prompt
|
|
166
|
+
}'
|
|
167
|
+
else
|
|
168
|
+
echo "$PROMPT"
|
|
169
|
+
fi
|