forge-workflow 0.0.3 → 0.0.5

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