forge-workflow 0.0.4 → 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 -340
  2. package/.claude/commands/plan.md +521 -521
  3. package/.claude/commands/premerge.md +176 -176
  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 -164
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +48 -48
  10. package/.claude/commands/validate.md +282 -282
  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 -337
  17. package/.cline/workflows/plan.md +518 -518
  18. package/.cline/workflows/premerge.md +173 -173
  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 -161
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +45 -45
  25. package/.cline/workflows/validate.md +279 -279
  26. package/.cline/workflows/verify.md +218 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +340 -340
  29. package/.codex/skills/plan/SKILL.md +521 -521
  30. package/.codex/skills/premerge/SKILL.md +176 -176
  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 -164
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +48 -48
  37. package/.codex/skills/validate/SKILL.md +282 -282
  38. package/.codex/skills/verify/SKILL.md +221 -221
  39. package/.cursor/commands/dev.md +337 -337
  40. package/.cursor/commands/plan.md +518 -518
  41. package/.cursor/commands/premerge.md +173 -173
  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 -161
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +45 -45
  48. package/.cursor/commands/validate.md +279 -279
  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 -342
  54. package/.github/prompts/plan.prompt.md +523 -523
  55. package/.github/prompts/premerge.prompt.md +178 -178
  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 -166
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +50 -50
  62. package/.github/prompts/validate.prompt.md +284 -284
  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 -341
  67. package/.kilocode/workflows/plan.md +522 -522
  68. package/.kilocode/workflows/premerge.md +177 -177
  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 -165
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +49 -49
  75. package/.kilocode/workflows/validate.md +283 -283
  76. package/.kilocode/workflows/verify.md +222 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +340 -340
  79. package/.opencode/commands/plan.md +521 -521
  80. package/.opencode/commands/premerge.md +176 -176
  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 -164
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +48 -48
  87. package/.opencode/commands/validate.md +282 -282
  88. package/.opencode/commands/verify.md +221 -221
  89. package/.roo/commands/dev.md +341 -341
  90. package/.roo/commands/plan.md +522 -522
  91. package/.roo/commands/premerge.md +177 -177
  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 -165
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +49 -49
  98. package/.roo/commands/validate.md +283 -283
  99. package/.roo/commands/verify.md +222 -222
  100. package/AGENTS.md +175 -175
  101. package/CLAUDE.md +100 -100
  102. package/README.md +429 -416
  103. package/bin/forge-cmd.js +313 -313
  104. package/bin/forge-preflight.js +309 -309
  105. package/bin/forge.js +4596 -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 +602 -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 +630 -630
  115. package/docs/VALIDATION.md +363 -363
  116. package/install.sh +40 -1056
  117. package/lefthook.yml +39 -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/dev.js +513 -513
  132. package/lib/commands/plan.js +692 -692
  133. package/lib/commands/recommend.js +119 -119
  134. package/lib/commands/ship.js +377 -377
  135. package/lib/commands/status.js +378 -378
  136. package/lib/commands/validate.js +602 -602
  137. package/lib/context-merge.js +359 -359
  138. package/lib/dep-guard/analyzer.js +294 -294
  139. package/lib/dep-guard/behavior-detector.js +98 -98
  140. package/lib/dep-guard/contract-detector.js +162 -162
  141. package/lib/dep-guard/import-detector.js +498 -498
  142. package/lib/dep-guard/path-utils.js +13 -13
  143. package/lib/dep-guard/rubric.js +120 -120
  144. package/lib/dep-guard/task-parser.js +318 -318
  145. package/lib/detect-agent.js +191 -191
  146. package/lib/detect-worktree.js +47 -47
  147. package/lib/file-hash.js +26 -26
  148. package/lib/husky-migration.js +450 -0
  149. package/lib/lefthook-check.js +65 -0
  150. package/lib/pat-setup.js +207 -0
  151. package/lib/plugin-catalog.js +350 -350
  152. package/lib/plugin-manager.js +166 -166
  153. package/lib/plugin-recommender.js +141 -141
  154. package/lib/project-discovery.js +491 -491
  155. package/lib/setup-action-log.js +139 -139
  156. package/lib/setup-summary-renderer.js +106 -106
  157. package/lib/setup-utils.js +96 -0
  158. package/lib/setup.js +192 -192
  159. package/lib/smart-merge.js +64 -0
  160. package/lib/symlink-utils.js +81 -0
  161. package/lib/workflow-profiles.js +197 -197
  162. package/package.json +131 -128
  163. package/scripts/beads-context.sh +291 -0
  164. package/scripts/beads-context.test.js +563 -0
  165. package/scripts/behavioral-judge.sh +378 -0
  166. package/scripts/benchmark.js +85 -0
  167. package/scripts/branch-protection.js +183 -0
  168. package/scripts/check-agents.js +172 -0
  169. package/scripts/commitlint.js +42 -0
  170. package/scripts/conflict-detect.sh +323 -0
  171. package/scripts/dep-guard-analyze.js +71 -0
  172. package/scripts/dep-guard.sh +811 -0
  173. package/scripts/eval_win.py +249 -0
  174. package/scripts/file-index.sh +399 -0
  175. package/scripts/github-beads-sync/comment.mjs +64 -0
  176. package/scripts/github-beads-sync/config.mjs +148 -0
  177. package/scripts/github-beads-sync/github-api.mjs +131 -0
  178. package/scripts/github-beads-sync/index.mjs +332 -0
  179. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  180. package/scripts/github-beads-sync/mapping.mjs +78 -0
  181. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  182. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  183. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  184. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  185. package/scripts/github-beads-sync.config.json +26 -0
  186. package/scripts/improve-command.js +375 -0
  187. package/scripts/lib/eval-runner.js +229 -0
  188. package/scripts/lib/eval-schema.js +135 -0
  189. package/scripts/lib/eval-storage.js +78 -0
  190. package/scripts/lib/grading.js +203 -0
  191. package/scripts/lib/transcript-parser.js +63 -0
  192. package/scripts/lint.js +47 -0
  193. package/scripts/migrate-to-bun-test.js +412 -0
  194. package/scripts/run-command-eval.js +236 -0
  195. package/scripts/smart-status.sh +782 -0
  196. package/scripts/sync-commands.js +571 -0
  197. package/scripts/sync-utils.sh +460 -0
  198. package/scripts/test-dashboard.js +123 -0
  199. package/scripts/test.js +44 -0
  200. package/scripts/validate.sh +94 -0
  201. package/skills/parallel-deep-research/SKILL.md +108 -108
  202. package/skills/parallel-deep-research/evals/README.md +27 -27
  203. package/skills/parallel-deep-research/evals/evals.json +62 -62
  204. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  205. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  206. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  207. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  208. package/.cursor/hooks/state/continual-learning-index.json +0 -19
  209. package/.cursor/hooks/state/continual-learning.json +0 -8
@@ -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