forge-workflow 0.0.4 → 0.0.6

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 (248) hide show
  1. package/.claude/commands/dev.md +345 -340
  2. package/.claude/commands/plan.md +566 -521
  3. package/.claude/commands/premerge.md +186 -176
  4. package/.claude/commands/research.md +42 -42
  5. package/.claude/commands/review.md +448 -442
  6. package/.claude/commands/rollback.md +721 -721
  7. package/.claude/commands/ship.md +212 -164
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +90 -48
  10. package/.claude/commands/validate.md +288 -282
  11. package/.claude/commands/verify.md +269 -221
  12. package/.claude/rules/greptile-review-process.md +285 -285
  13. package/.claude/rules/workflow.md +121 -105
  14. package/.claude/scripts/greptile-resolve.sh +558 -526
  15. package/.claude/scripts/load-env.sh +32 -32
  16. package/.cline/workflows/dev.md +342 -337
  17. package/.cline/workflows/plan.md +563 -518
  18. package/.cline/workflows/premerge.md +183 -173
  19. package/.cline/workflows/research.md +39 -39
  20. package/.cline/workflows/review.md +445 -439
  21. package/.cline/workflows/rollback.md +718 -718
  22. package/.cline/workflows/ship.md +209 -161
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +87 -45
  25. package/.cline/workflows/validate.md +285 -279
  26. package/.cline/workflows/verify.md +266 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +345 -340
  29. package/.codex/skills/plan/SKILL.md +566 -521
  30. package/.codex/skills/premerge/SKILL.md +186 -176
  31. package/.codex/skills/research/SKILL.md +42 -42
  32. package/.codex/skills/review/SKILL.md +448 -442
  33. package/.codex/skills/rollback/SKILL.md +721 -721
  34. package/.codex/skills/ship/SKILL.md +212 -164
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +90 -48
  37. package/.codex/skills/validate/SKILL.md +288 -282
  38. package/.codex/skills/verify/SKILL.md +269 -221
  39. package/.cursor/commands/dev.md +342 -337
  40. package/.cursor/commands/plan.md +563 -518
  41. package/.cursor/commands/premerge.md +183 -173
  42. package/.cursor/commands/research.md +39 -39
  43. package/.cursor/commands/review.md +445 -439
  44. package/.cursor/commands/rollback.md +718 -718
  45. package/.cursor/commands/ship.md +209 -161
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +87 -45
  48. package/.cursor/commands/validate.md +285 -279
  49. package/.cursor/commands/verify.md +266 -218
  50. package/.cursor/rules/permissions-guidance.mdc +37 -37
  51. package/.forge/hooks/check-tdd.js +240 -240
  52. package/.github/PLUGIN_TEMPLATE.json +32 -32
  53. package/.github/prompts/dev.prompt.md +347 -342
  54. package/.github/prompts/plan.prompt.md +568 -523
  55. package/.github/prompts/premerge.prompt.md +188 -178
  56. package/.github/prompts/research.prompt.md +44 -44
  57. package/.github/prompts/review.prompt.md +450 -444
  58. package/.github/prompts/rollback.prompt.md +723 -723
  59. package/.github/prompts/ship.prompt.md +214 -166
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +92 -50
  62. package/.github/prompts/validate.prompt.md +290 -284
  63. package/.github/prompts/verify.prompt.md +271 -223
  64. package/.github/workflows/beads-to-github.yml +56 -0
  65. package/.github/workflows/github-to-beads.yml +97 -0
  66. package/.kilocode/workflows/dev.md +346 -341
  67. package/.kilocode/workflows/plan.md +567 -522
  68. package/.kilocode/workflows/premerge.md +187 -177
  69. package/.kilocode/workflows/research.md +43 -43
  70. package/.kilocode/workflows/review.md +449 -443
  71. package/.kilocode/workflows/rollback.md +722 -722
  72. package/.kilocode/workflows/ship.md +213 -165
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +91 -49
  75. package/.kilocode/workflows/validate.md +289 -283
  76. package/.kilocode/workflows/verify.md +270 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +345 -340
  79. package/.opencode/commands/plan.md +566 -521
  80. package/.opencode/commands/premerge.md +186 -176
  81. package/.opencode/commands/research.md +42 -42
  82. package/.opencode/commands/review.md +448 -442
  83. package/.opencode/commands/rollback.md +721 -721
  84. package/.opencode/commands/ship.md +212 -164
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +90 -48
  87. package/.opencode/commands/validate.md +288 -282
  88. package/.opencode/commands/verify.md +269 -221
  89. package/.roo/commands/dev.md +346 -341
  90. package/.roo/commands/plan.md +567 -522
  91. package/.roo/commands/premerge.md +187 -177
  92. package/.roo/commands/research.md +43 -43
  93. package/.roo/commands/review.md +449 -443
  94. package/.roo/commands/rollback.md +722 -722
  95. package/.roo/commands/ship.md +213 -165
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +91 -49
  98. package/.roo/commands/validate.md +289 -283
  99. package/.roo/commands/verify.md +270 -222
  100. package/AGENTS.md +272 -175
  101. package/CLAUDE.md +110 -100
  102. package/README.md +429 -416
  103. package/bin/forge-cmd.js +317 -313
  104. package/bin/forge-preflight.js +322 -309
  105. package/bin/forge.js +4765 -4303
  106. package/docs/AGENT_INSTALL_PROMPT.md +342 -342
  107. package/docs/BEADS_GITHUB_SYNC.md +251 -251
  108. package/docs/ENHANCED_ONBOARDING.md +612 -602
  109. package/docs/EXAMPLES.md +482 -482
  110. package/docs/GREPTILE_SETUP.md +400 -400
  111. package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
  112. package/docs/ROADMAP.md +359 -359
  113. package/docs/SETUP.md +663 -631
  114. package/docs/TOOLCHAIN.md +653 -630
  115. package/docs/VALIDATION.md +363 -363
  116. package/install.sh +40 -1056
  117. package/lefthook.yml +50 -39
  118. package/lib/agents/README.md +198 -198
  119. package/lib/agents/claude.plugin.json +28 -28
  120. package/lib/agents/cline.plugin.json +22 -22
  121. package/lib/agents/codex.plugin.json +19 -19
  122. package/lib/agents/copilot.plugin.json +24 -24
  123. package/lib/agents/cursor.plugin.json +25 -25
  124. package/lib/agents/kilocode.plugin.json +22 -22
  125. package/lib/agents/opencode.plugin.json +20 -20
  126. package/lib/agents/roo.plugin.json +23 -23
  127. package/lib/agents-config.js +2112 -2112
  128. package/lib/beads-health-check.js +143 -0
  129. package/lib/beads-setup.js +341 -0
  130. package/lib/beads-sync-scaffold.js +260 -0
  131. package/lib/commands/_registry.js +134 -0
  132. package/lib/commands/clean.js +181 -0
  133. package/lib/commands/dev.js +571 -513
  134. package/lib/commands/plan.js +692 -692
  135. package/lib/commands/push.js +196 -0
  136. package/lib/commands/recommend.js +119 -119
  137. package/lib/commands/ship.js +377 -377
  138. package/lib/commands/status.js +378 -378
  139. package/lib/commands/sync.js +55 -0
  140. package/lib/commands/team.js +37 -0
  141. package/lib/commands/test.js +207 -0
  142. package/lib/commands/validate.js +602 -602
  143. package/lib/commands/worktree.js +310 -0
  144. package/lib/context-merge.js +359 -359
  145. package/lib/dep-guard/analyzer.js +294 -294
  146. package/lib/dep-guard/behavior-detector.js +98 -98
  147. package/lib/dep-guard/contract-detector.js +162 -162
  148. package/lib/dep-guard/import-detector.js +498 -498
  149. package/lib/dep-guard/path-utils.js +13 -13
  150. package/lib/dep-guard/rubric.js +120 -120
  151. package/lib/dep-guard/task-parser.js +318 -318
  152. package/lib/detect-agent.js +191 -191
  153. package/lib/detect-worktree.js +47 -47
  154. package/lib/docs-command.js +51 -0
  155. package/lib/docs-copy.js +50 -0
  156. package/lib/file-hash.js +26 -26
  157. package/lib/freshness-token.js +148 -0
  158. package/lib/greptile-match.js +80 -0
  159. package/lib/husky-migration.js +450 -0
  160. package/lib/lefthook-check.js +65 -0
  161. package/lib/pat-setup.js +207 -0
  162. package/lib/plugin-catalog.js +350 -350
  163. package/lib/plugin-manager.js +166 -166
  164. package/lib/plugin-recommender.js +141 -141
  165. package/lib/project-discovery.js +491 -491
  166. package/lib/reset.js +309 -0
  167. package/lib/setup-action-log.js +139 -139
  168. package/lib/setup-summary-renderer.js +106 -106
  169. package/lib/setup-utils.js +96 -0
  170. package/lib/setup.js +192 -192
  171. package/lib/smart-merge.js +64 -0
  172. package/lib/symlink-utils.js +81 -0
  173. package/lib/task-ownership.js +117 -0
  174. package/lib/workflow-profiles.js +197 -197
  175. package/package.json +131 -128
  176. package/scripts/beads-context.sh +426 -0
  177. package/scripts/beads-context.test.js +567 -0
  178. package/scripts/behavioral-judge.sh +378 -0
  179. package/scripts/benchmark.js +85 -0
  180. package/scripts/branch-protection.js +183 -0
  181. package/scripts/check-agents.js +172 -0
  182. package/scripts/check-forge-token.js +98 -0
  183. package/scripts/commitlint.js +42 -0
  184. package/scripts/conflict-detect.sh +323 -0
  185. package/scripts/dep-guard-analyze.js +71 -0
  186. package/scripts/dep-guard.sh +789 -0
  187. package/scripts/eval_win.py +249 -0
  188. package/scripts/file-index.sh +493 -0
  189. package/scripts/forge-team/index.sh +86 -0
  190. package/scripts/forge-team/lib/agent-prompt.sh +52 -0
  191. package/scripts/forge-team/lib/claim.sh +256 -0
  192. package/scripts/forge-team/lib/dashboard.sh +341 -0
  193. package/scripts/forge-team/lib/epic.sh +332 -0
  194. package/scripts/forge-team/lib/hooks.sh +253 -0
  195. package/scripts/forge-team/lib/identity.sh +235 -0
  196. package/scripts/forge-team/lib/sync-github.sh +317 -0
  197. package/scripts/forge-team/lib/verify.sh +284 -0
  198. package/scripts/forge-team/lib/workload.sh +296 -0
  199. package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
  200. package/scripts/forge-team/tests/claim.test.sh +179 -0
  201. package/scripts/forge-team/tests/dashboard.test.sh +170 -0
  202. package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
  203. package/scripts/forge-team/tests/epic.test.sh +176 -0
  204. package/scripts/forge-team/tests/hooks.test.sh +239 -0
  205. package/scripts/forge-team/tests/identity.test.sh +176 -0
  206. package/scripts/forge-team/tests/integration.test.sh +371 -0
  207. package/scripts/forge-team/tests/sync-github.test.sh +209 -0
  208. package/scripts/forge-team/tests/verify.test.sh +314 -0
  209. package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
  210. package/scripts/forge-team/tests/workload.test.sh +209 -0
  211. package/scripts/github-beads-sync/comment.mjs +64 -0
  212. package/scripts/github-beads-sync/config.mjs +148 -0
  213. package/scripts/github-beads-sync/github-api.mjs +131 -0
  214. package/scripts/github-beads-sync/index.mjs +332 -0
  215. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  216. package/scripts/github-beads-sync/mapping.mjs +78 -0
  217. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  218. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  219. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  220. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  221. package/scripts/github-beads-sync.config.json +26 -0
  222. package/scripts/improve-command.js +375 -0
  223. package/scripts/lib/eval-runner.js +268 -0
  224. package/scripts/lib/eval-schema.js +135 -0
  225. package/scripts/lib/eval-storage.js +78 -0
  226. package/scripts/lib/grading.js +203 -0
  227. package/scripts/lib/jsonl-lock.sh +48 -0
  228. package/scripts/lib/sanitize.sh +116 -0
  229. package/scripts/lib/transcript-parser.js +63 -0
  230. package/scripts/lint.js +47 -0
  231. package/scripts/migrate-to-bun-test.js +412 -0
  232. package/scripts/pr-coordinator.sh +706 -0
  233. package/scripts/run-command-eval.js +236 -0
  234. package/scripts/smart-status.sh +809 -0
  235. package/scripts/sync-commands.js +571 -0
  236. package/scripts/sync-utils.sh +455 -0
  237. package/scripts/test-dashboard.js +123 -0
  238. package/scripts/test.js +46 -0
  239. package/scripts/validate.sh +94 -0
  240. package/skills/parallel-deep-research/SKILL.md +108 -108
  241. package/skills/parallel-deep-research/evals/README.md +27 -27
  242. package/skills/parallel-deep-research/evals/evals.json +62 -62
  243. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  244. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  245. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  246. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  247. package/.cursor/hooks/state/continual-learning-index.json +0 -19
  248. package/.cursor/hooks/state/continual-learning.json +0 -8
