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