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.
- package/.claude/commands/dev.md +340 -314
- package/.claude/commands/plan.md +521 -478
- package/.claude/commands/premerge.md +176 -179
- package/.claude/commands/research.md +42 -42
- package/.claude/commands/review.md +442 -442
- package/.claude/commands/rollback.md +721 -721
- package/.claude/commands/ship.md +164 -134
- package/.claude/commands/sonarcloud.md +152 -152
- package/.claude/commands/status.md +48 -77
- package/.claude/commands/validate.md +282 -237
- package/.claude/commands/verify.md +221 -221
- package/.claude/rules/greptile-review-process.md +285 -285
- package/.claude/rules/workflow.md +105 -105
- package/.claude/scripts/greptile-resolve.sh +526 -526
- package/.claude/scripts/load-env.sh +32 -32
- package/.cline/workflows/dev.md +337 -311
- package/.cline/workflows/plan.md +518 -475
- package/.cline/workflows/premerge.md +173 -176
- package/.cline/workflows/research.md +39 -39
- package/.cline/workflows/review.md +439 -439
- package/.cline/workflows/rollback.md +718 -718
- package/.cline/workflows/ship.md +161 -131
- package/.cline/workflows/sonarcloud.md +146 -146
- package/.cline/workflows/status.md +45 -74
- package/.cline/workflows/validate.md +279 -234
- package/.cline/workflows/verify.md +218 -218
- package/.codex/config.toml +11 -11
- package/.codex/skills/dev/SKILL.md +340 -314
- package/.codex/skills/plan/SKILL.md +521 -478
- package/.codex/skills/premerge/SKILL.md +176 -179
- package/.codex/skills/research/SKILL.md +42 -42
- package/.codex/skills/review/SKILL.md +442 -442
- package/.codex/skills/rollback/SKILL.md +721 -721
- package/.codex/skills/ship/SKILL.md +164 -134
- package/.codex/skills/sonarcloud/SKILL.md +149 -149
- package/.codex/skills/status/SKILL.md +48 -77
- package/.codex/skills/validate/SKILL.md +282 -237
- package/.codex/skills/verify/SKILL.md +221 -221
- package/.cursor/commands/dev.md +337 -311
- package/.cursor/commands/plan.md +518 -475
- package/.cursor/commands/premerge.md +173 -176
- package/.cursor/commands/research.md +39 -39
- package/.cursor/commands/review.md +439 -439
- package/.cursor/commands/rollback.md +718 -718
- package/.cursor/commands/ship.md +161 -131
- package/.cursor/commands/sonarcloud.md +146 -146
- package/.cursor/commands/status.md +45 -74
- package/.cursor/commands/validate.md +279 -234
- package/.cursor/commands/verify.md +218 -218
- package/.cursor/rules/permissions-guidance.mdc +37 -37
- package/.forge/hooks/check-tdd.js +240 -240
- package/.github/PLUGIN_TEMPLATE.json +32 -32
- package/.github/prompts/dev.prompt.md +342 -316
- package/.github/prompts/plan.prompt.md +523 -480
- package/.github/prompts/premerge.prompt.md +178 -181
- package/.github/prompts/research.prompt.md +44 -44
- package/.github/prompts/review.prompt.md +444 -444
- package/.github/prompts/rollback.prompt.md +723 -723
- package/.github/prompts/ship.prompt.md +166 -136
- package/.github/prompts/sonarcloud.prompt.md +151 -151
- package/.github/prompts/status.prompt.md +50 -79
- package/.github/prompts/validate.prompt.md +284 -239
- package/.github/prompts/verify.prompt.md +223 -223
- package/.github/workflows/beads-to-github.yml +56 -0
- package/.github/workflows/github-to-beads.yml +97 -0
- package/.kilocode/workflows/dev.md +341 -315
- package/.kilocode/workflows/plan.md +522 -479
- package/.kilocode/workflows/premerge.md +177 -180
- package/.kilocode/workflows/research.md +43 -43
- package/.kilocode/workflows/review.md +443 -443
- package/.kilocode/workflows/rollback.md +722 -722
- package/.kilocode/workflows/ship.md +165 -135
- package/.kilocode/workflows/sonarcloud.md +150 -150
- package/.kilocode/workflows/status.md +49 -78
- package/.kilocode/workflows/validate.md +283 -238
- package/.kilocode/workflows/verify.md +222 -222
- package/.mcp.json.example +12 -12
- package/.opencode/commands/dev.md +340 -314
- package/.opencode/commands/plan.md +521 -478
- package/.opencode/commands/premerge.md +176 -179
- package/.opencode/commands/research.md +42 -42
- package/.opencode/commands/review.md +442 -442
- package/.opencode/commands/rollback.md +721 -721
- package/.opencode/commands/ship.md +164 -134
- package/.opencode/commands/sonarcloud.md +149 -149
- package/.opencode/commands/status.md +48 -77
- package/.opencode/commands/validate.md +282 -237
- package/.opencode/commands/verify.md +221 -221
- package/.roo/commands/dev.md +341 -315
- package/.roo/commands/plan.md +522 -479
- package/.roo/commands/premerge.md +177 -180
- package/.roo/commands/research.md +43 -43
- package/.roo/commands/review.md +443 -443
- package/.roo/commands/rollback.md +722 -722
- package/.roo/commands/ship.md +165 -135
- package/.roo/commands/sonarcloud.md +150 -150
- package/.roo/commands/status.md +49 -78
- package/.roo/commands/validate.md +283 -238
- package/.roo/commands/verify.md +222 -222
- package/AGENTS.md +175 -169
- package/CLAUDE.md +100 -99
- package/LICENSE +21 -21
- package/README.md +429 -414
- package/bin/forge-cmd.js +313 -313
- package/bin/{forge-validate.js → forge-preflight.js} +309 -303
- package/bin/forge.js +4596 -4232
- package/docs/AGENT_INSTALL_PROMPT.md +342 -342
- package/docs/BEADS_GITHUB_SYNC.md +251 -0
- package/docs/ENHANCED_ONBOARDING.md +602 -602
- package/docs/EXAMPLES.md +482 -482
- package/docs/GREPTILE_SETUP.md +400 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
- package/docs/ROADMAP.md +359 -359
- package/docs/SETUP.md +663 -632
- package/docs/TOOLCHAIN.md +630 -630
- package/docs/VALIDATION.md +363 -363
- package/install.sh +40 -1058
- package/lefthook.yml +39 -39
- package/lib/agents/README.md +198 -198
- package/lib/agents/claude.plugin.json +28 -28
- package/lib/agents/cline.plugin.json +22 -22
- package/lib/agents/codex.plugin.json +19 -19
- package/lib/agents/copilot.plugin.json +24 -24
- package/lib/agents/cursor.plugin.json +25 -25
- package/lib/agents/kilocode.plugin.json +22 -22
- package/lib/agents/opencode.plugin.json +20 -20
- package/lib/agents/roo.plugin.json +23 -23
- package/lib/agents-config.js +2112 -2112
- package/lib/beads-health-check.js +143 -0
- package/lib/beads-setup.js +341 -0
- package/lib/beads-sync-scaffold.js +260 -0
- package/lib/commands/dev.js +513 -513
- package/lib/commands/plan.js +692 -692
- package/lib/commands/recommend.js +119 -119
- package/lib/commands/ship.js +377 -377
- package/lib/commands/status.js +378 -378
- package/lib/commands/validate.js +602 -602
- package/lib/context-merge.js +359 -359
- package/lib/dep-guard/analyzer.js +294 -294
- package/lib/dep-guard/behavior-detector.js +98 -98
- package/lib/dep-guard/contract-detector.js +162 -162
- package/lib/dep-guard/import-detector.js +498 -498
- package/lib/dep-guard/path-utils.js +13 -13
- package/lib/dep-guard/rubric.js +120 -120
- package/lib/dep-guard/task-parser.js +318 -318
- package/lib/detect-agent.js +191 -0
- package/lib/detect-worktree.js +47 -0
- package/lib/file-hash.js +26 -0
- package/lib/husky-migration.js +450 -0
- package/lib/lefthook-check.js +65 -0
- package/lib/pat-setup.js +207 -0
- package/lib/plugin-catalog.js +350 -350
- package/lib/plugin-manager.js +166 -166
- package/lib/plugin-recommender.js +141 -141
- package/lib/project-discovery.js +491 -491
- package/lib/setup-action-log.js +139 -0
- package/lib/setup-summary-renderer.js +106 -0
- package/lib/setup-utils.js +96 -0
- package/lib/setup.js +192 -118
- package/lib/smart-merge.js +64 -0
- package/lib/symlink-utils.js +81 -0
- package/lib/workflow-profiles.js +197 -197
- package/package.json +131 -129
- package/scripts/beads-context.sh +291 -0
- package/scripts/beads-context.test.js +563 -0
- package/scripts/behavioral-judge.sh +378 -0
- package/scripts/benchmark.js +85 -0
- package/scripts/branch-protection.js +183 -0
- package/scripts/check-agents.js +172 -0
- package/scripts/commitlint.js +42 -0
- package/scripts/conflict-detect.sh +323 -0
- package/scripts/dep-guard-analyze.js +71 -0
- package/scripts/dep-guard.sh +811 -0
- package/scripts/eval_win.py +249 -0
- package/scripts/file-index.sh +399 -0
- package/scripts/github-beads-sync/comment.mjs +64 -0
- package/scripts/github-beads-sync/config.mjs +148 -0
- package/scripts/github-beads-sync/github-api.mjs +131 -0
- package/scripts/github-beads-sync/index.mjs +332 -0
- package/scripts/github-beads-sync/label-mapper.mjs +54 -0
- package/scripts/github-beads-sync/mapping.mjs +78 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
- package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
- package/scripts/github-beads-sync/run-bd.mjs +159 -0
- package/scripts/github-beads-sync/sanitize.mjs +121 -0
- package/scripts/github-beads-sync.config.json +26 -0
- package/scripts/improve-command.js +375 -0
- package/scripts/lib/eval-runner.js +229 -0
- package/scripts/lib/eval-schema.js +135 -0
- package/scripts/lib/eval-storage.js +78 -0
- package/scripts/lib/grading.js +203 -0
- package/scripts/lib/transcript-parser.js +63 -0
- package/scripts/lint.js +47 -0
- package/scripts/migrate-to-bun-test.js +412 -0
- package/scripts/run-command-eval.js +236 -0
- package/scripts/smart-status.sh +782 -0
- package/scripts/sync-commands.js +571 -0
- package/scripts/sync-utils.sh +460 -0
- package/scripts/test-dashboard.js +123 -0
- package/scripts/test.js +44 -0
- package/scripts/validate.sh +94 -0
- package/skills/parallel-deep-research/SKILL.md +108 -108
- package/skills/parallel-deep-research/evals/README.md +27 -27
- package/skills/parallel-deep-research/evals/evals.json +62 -62
- package/skills/sonarcloud-analysis/SKILL.md +171 -171
- package/skills/sonarcloud-analysis/evals/README.md +27 -27
- package/skills/sonarcloud-analysis/evals/evals.json +50 -50
- package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
- 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
|