@@ -0,0 +1,789 @@
1
+ #!/usr/bin/env bash
2
+ # dep-guard.sh — Dependency-guard helper for pre-change impact analysis.
3
+ #
4
+ # Subcommands:
5
+ # find-consumers <file-path> Find files that import/require a given module
6
+ # check-ripple <issue-id> Check ripple impact via keyword matching
7
+ # store-contracts <issue-id> <contracts-string> Store contract metadata on a Beads issue
8
+ # extract-contracts <file-path> Extract public API contracts from a file
9
+ #
10
+ # Cross-platform: works on Windows (Git Bash), macOS, and Linux.
11
+ # OWASP A03: All variables properly quoted to prevent shell injection.
12
+
13
+ set -euo pipefail
14
+
15
+ # Source shared sanitize library
16
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+ source "$SCRIPT_DIR/lib/sanitize.sh"
18
+
19
+ # ── Helpers ──────────────────────────────────────────────────────────────
20
+
21
+ usage() {
22
+ cat >&2 <<'EOF'
23
+ Usage: dep-guard.sh <subcommand> [args...]
24
+
25
+ Subcommands:
26
+ find-consumers <file-path> Find files that import/require a given module
27
+ check-ripple <issue-id> Check ripple impact via keyword matching
28
+ apply-decision <issue-id> <dependent-id> <depends-on-id> "<rationale>" Apply approved dependency decision via Beads
29
+ store-contracts <issue-id> <contracts-string> Store contract metadata on a Beads issue
30
+ extract-contracts <file-path> Extract public API contracts from a file
31
+ EOF
32
+ exit 1
33
+ }
34
+
35
+ die() {
36
+ echo "Error: $1" >&2
37
+ exit 1
38
+ }
39
+
40
+ cycles_output_is_safe() {
41
+ local output="$1"
42
+ printf '%s' "$output" | grep -Eqi 'no cycles? found|no cycles? detected|no dependency cycles|0 dependency cycles|0 cycles'
43
+ }
44
+
45
+ rollback_dependency() {
46
+ local dependent_issue="$1"
47
+ local depends_on_issue="$2"
48
+
49
+ local rollback_output
50
+ rollback_output="$(${BD_CMD:-bd} dep remove "$dependent_issue" "$depends_on_issue" 2>&1)" || {
51
+ echo "$rollback_output" >&2
52
+ die "Cycle detected for ${dependent_issue} -> ${depends_on_issue}; rollback failed and requires manual intervention"
53
+ }
54
+
55
+ printf '%s\n' "$rollback_output" > /dev/null
56
+ }
57
+
58
+ rollback_and_die() {
59
+ local dependent_issue="$1"
60
+ local depends_on_issue="$2"
61
+ local message="$3"
62
+ local command_output="${4:-}"
63
+
64
+ rollback_dependency "$dependent_issue" "$depends_on_issue"
65
+ if [[ -n "$command_output" ]]; then
66
+ echo "$command_output" >&2
67
+ fi
68
+ die "$message"
69
+ }
70
+
71
+ # Run bd update and check for errors in both exit code and output.
72
+ # bd update exits 0 even for non-existent issues, so we check stdout too.
73
+ bd_update() {
74
+ local output
75
+ output="$(${BD_CMD:-bd} update "$@" 2>&1)"
76
+ local rc=$?
77
+
78
+ if [[ $rc -ne 0 ]]; then
79
+ echo "$output" >&2
80
+ return 1
81
+ fi
82
+
83
+ # bd prints "Error resolving/updating ..." to stdout for non-existent issues
84
+ # Use specific patterns to avoid false positives from data containing "error"
85
+ if printf '%s' "$output" | grep -Eqi '^Error|Error resolving|Error updating'; then
86
+ echo "$output" >&2
87
+ return 1
88
+ fi
89
+
90
+ echo "$output"
91
+ return 0
92
+ }
93
+
94
+ bd_comment_add() {
95
+ local output
96
+ output="$(${BD_CMD:-bd} comments add "$@" 2>&1)"
97
+ local rc=$?
98
+
99
+ if [[ $rc -ne 0 ]]; then
100
+ echo "$output" >&2
101
+ return 1
102
+ fi
103
+
104
+ if printf '%s' "$output" | grep -Eqi '^Error|Error resolving|Error adding'; then
105
+ echo "$output" >&2
106
+ return 1
107
+ fi
108
+
109
+ echo "$output"
110
+ return 0
111
+ }
112
+
113
+ bd_set_state() {
114
+ local output
115
+ output="$(${BD_CMD:-bd} set-state "$@" 2>&1)"
116
+ local rc=$?
117
+
118
+ if [[ $rc -ne 0 ]]; then
119
+ echo "$output" >&2
120
+ return 1
121
+ fi
122
+
123
+ if printf '%s' "$output" | grep -Eqi '^Error|Error resolving|Error setting'; then
124
+ echo "$output" >&2
125
+ return 1
126
+ fi
127
+
128
+ echo "$output"
129
+ return 0
130
+ }
131
+
132
+ # Run bd show <id> --json and return the JSON string.
133
+ # Handles both array and object JSON responses.
134
+ bd_show_json() {
135
+ local issue_id="$1"
136
+
137
+ local json
138
+ json="$(${BD_CMD:-bd} show "$issue_id" --json 2>&1)" || die "Failed to show issue ${issue_id}"
139
+
140
+ # bd show may exit 0 but print an error for non-existent issues
141
+ if printf '%s' "$json" | grep -Eqi '^Error (fetching|resolving)'; then
142
+ die "Issue not found: ${issue_id}"
143
+ fi
144
+
145
+ # bd show returns "[]" or empty for non-existent issues
146
+ if [[ -z "$json" || "$json" == "[]" || "$json" == "null" ]]; then
147
+ die "Issue not found: ${issue_id}"
148
+ fi
149
+
150
+ # Normalize: if the response is an array, extract the first element
151
+ if command -v jq &>/dev/null; then
152
+ local normalized
153
+ normalized="$(printf '%s' "$json" | jq 'if type == "array" then .[0] else . end' 2>/dev/null)" || true
154
+ if [[ -n "$normalized" && "$normalized" != "null" ]]; then
155
+ json="$normalized"
156
+ fi
157
+ fi
158
+
159
+ printf '%s' "$json"
160
+ }
161
+
162
+ # Emit contract lines from files and description text.
163
+ # Args: $1 = comma-separated file paths, $2 = "What to implement" text
164
+ # Output: lines of "<file>:<function>(modified)" to stdout
165
+ emit_contracts() {
166
+ local files_str="$1"
167
+ local what_str="$2"
168
+
169
+ if [[ -z "$files_str" || -z "$what_str" ]]; then
170
+ return
171
+ fi
172
+
173
+ # Extract word() patterns from the what section
174
+ local funcs
175
+ funcs="$(printf '%s' "$what_str" | grep -oE '[a-zA-Z_][a-zA-Z0-9_]*\(\)' || true)"
176
+
177
+ if [[ -z "$funcs" ]]; then
178
+ return
179
+ fi
180
+
181
+ # Parse comma-separated file paths, strip backticks and whitespace
182
+ local IFS=','
183
+ local file_list
184
+ read -ra file_list <<< "$files_str"
185
+
186
+ local fp fn
187
+ for fp in "${file_list[@]}"; do
188
+ # Strip leading/trailing whitespace and backticks
189
+ fp="$(printf '%s' "$fp" | sed 's/^[[:space:]`]*//;s/[[:space:]`]*$//')"
190
+ [[ -z "$fp" ]] && continue
191
+
192
+ while IFS= read -r fn; do
193
+ [[ -z "$fn" ]] && continue
194
+ # Strip the trailing () from the function name
195
+ fn="${fn%()}"
196
+ printf '%s\n' "${fp}:${fn}(modified)"
197
+ done <<< "$funcs"
198
+ done
199
+ }
200
+
201
+ extract_task_file_from_design() {
202
+ local design_text="$1"
203
+
204
+ if [[ -z "$design_text" || "$design_text" != *"|"* ]]; then
205
+ return 1
206
+ fi
207
+
208
+ local task_file="${design_text#*|}"
209
+ task_file="$(printf '%s' "$task_file" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
210
+ [[ -n "$task_file" ]] || return 1
211
+ printf '%s' "$task_file"
212
+ }
213
+
214
+ run_phase3_analyzer() {
215
+ local current_json_file="$1"
216
+ local open_json_file="$2"
217
+ local in_progress_json_file="$3"
218
+ local task_file="$4"
219
+ local repository_root="$5"
220
+ local analyzer_script="${DEP_GUARD_ANALYZE_SCRIPT:-scripts/dep-guard-analyze.js}"
221
+
222
+ node "$analyzer_script" \
223
+ "$current_json_file" \
224
+ "$open_json_file" \
225
+ "$in_progress_json_file" \
226
+ "$task_file" \
227
+ "$repository_root"
228
+ }
229
+
230
+ render_phase3_review() {
231
+ local analysis_file="$1"
232
+
233
+ node - "$analysis_file" <<'NODE'
234
+ const fs = require('node:fs');
235
+
236
+ const analysisPath = process.argv[2];
237
+ const analysis = JSON.parse(fs.readFileSync(analysisPath, 'utf8'));
238
+ const detectorCategories = Object.entries(analysis.scores ?? {})
239
+ .filter(([name, score]) => name !== 'rubric' && Number(score) > 0)
240
+ .map(([name]) => name);
241
+
242
+ const lines = [
243
+ '',
244
+ `Structured dependency review for ${analysis.currentIssue.id}...`,
245
+ '',
246
+ ];
247
+
248
+ if (!Array.isArray(analysis.issues) || analysis.issues.length === 0) {
249
+ lines.push('No conflicts detected');
250
+ process.stdout.write(`${lines.join('\n')}\n`);
251
+ process.exit(0);
252
+ }
253
+
254
+ const seenPairs = new Set();
255
+ for (const finding of analysis.issues) {
256
+ const key = `${analysis.currentIssue.id}->${finding.targetIssueId}`;
257
+ if (seenPairs.has(key)) {
258
+ continue;
259
+ }
260
+ seenPairs.add(key);
261
+ lines.push(`Issue pair: ${analysis.currentIssue.id} -> ${finding.targetIssueId}`);
262
+ }
263
+
264
+ lines.push(`Rubric score: ${analysis.scores?.rubric ?? 0}`);
265
+ lines.push(
266
+ `Confidence: ${(analysis.confidence?.score ?? 0).toFixed(2)}${
267
+ analysis.confidence?.belowThreshold ? ' (below 70% threshold)' : ''
268
+ }`,
269
+ );
270
+ lines.push(`Detector categories: ${detectorCategories.join(', ') || 'none'}`);
271
+ lines.push(`Needs user decision: ${analysis.needsUserDecision ? 'yes' : 'no'}`);
272
+
273
+ if (Array.isArray(analysis.detectorConflicts) && analysis.detectorConflicts.length > 0) {
274
+ lines.push('Conflicts:');
275
+ for (const conflict of analysis.detectorConflicts) {
276
+ lines.push(` - ${conflict}`);
277
+ }
278
+ }
279
+
280
+ if (Array.isArray(analysis.proposals) && analysis.proposals.length > 0) {
281
+ lines.push('');
282
+ lines.push('Proposed dependency updates:');
283
+ for (const proposal of analysis.proposals) {
284
+ lines.push(` - ${proposal.dependentIssueId} depends on ${proposal.dependsOnIssueId}`);
285
+ if (Array.isArray(proposal.pros) && proposal.pros.length > 0) {
286
+ lines.push(` Pros: ${proposal.pros.join('; ')}`);
287
+ }
288
+ if (Array.isArray(proposal.cons) && proposal.cons.length > 0) {
289
+ lines.push(` Cons: ${proposal.cons.join('; ')}`);
290
+ }
291
+ }
292
+ }
293
+
294
+ process.stdout.write(`${lines.join('\n')}\n`);
295
+ NODE
296
+ }
297
+
298
+ # ── Subcommands ──────────────────────────────────────────────────────────
299
+
300
+ cmd_find_consumers() {
301
+ if [[ $# -lt 1 || -z "$1" ]]; then
302
+ echo "Usage: dep-guard.sh find-consumers <function-or-pattern>" >&2
303
+ exit 1
304
+ fi
305
+
306
+ local pattern
307
+ pattern="$(sanitize "$1")"
308
+
309
+ # Build list of directories that actually exist
310
+ local dirs=()
311
+ for d in lib/ scripts/ bin/ .claude/commands/ .forge/hooks/; do
312
+ [[ -d "$d" ]] && dirs+=("$d")
313
+ done
314
+
315
+ if [[ ${#dirs[@]} -eq 0 ]]; then
316
+ echo "No consumers found"
317
+ return 0
318
+ fi
319
+
320
+ # Grep across key directories, excluding noise
321
+ local results
322
+ results="$(grep -rn -e "$pattern" \
323
+ --include='*.js' --include='*.sh' --include='*.md' --include='*.ts' --include='*.json' \
324
+ --exclude-dir=node_modules --exclude-dir=.worktrees --exclude-dir=test --exclude-dir=test-env \
325
+ --exclude='dep-guard.sh' \
326
+ "${dirs[@]}" 2>/dev/null || true)"
327
+
328
+ if [[ -z "$results" ]]; then
329
+ echo "No consumers found"
330
+ return 0
331
+ fi
332
+
333
+ echo "$results"
334
+ return 0
335
+ }
336
+
337
+ cmd_check_ripple_keyword_v1() {
338
+ if [[ $# -lt 1 || -z "$1" ]]; then
339
+ echo "Usage: dep-guard.sh check-ripple <issue-id>" >&2
340
+ exit 1
341
+ fi
342
+
343
+ local issue_id
344
+ issue_id="$(sanitize "$1")"
345
+
346
+ # ── Step 1: Validate issue exists ──────────────────────────────────────
347
+ local src_json
348
+ src_json="$(bd_show_json "$issue_id")"
349
+
350
+ # ── Step 2: Extract source title ───────────────────────────────────────
351
+ local src_title=""
352
+ if command -v jq &>/dev/null; then
353
+ src_title="$(printf '%s' "$src_json" | jq -r '.title // ""' 2>/dev/null)" || true
354
+ fi
355
+ # Fallback: grep for title in JSON
356
+ if [[ -z "$src_title" ]]; then
357
+ src_title="$(printf '%s' "$src_json" | grep -oE '"title"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/^"title"[[:space:]]*:[[:space:]]*"//;s/"$//')" || true
358
+ fi
359
+
360
+ if [[ -z "$src_title" ]]; then
361
+ echo "⚠️ Warning: could not extract title for ${issue_id} — ripple check skipped" >&2
362
+ return 0
363
+ fi
364
+
365
+ echo ""
366
+ printf '%s\n' "📋 Ripple check for ${issue_id}..."
367
+ echo ""
368
+
369
+ # ── Step 3: Collect active issues ──────────────────────────────────────
370
+ # Run bd list for open and in_progress separately, combine results
371
+ local list_output=""
372
+ local open_list=""
373
+ local ip_list=""
374
+ open_list="$(${BD_CMD:-bd} list --status=open 2>/dev/null)" || true
375
+ ip_list="$(${BD_CMD:-bd} list --status=in_progress 2>/dev/null)" || true
376
+
377
+ # Combine and deduplicate (in case bd list returns overlapping results)
378
+ local combined=""
379
+ if [[ -n "$open_list" && -n "$ip_list" ]]; then
380
+ combined="${open_list}"$'\n'"${ip_list}"
381
+ elif [[ -n "$open_list" ]]; then
382
+ combined="$open_list"
383
+ elif [[ -n "$ip_list" ]]; then
384
+ combined="$ip_list"
385
+ fi
386
+
387
+ # Deduplicate by unique lines (preserves order via awk)
388
+ if [[ -n "$combined" ]]; then
389
+ list_output="$(printf '%s\n' "$combined" | awk '!seen[$0]++')"
390
+ fi
391
+
392
+ if [[ -z "$list_output" ]]; then
393
+ echo "⚠️ Warning: could not fetch active issue list — ripple check skipped" >&2
394
+ return 0
395
+ fi
396
+
397
+ # ── Step 4: Parse each active issue (excluding source) ─────────────────
398
+ # Format: ○ forge-xxx [● P2] [feature] - Title of the issue
399
+ # ◐ forge-yyy [● P1] [task] - Another issue title
400
+ local overlap_count=0
401
+ local overlap_report=""
402
+
403
+ # Stop words to exclude from keyword matching
404
+ local stop_words=" the a an and or is in to for of with on at by from add fix update implement create remove delete make use get set run test check all this that it be as not no but if do we they are was were been have has had will would could should may can each every both also into than then when where which what how why who its new first last same other "
405
+
406
+ # Tokenize source title: lowercase, split on non-alpha, filter stop words + short terms
407
+ local src_terms=""
408
+ src_terms="$(printf '%s' "$src_title" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alpha:]' '\n' | sort -u)"
409
+
410
+ # Filter source terms
411
+ local filtered_src_terms=""
412
+ while IFS= read -r term; do
413
+ [[ -z "$term" ]] && continue
414
+ # Skip terms < 3 characters
415
+ [[ ${#term} -lt 3 ]] && continue
416
+ # Skip stop words
417
+ if [[ "$stop_words" == *" ${term} "* ]]; then
418
+ continue
419
+ fi
420
+ filtered_src_terms="${filtered_src_terms} ${term}"
421
+ done <<< "$src_terms"
422
+
423
+ # Process each line in list_output
424
+ while IFS= read -r line; do
425
+ [[ -z "$line" ]] && continue
426
+
427
+ # Extract issue ID (forge-xxx pattern)
428
+ local cand_id=""
429
+ cand_id="$(printf '%s' "$line" | grep -oE 'forge-[a-z0-9]+' | head -1)" || continue
430
+ [[ -z "$cand_id" ]] && continue
431
+
432
+ # Skip the source issue itself
433
+ [[ "$cand_id" == "$issue_id" ]] && continue
434
+
435
+ # Extract status symbol and map to label
436
+ local cand_status="open"
437
+ if printf '%s' "$line" | grep -q '◐'; then
438
+ cand_status="in_progress"
439
+ fi
440
+
441
+ # Extract priority (P1, P2, P3, etc.)
442
+ local cand_priority=""
443
+ cand_priority="$(printf '%s' "$line" | grep -oE 'P[0-9]+' | head -1)" || true
444
+ [[ -z "$cand_priority" ]] && cand_priority="P2"
445
+
446
+ # Extract title (everything after " - ")
447
+ local cand_title=""
448
+ cand_title="$(printf '%s' "$line" | sed 's/^[^]]*\] [^]]*\] - //')" || continue
449
+ [[ -z "$cand_title" ]] && continue
450
+
451
+ # Tokenize candidate title
452
+ local cand_terms=""
453
+ cand_terms="$(printf '%s' "$cand_title" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alpha:]' '\n' | sort -u)"
454
+
455
+ # Filter candidate terms
456
+ local filtered_cand_terms=""
457
+ while IFS= read -r term; do
458
+ [[ -z "$term" ]] && continue
459
+ [[ ${#term} -lt 3 ]] && continue
460
+ if [[ "$stop_words" == *" ${term} "* ]]; then
461
+ continue
462
+ fi
463
+ filtered_cand_terms="${filtered_cand_terms} ${term}"
464
+ done <<< "$cand_terms"
465
+
466
+ # ── Step 6: Find shared meaningful terms ───────────────────────────────
467
+ local shared_terms=""
468
+ local shared_count=0
469
+ for src_t in $filtered_src_terms; do
470
+ for cand_t in $filtered_cand_terms; do
471
+ if [[ "$src_t" == "$cand_t" ]]; then
472
+ if [[ -z "$shared_terms" ]]; then
473
+ shared_terms="\"${src_t}\""
474
+ else
475
+ shared_terms="${shared_terms}, \"${src_t}\""
476
+ fi
477
+ shared_count=$((shared_count + 1))
478
+ break
479
+ fi
480
+ done
481
+ done
482
+
483
+ # ── Step 7: Report if >= 2 shared terms ────────────────────────────────
484
+ if [[ $shared_count -ge 2 ]]; then
485
+ overlap_count=$((overlap_count + 1))
486
+ overlap_report="${overlap_report} ${cand_id} (${cand_status}, ${cand_priority}): \"${cand_title}\""$'\n'
487
+ overlap_report="${overlap_report} Overlap: keyword match — ${shared_terms}"$'\n'
488
+ overlap_report="${overlap_report} Confidence: LOW (keyword only, no contract data)"$'\n'
489
+ overlap_report="${overlap_report}"$'\n'
490
+ overlap_report="${overlap_report} Options:"$'\n'
491
+ overlap_report="${overlap_report} (a) Add dependency: bd dep add ${issue_id} ${cand_id}"$'\n'
492
+ overlap_report="${overlap_report} (b) Proceed — no real conflict"$'\n'
493
+ overlap_report="${overlap_report} (c) Investigate: bd show ${cand_id}"$'\n'
494
+ overlap_report="${overlap_report}"$'\n'
495
+ fi
496
+
497
+ done <<< "$list_output"
498
+
499
+ # ── Step 8: Output final report ──────────────────────────────────────
500
+ if [[ $overlap_count -gt 0 ]]; then
501
+ printf '%s\n' "⚠️ Potential overlap with ${overlap_count} issue(s):"
502
+ echo ""
503
+ printf '%s' "$overlap_report"
504
+ else
505
+ printf '%s\n' "✅ No conflicts detected"
506
+ fi
507
+
508
+ return 0
509
+ }
510
+
511
+ cmd_check_ripple() {
512
+ if [[ $# -lt 1 || -z "$1" ]]; then
513
+ echo "Usage: dep-guard.sh check-ripple <issue-id>" >&2
514
+ exit 1
515
+ fi
516
+
517
+ local issue_id
518
+ issue_id="$(sanitize "$1")"
519
+
520
+ local src_json
521
+ src_json="$(bd_show_json "$issue_id")"
522
+
523
+ local src_title=""
524
+ if command -v jq &>/dev/null; then
525
+ src_title="$(printf '%s' "$src_json" | jq -r '.title // ""' 2>/dev/null)" || true
526
+ fi
527
+ if [[ -z "$src_title" ]]; then
528
+ src_title="$(printf '%s' "$src_json" | grep -oE '"title"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/^"title"[[:space:]]*:[[:space:]]*"//;s/"$//')" || true
529
+ fi
530
+
531
+ if [[ -z "$src_title" ]]; then
532
+ echo "⚠️ Warning: could not extract title for ${issue_id} — ripple check skipped" >&2
533
+ return 0
534
+ fi
535
+
536
+ local design_text=""
537
+ if command -v jq &>/dev/null; then
538
+ design_text="$(printf '%s' "$src_json" | jq -r '.design // ""' 2>/dev/null)" || true
539
+ fi
540
+ if [[ -z "$design_text" ]]; then
541
+ design_text="$(printf '%s' "$src_json" | grep -oE '"design"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/^"design"[[:space:]]*:[[:space:]]*"//;s/"$//')" || true
542
+ fi
543
+
544
+ local task_file=""
545
+ task_file="$(extract_task_file_from_design "$design_text" || true)"
546
+ if [[ -z "$task_file" || ! -f "$task_file" ]]; then
547
+ echo "⚠️ Structured analyzer unavailable, falling back to keyword-only ripple check." >&2
548
+ cmd_check_ripple_keyword_v1 "$issue_id"
549
+ return 0
550
+ fi
551
+
552
+ local open_json=""
553
+ local in_progress_json=""
554
+ open_json="$(${BD_CMD:-bd} list --status=open --json 2>/dev/null)" || true
555
+ in_progress_json="$(${BD_CMD:-bd} list --status=in_progress --json 2>/dev/null)" || true
556
+
557
+ if [[ -z "$open_json" && -z "$in_progress_json" ]]; then
558
+ echo "⚠️ Warning: could not fetch active issue list — ripple check skipped" >&2
559
+ return 0
560
+ fi
561
+
562
+ local tmp_dir
563
+ tmp_dir="$(mktemp -d)"
564
+ trap 'rm -rf "$tmp_dir"' RETURN
565
+
566
+ printf '%s' "$src_json" > "${tmp_dir}/current.json"
567
+ printf '%s' "${open_json:-[]}" > "${tmp_dir}/open.json"
568
+ printf '%s' "${in_progress_json:-[]}" > "${tmp_dir}/in-progress.json"
569
+
570
+ local repository_root="${DEP_GUARD_REPOSITORY_ROOT:-$PWD}"
571
+ if run_phase3_analyzer \
572
+ "${tmp_dir}/current.json" \
573
+ "${tmp_dir}/open.json" \
574
+ "${tmp_dir}/in-progress.json" \
575
+ "$task_file" \
576
+ "$repository_root" \
577
+ > "${tmp_dir}/analysis.json" 2> "${tmp_dir}/analysis.err"; then
578
+ if render_phase3_review "${tmp_dir}/analysis.json"; then
579
+ trap - RETURN
580
+ rm -rf "$tmp_dir"
581
+ return 0
582
+ fi
583
+ echo "⚠️ Phase 3 review rendering failed — falling back to keyword-only ripple check." >&2
584
+ else
585
+ echo "⚠️ Structured analyzer unavailable, falling back to keyword-only ripple check." >&2
586
+ if [[ -s "${tmp_dir}/analysis.err" ]]; then
587
+ cat "${tmp_dir}/analysis.err" >&2
588
+ fi
589
+ fi
590
+
591
+ trap - RETURN
592
+ rm -rf "$tmp_dir"
593
+ cmd_check_ripple_keyword_v1 "$issue_id"
594
+ }
595
+
596
+ cmd_apply_decision() {
597
+ if [[ $# -lt 4 ]]; then
598
+ echo "Usage: dep-guard.sh apply-decision <issue-id> <dependent-id> <depends-on-id> \"<rationale>\"" >&2
599
+ exit 1
600
+ fi
601
+
602
+ local issue_id
603
+ issue_id="$(sanitize "$1")"
604
+ local dependent_issue
605
+ dependent_issue="$(sanitize "$2")"
606
+ local depends_on_issue
607
+ depends_on_issue="$(sanitize "$3")"
608
+ local rationale
609
+ rationale="$(sanitize "$4")"
610
+
611
+ local dep_add_output
612
+ dep_add_output="$(${BD_CMD:-bd} dep add "$dependent_issue" "$depends_on_issue" 2>&1)" || {
613
+ echo "$dep_add_output" >&2
614
+ die "Failed to add dependency ${dependent_issue} -> ${depends_on_issue}"
615
+ }
616
+
617
+ # Check for cycles — use exit code as primary signal
618
+ if ! ${BD_CMD:-bd} dep cycles &>/dev/null; then
619
+ rollback_dependency "$dependent_issue" "$depends_on_issue"
620
+ die "Cycle detected for ${dependent_issue} -> ${depends_on_issue}"
621
+ fi
622
+
623
+ local graph_output
624
+ graph_output="$(${BD_CMD:-bd} graph "$issue_id" 2>&1)" || rollback_and_die \
625
+ "$dependent_issue" \
626
+ "$depends_on_issue" \
627
+ "Failed to render dependency graph for ${issue_id}" \
628
+ "$graph_output"
629
+
630
+ local ready_output
631
+ ready_output="$(${BD_CMD:-bd} ready 2>&1)" || rollback_and_die \
632
+ "$dependent_issue" \
633
+ "$depends_on_issue" \
634
+ "Failed to summarize ready work" \
635
+ "$ready_output"
636
+
637
+ if ! bd_set_state "$issue_id" "logicdep=approved" --reason "$rationale" > /dev/null; then
638
+ rollback_and_die \
639
+ "$dependent_issue" \
640
+ "$depends_on_issue" \
641
+ "Failed to persist approved decision state on ${issue_id}"
642
+ fi
643
+
644
+ if ! bd_comment_add "$issue_id" "Approved dependency: ${dependent_issue} depends on ${depends_on_issue}. ${rationale}" > /dev/null; then
645
+ rollback_and_die \
646
+ "$dependent_issue" \
647
+ "$depends_on_issue" \
648
+ "Failed to record approval rationale on ${issue_id}"
649
+ fi
650
+
651
+ echo "Approved dependency applied: ${dependent_issue} depends on ${depends_on_issue}"
652
+ echo "Graph:"
653
+ printf '%s\n' "$graph_output"
654
+ echo "Ready impact:"
655
+ printf '%s\n' "$ready_output"
656
+ }
657
+
658
+ cmd_store_contracts() {
659
+ if [[ $# -lt 2 ]]; then
660
+ echo "Usage: dep-guard.sh store-contracts <issue-id> <contracts-string>" >&2
661
+ exit 1
662
+ fi
663
+
664
+ local issue_id
665
+ issue_id="$(sanitize "$1")"
666
+ local contracts="$2"
667
+
668
+ if [[ -z "$contracts" ]]; then
669
+ die "Contracts string cannot be empty"
670
+ fi
671
+
672
+ contracts="$(sanitize "$contracts")"
673
+
674
+ # Validate issue exists
675
+ bd_show_json "$issue_id" > /dev/null
676
+
677
+ # Store contracts metadata with timestamp for deduplication.
678
+ # bd only supports --append-notes (no replace), so we prefix with a timestamp.
679
+ # Consumers (check-ripple) should use the LATEST contracts: line and ignore older ones.
680
+ local ts
681
+ ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
682
+ if ! bd_update "$issue_id" --append-notes "contracts@${ts}: ${contracts}" > /dev/null; then
683
+ die "Failed to store contracts on issue ${issue_id}"
684
+ fi
685
+
686
+ echo "Contracts stored on ${issue_id} (${ts})"
687
+ }
688
+
689
+ cmd_extract_contracts() {
690
+ if [[ $# -lt 1 ]]; then
691
+ echo "Usage: dep-guard.sh extract-contracts <file-path>" >&2
692
+ exit 1
693
+ fi
694
+
695
+ local task_file="$1"
696
+
697
+ # Validate file exists
698
+ if [[ ! -f "$task_file" ]]; then
699
+ die "File does not exist: ${task_file}"
700
+ fi
701
+
702
+ # Check that file contains at least one ## Task header
703
+ if ! grep -q '^## Task' "$task_file"; then
704
+ die "No tasks found in ${task_file}"
705
+ fi
706
+
707
+ # Parse task blocks using line-by-line bash processing.
708
+ # For each task: collect File(s) paths and function names from "What to implement".
709
+ local current_files=""
710
+ local current_what=""
711
+ local in_what=0
712
+ local all_contracts=""
713
+
714
+ while IFS= read -r line || [[ -n "$line" ]]; do
715
+ # New task block — flush previous
716
+ if [[ "$line" =~ ^##\ Task ]]; then
717
+ local _emitted
718
+ _emitted="$(emit_contracts "$current_files" "$current_what")"
719
+ [[ -n "$_emitted" ]] && all_contracts="${all_contracts}${_emitted}"$'\n'
720
+ current_files=""
721
+ current_what=""
722
+ in_what=0
723
+ continue
724
+ fi
725
+
726
+ # File(s): line
727
+ if [[ "$line" =~ ^File\(s\): ]]; then
728
+ current_files="${line#File(s):}"
729
+ current_files="$(printf '%s' "$current_files" | sed 's/^[[:space:]]*//')"
730
+ in_what=0
731
+ continue
732
+ fi
733
+
734
+ # What to implement: line
735
+ if [[ "$line" =~ ^What\ to\ implement: ]]; then
736
+ current_what="${line#What to implement:}"
737
+ current_what="$(printf '%s' "$current_what" | sed 's/^[[:space:]]*//')"
738
+ in_what=1
739
+ continue
740
+ fi
741
+
742
+ # Continue what section (stop at section boundaries)
743
+ if [[ $in_what -eq 1 ]]; then
744
+ if [[ "$line" =~ ^(##\ Task|File\(s\):|What\ to\ implement:|TDD\ |Expected\ output|---) ]]; then
745
+ in_what=0
746
+ continue
747
+ fi
748
+ current_what="${current_what} ${line}"
749
+ continue
750
+ fi
751
+ done < "$task_file"
752
+
753
+ # Flush the last task block
754
+ local _emitted
755
+ _emitted="$(emit_contracts "$current_files" "$current_what")"
756
+ [[ -n "$_emitted" ]] && all_contracts="${all_contracts}${_emitted}"$'\n'
757
+
758
+ # Deduplicate and sort
759
+ local contracts
760
+ contracts="$(printf '%s' "$all_contracts" | grep -v '^$' | sort -u)"
761
+
762
+ if [[ -z "$contracts" ]]; then
763
+ echo "No contracts found" >&2
764
+ exit 1
765
+ fi
766
+
767
+ printf '%s\n' "$contracts"
768
+ }
769
+
770
+ # ── Main dispatcher ──────────────────────────────────────────────────────
771
+
772
+ if [[ $# -lt 1 ]]; then
773
+ usage
774
+ fi
775
+
776
+ subcommand="$1"
777
+ shift
778
+
779
+ case "$subcommand" in
780
+ find-consumers) cmd_find_consumers "$@" ;;
781
+ check-ripple) cmd_check_ripple "$@" ;;
782
+ apply-decision) cmd_apply_decision "$@" ;;
783
+ store-contracts) cmd_store_contracts "$@" ;;
784
+ extract-contracts) cmd_extract_contracts "$@" ;;
785
+ *)
786
+ echo "Error: Unknown subcommand '${subcommand}'" >&2
787
+ usage
788
+ ;;
789
+ esac