forge-workflow 0.0.9 → 0.0.10
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 +2 -2
- package/.claude/commands/plan.md +2 -2
- package/.claude/commands/ship.md +2 -2
- package/.claude/commands/status.md +4 -4
- package/.cline/workflows/dev.md +2 -2
- package/.cline/workflows/plan.md +2 -2
- package/.cline/workflows/ship.md +2 -2
- package/.cline/workflows/status.md +4 -4
- package/.codex/skills/dev/SKILL.md +2 -2
- package/.codex/skills/plan/SKILL.md +2 -2
- package/.codex/skills/ship/SKILL.md +2 -2
- package/.codex/skills/status/SKILL.md +4 -4
- package/.cursor/commands/dev.md +2 -2
- package/.cursor/commands/plan.md +2 -2
- package/.cursor/commands/ship.md +2 -2
- package/.cursor/commands/status.md +4 -4
- package/.github/prompts/dev.prompt.md +2 -2
- package/.github/prompts/plan.prompt.md +2 -2
- package/.github/prompts/ship.prompt.md +2 -2
- package/.github/prompts/status.prompt.md +4 -4
- package/.github/workflows/beads-to-github.yml +43 -10
- package/.github/workflows/github-to-beads.yml +10 -7
- package/.kilocode/workflows/dev.md +2 -2
- package/.kilocode/workflows/plan.md +2 -2
- package/.kilocode/workflows/ship.md +2 -2
- package/.kilocode/workflows/status.md +4 -4
- package/.opencode/commands/dev.md +2 -2
- package/.opencode/commands/plan.md +2 -2
- package/.opencode/commands/ship.md +2 -2
- package/.opencode/commands/status.md +4 -4
- package/.roo/commands/dev.md +2 -2
- package/.roo/commands/plan.md +2 -2
- package/.roo/commands/ship.md +2 -2
- package/.roo/commands/status.md +4 -4
- package/AGENTS.md +1 -0
- package/CLAUDE.md +12 -0
- package/bin/forge.js +10 -5
- package/docs/BEADS_GITHUB_SYNC.md +26 -0
- package/docs/TOOLCHAIN.md +130 -148
- package/lib/beads-bootstrap.js +225 -0
- package/lib/beads-health-check.js +55 -10
- package/lib/beads-setup.js +104 -28
- package/lib/beads-sync-scaffold.js +11 -6
- package/lib/commands/_issue.js +11 -1
- package/lib/commands/issues.js +49 -0
- package/lib/commands/recommend.js +22 -1
- package/lib/commands/setup.js +16 -10
- package/lib/commands/status.js +181 -0
- package/lib/commands/team.js +11 -1
- package/lib/commands/test.js +37 -2
- package/lib/commands/validate.js +14 -8
- package/lib/commands/worktree.js +27 -54
- package/lib/dep-guard/keyword-ripple.js +184 -0
- package/lib/detect-worktree.js +9 -10
- package/lib/forge-issues.js +326 -0
- package/lib/issue-sync/authority.js +100 -0
- package/lib/issue-sync/github-pull.js +184 -0
- package/lib/issue-sync/import-primitives.js +98 -0
- package/lib/issue-sync/legacy-link-bridge.js +436 -0
- package/lib/issue-sync/link-store.js +292 -0
- package/lib/issue-sync/project-github.js +123 -0
- package/lib/issue-sync/reconcile.js +195 -0
- package/lib/issue-sync/schema.js +126 -0
- package/lib/lefthook-check.js +5 -2
- package/lib/project-memory.js +564 -0
- package/lib/runtime-health.js +100 -12
- package/lib/smart-status/conflicts.js +205 -0
- package/lib/smart-status/scoring.js +177 -0
- package/lib/status/beads-snapshot.js +102 -0
- package/lib/status/presenter.js +65 -0
- package/lib/workflow/enforce-stage.js +3 -1
- package/lib/workflow/state-manager.js +164 -8
- package/package.json +12 -4
- package/scripts/beads-context.sh +124 -5
- package/scripts/beads-context.test.js +21 -4
- package/scripts/beads-migrate-to-dolt.sh +7 -0
- package/scripts/beads-upgrade-smoke.sh +263 -0
- package/scripts/behavioral-judge.sh +115 -11
- package/scripts/benchmark.js +349 -63
- package/scripts/dep-guard-analyze.js +52 -17
- package/scripts/dep-guard-keyword-ripple.js +29 -0
- package/scripts/dep-guard-render-review.js +86 -0
- package/scripts/dep-guard.sh +45 -232
- package/scripts/forge-team/lib/sync-github.sh +160 -28
- package/scripts/forge-team/tests/sync-github.test.sh +195 -58
- package/scripts/github-beads-sync/index.mjs +122 -98
- package/scripts/github-beads-sync/mapping.mjs +54 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +2 -2
- package/scripts/github-beads-sync/reverse-sync.mjs +31 -7
- package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
- package/scripts/preflight.sh +181 -0
- package/scripts/smart-status-score.js +31 -0
- package/scripts/smart-status-sessions.js +51 -0
- package/scripts/smart-status.sh +74 -329
- package/scripts/sync-agentic-workflow.js +48 -0
- package/scripts/test-ci-shard.js +244 -0
- package/scripts/test-dashboard.js +188 -52
- package/scripts/test-full-suite.js +186 -0
- package/scripts/test-profile.js +278 -0
- package/scripts/test.js +219 -28
- package/scripts/validate.js +143 -0
- package/scripts/validate.sh +18 -1
package/scripts/dep-guard.sh
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# dep-guard.sh
|
|
2
|
+
# dep-guard.sh -- Dependency-guard helper for pre-change impact analysis.
|
|
3
3
|
#
|
|
4
4
|
# Subcommands:
|
|
5
5
|
# find-consumers <file-path> Find files that import/require a given module
|
|
@@ -14,9 +14,10 @@ set -euo pipefail
|
|
|
14
14
|
|
|
15
15
|
# Source shared sanitize library
|
|
16
16
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
NODE_CMD="${NODE_CMD:-node}"
|
|
17
18
|
source "$SCRIPT_DIR/lib/sanitize.sh"
|
|
18
19
|
|
|
19
|
-
#
|
|
20
|
+
# ---- Helpers ----------------------------------------------------------------
|
|
20
21
|
|
|
21
22
|
usage() {
|
|
22
23
|
cat >&2 <<'EOF'
|
|
@@ -212,90 +213,28 @@ extract_task_file_from_design() {
|
|
|
212
213
|
}
|
|
213
214
|
|
|
214
215
|
run_phase3_analyzer() {
|
|
215
|
-
local
|
|
216
|
-
local
|
|
217
|
-
local
|
|
216
|
+
local current_json="$1"
|
|
217
|
+
local open_json="$2"
|
|
218
|
+
local in_progress_json="$3"
|
|
218
219
|
local task_file="$4"
|
|
219
220
|
local repository_root="$5"
|
|
220
221
|
local analyzer_script="${DEP_GUARD_ANALYZE_SCRIPT:-scripts/dep-guard-analyze.js}"
|
|
221
222
|
|
|
222
|
-
|
|
223
|
-
"$
|
|
224
|
-
"$
|
|
225
|
-
"$
|
|
226
|
-
"$task_file" \
|
|
227
|
-
"$repository_root"
|
|
223
|
+
printf '{"currentIssue":%s,"openIssues":%s,"inProgressIssues":%s,"taskFile":%s,"repositoryRoot":%s}' \
|
|
224
|
+
"$current_json" \
|
|
225
|
+
"${open_json:-[]}" \
|
|
226
|
+
"${in_progress_json:-[]}" \
|
|
227
|
+
"$(printf '%s' "$task_file" | jq -R '.')" \
|
|
228
|
+
"$(printf '%s' "$repository_root" | jq -R '.')" \
|
|
229
|
+
| "$NODE_CMD" "$analyzer_script" --stdin
|
|
228
230
|
}
|
|
229
231
|
|
|
230
232
|
render_phase3_review() {
|
|
231
|
-
local
|
|
232
|
-
|
|
233
|
-
node - "$analysis_file" <<'NODE'
|
|
234
|
-
const fs = require('node:fs');
|
|
235
|
-
|
|
236
|
-
const analysisPath = process.argv[2];
|
|
237
|
-
const analysis = JSON.parse(fs.readFileSync(analysisPath, 'utf8'));
|
|
238
|
-
const detectorCategories = Object.entries(analysis.scores ?? {})
|
|
239
|
-
.filter(([name, score]) => name !== 'rubric' && Number(score) > 0)
|
|
240
|
-
.map(([name]) => name);
|
|
241
|
-
|
|
242
|
-
const lines = [
|
|
243
|
-
'',
|
|
244
|
-
`Structured dependency review for ${analysis.currentIssue.id}...`,
|
|
245
|
-
'',
|
|
246
|
-
];
|
|
247
|
-
|
|
248
|
-
if (!Array.isArray(analysis.issues) || analysis.issues.length === 0) {
|
|
249
|
-
lines.push('No conflicts detected');
|
|
250
|
-
process.stdout.write(`${lines.join('\n')}\n`);
|
|
251
|
-
process.exit(0);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const seenPairs = new Set();
|
|
255
|
-
for (const finding of analysis.issues) {
|
|
256
|
-
const key = `${analysis.currentIssue.id}->${finding.targetIssueId}`;
|
|
257
|
-
if (seenPairs.has(key)) {
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
seenPairs.add(key);
|
|
261
|
-
lines.push(`Issue pair: ${analysis.currentIssue.id} -> ${finding.targetIssueId}`);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
lines.push(`Rubric score: ${analysis.scores?.rubric ?? 0}`);
|
|
265
|
-
lines.push(
|
|
266
|
-
`Confidence: ${(analysis.confidence?.score ?? 0).toFixed(2)}${
|
|
267
|
-
analysis.confidence?.belowThreshold ? ' (below 70% threshold)' : ''
|
|
268
|
-
}`,
|
|
269
|
-
);
|
|
270
|
-
lines.push(`Detector categories: ${detectorCategories.join(', ') || 'none'}`);
|
|
271
|
-
lines.push(`Needs user decision: ${analysis.needsUserDecision ? 'yes' : 'no'}`);
|
|
272
|
-
|
|
273
|
-
if (Array.isArray(analysis.detectorConflicts) && analysis.detectorConflicts.length > 0) {
|
|
274
|
-
lines.push('Conflicts:');
|
|
275
|
-
for (const conflict of analysis.detectorConflicts) {
|
|
276
|
-
lines.push(` - ${conflict}`);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (Array.isArray(analysis.proposals) && analysis.proposals.length > 0) {
|
|
281
|
-
lines.push('');
|
|
282
|
-
lines.push('Proposed dependency updates:');
|
|
283
|
-
for (const proposal of analysis.proposals) {
|
|
284
|
-
lines.push(` - ${proposal.dependentIssueId} depends on ${proposal.dependsOnIssueId}`);
|
|
285
|
-
if (Array.isArray(proposal.pros) && proposal.pros.length > 0) {
|
|
286
|
-
lines.push(` Pros: ${proposal.pros.join('; ')}`);
|
|
287
|
-
}
|
|
288
|
-
if (Array.isArray(proposal.cons) && proposal.cons.length > 0) {
|
|
289
|
-
lines.push(` Cons: ${proposal.cons.join('; ')}`);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
process.stdout.write(`${lines.join('\n')}\n`);
|
|
295
|
-
NODE
|
|
233
|
+
local renderer_script="${DEP_GUARD_RENDER_SCRIPT:-scripts/dep-guard-render-review.js}"
|
|
234
|
+
"$NODE_CMD" "$renderer_script"
|
|
296
235
|
}
|
|
297
236
|
|
|
298
|
-
#
|
|
237
|
+
# ---- Subcommands ------------------------------------------------------------
|
|
299
238
|
|
|
300
239
|
cmd_find_consumers() {
|
|
301
240
|
if [[ $# -lt 1 || -z "$1" ]]; then
|
|
@@ -343,11 +282,11 @@ cmd_check_ripple_keyword_v1() {
|
|
|
343
282
|
local issue_id
|
|
344
283
|
issue_id="$(sanitize "$1")"
|
|
345
284
|
|
|
346
|
-
#
|
|
285
|
+
# ---- Step 1: Validate issue exists ----------------------------------------
|
|
347
286
|
local src_json
|
|
348
287
|
src_json="$(bd_show_json "$issue_id")"
|
|
349
288
|
|
|
350
|
-
#
|
|
289
|
+
# ---- Step 2: Extract source title ----------------------------------------
|
|
351
290
|
local src_title=""
|
|
352
291
|
if command -v jq &>/dev/null; then
|
|
353
292
|
src_title="$(printf '%s' "$src_json" | jq -r '.title // ""' 2>/dev/null)" || true
|
|
@@ -358,15 +297,15 @@ cmd_check_ripple_keyword_v1() {
|
|
|
358
297
|
fi
|
|
359
298
|
|
|
360
299
|
if [[ -z "$src_title" ]]; then
|
|
361
|
-
echo "
|
|
300
|
+
echo "Warning: could not extract title for ${issue_id} - ripple check skipped" >&2
|
|
362
301
|
return 0
|
|
363
302
|
fi
|
|
364
303
|
|
|
365
304
|
echo ""
|
|
366
|
-
printf '%s\n' "
|
|
305
|
+
printf '%s\n' "Ripple check for ${issue_id}..."
|
|
367
306
|
echo ""
|
|
368
307
|
|
|
369
|
-
#
|
|
308
|
+
# ---- Step 3: Collect active issues ---------------------------------------
|
|
370
309
|
# Run bd list for open and in_progress separately, combine results
|
|
371
310
|
local list_output=""
|
|
372
311
|
local open_list=""
|
|
@@ -390,122 +329,24 @@ cmd_check_ripple_keyword_v1() {
|
|
|
390
329
|
fi
|
|
391
330
|
|
|
392
331
|
if [[ -z "$list_output" ]]; then
|
|
393
|
-
echo "
|
|
332
|
+
echo "Warning: could not fetch active issue list - ripple check skipped" >&2
|
|
394
333
|
return 0
|
|
395
334
|
fi
|
|
396
335
|
|
|
397
|
-
#
|
|
398
|
-
# Format:
|
|
399
|
-
#
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
# Stop words to exclude from keyword matching
|
|
404
|
-
local stop_words=" the a an and or is in to for of with on at by from add fix update implement create remove delete make use get set run test check all this that it be as not no but if do we they are was were been have has had will would could should may can each every both also into than then when where which what how why who its new first last same other "
|
|
405
|
-
|
|
406
|
-
# Tokenize source title: lowercase, split on non-alpha, filter stop words + short terms
|
|
407
|
-
local src_terms=""
|
|
408
|
-
src_terms="$(printf '%s' "$src_title" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alpha:]' '\n' | sort -u)"
|
|
409
|
-
|
|
410
|
-
# Filter source terms
|
|
411
|
-
local filtered_src_terms=""
|
|
412
|
-
while IFS= read -r term; do
|
|
413
|
-
[[ -z "$term" ]] && continue
|
|
414
|
-
# Skip terms < 3 characters
|
|
415
|
-
[[ ${#term} -lt 3 ]] && continue
|
|
416
|
-
# Skip stop words
|
|
417
|
-
if [[ "$stop_words" == *" ${term} "* ]]; then
|
|
418
|
-
continue
|
|
419
|
-
fi
|
|
420
|
-
filtered_src_terms="${filtered_src_terms} ${term}"
|
|
421
|
-
done <<< "$src_terms"
|
|
422
|
-
|
|
423
|
-
# Process each line in list_output
|
|
424
|
-
while IFS= read -r line; do
|
|
425
|
-
[[ -z "$line" ]] && continue
|
|
426
|
-
|
|
427
|
-
# Extract issue ID (forge-xxx pattern)
|
|
428
|
-
local cand_id=""
|
|
429
|
-
cand_id="$(printf '%s' "$line" | grep -oE 'forge-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*' | head -1)" || continue
|
|
430
|
-
[[ -z "$cand_id" ]] && continue
|
|
431
|
-
|
|
432
|
-
# Skip the source issue itself
|
|
433
|
-
[[ "$cand_id" == "$issue_id" ]] && continue
|
|
434
|
-
|
|
435
|
-
# Extract status symbol and map to label
|
|
436
|
-
local cand_status="open"
|
|
437
|
-
if printf '%s' "$line" | grep -q '◐'; then
|
|
438
|
-
cand_status="in_progress"
|
|
439
|
-
fi
|
|
440
|
-
|
|
441
|
-
# Extract priority (P1, P2, P3, etc.)
|
|
442
|
-
local cand_priority=""
|
|
443
|
-
cand_priority="$(printf '%s' "$line" | grep -oE 'P[0-9]+' | head -1)" || true
|
|
444
|
-
[[ -z "$cand_priority" ]] && cand_priority="P2"
|
|
445
|
-
|
|
446
|
-
# Extract title (everything after " - ")
|
|
447
|
-
local cand_title=""
|
|
448
|
-
cand_title="$(printf '%s' "$line" | sed 's/^[^]]*\] [^]]*\] - //')" || continue
|
|
449
|
-
[[ -z "$cand_title" ]] && continue
|
|
450
|
-
|
|
451
|
-
# Tokenize candidate title
|
|
452
|
-
local cand_terms=""
|
|
453
|
-
cand_terms="$(printf '%s' "$cand_title" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alpha:]' '\n' | sort -u)"
|
|
454
|
-
|
|
455
|
-
# Filter candidate terms
|
|
456
|
-
local filtered_cand_terms=""
|
|
457
|
-
while IFS= read -r term; do
|
|
458
|
-
[[ -z "$term" ]] && continue
|
|
459
|
-
[[ ${#term} -lt 3 ]] && continue
|
|
460
|
-
if [[ "$stop_words" == *" ${term} "* ]]; then
|
|
461
|
-
continue
|
|
462
|
-
fi
|
|
463
|
-
filtered_cand_terms="${filtered_cand_terms} ${term}"
|
|
464
|
-
done <<< "$cand_terms"
|
|
465
|
-
|
|
466
|
-
# ── Step 6: Find shared meaningful terms ───────────────────────────────
|
|
467
|
-
local shared_terms=""
|
|
468
|
-
local shared_count=0
|
|
469
|
-
for src_t in $filtered_src_terms; do
|
|
470
|
-
for cand_t in $filtered_cand_terms; do
|
|
471
|
-
if [[ "$src_t" == "$cand_t" ]]; then
|
|
472
|
-
if [[ -z "$shared_terms" ]]; then
|
|
473
|
-
shared_terms="\"${src_t}\""
|
|
474
|
-
else
|
|
475
|
-
shared_terms="${shared_terms}, \"${src_t}\""
|
|
476
|
-
fi
|
|
477
|
-
shared_count=$((shared_count + 1))
|
|
478
|
-
break
|
|
479
|
-
fi
|
|
480
|
-
done
|
|
481
|
-
done
|
|
482
|
-
|
|
483
|
-
# ── Step 7: Report if >= 2 shared terms ────────────────────────────────
|
|
484
|
-
if [[ $shared_count -ge 2 ]]; then
|
|
485
|
-
overlap_count=$((overlap_count + 1))
|
|
486
|
-
overlap_report="${overlap_report} ${cand_id} (${cand_status}, ${cand_priority}): \"${cand_title}\""$'\n'
|
|
487
|
-
overlap_report="${overlap_report} Overlap: keyword match — ${shared_terms}"$'\n'
|
|
488
|
-
overlap_report="${overlap_report} Confidence: LOW (keyword only, no contract data)"$'\n'
|
|
489
|
-
overlap_report="${overlap_report}"$'\n'
|
|
490
|
-
overlap_report="${overlap_report} Options:"$'\n'
|
|
491
|
-
overlap_report="${overlap_report} (a) Add dependency: bd dep add ${issue_id} ${cand_id}"$'\n'
|
|
492
|
-
overlap_report="${overlap_report} (b) Proceed — no real conflict"$'\n'
|
|
493
|
-
overlap_report="${overlap_report} (c) Investigate: bd show ${cand_id}"$'\n'
|
|
494
|
-
overlap_report="${overlap_report}"$'\n'
|
|
495
|
-
fi
|
|
496
|
-
|
|
497
|
-
done <<< "$list_output"
|
|
498
|
-
|
|
499
|
-
# ── Step 8: Output final report ──────────────────────────────────────
|
|
500
|
-
if [[ $overlap_count -gt 0 ]]; then
|
|
501
|
-
printf '%s\n' "⚠️ Potential overlap with ${overlap_count} issue(s):"
|
|
502
|
-
echo ""
|
|
503
|
-
printf '%s' "$overlap_report"
|
|
504
|
-
else
|
|
505
|
-
printf '%s\n' "✅ No conflicts detected"
|
|
336
|
+
# ---- Step 4: Parse each active issue (excluding source) ------------------
|
|
337
|
+
# Format: - forge-xxx [P2] [feature] - Title of the issue
|
|
338
|
+
# - forge-yyy [P1] [task] - Another issue title
|
|
339
|
+
if ! command -v "$NODE_CMD" >/dev/null 2>&1; then
|
|
340
|
+
echo "Error: ${NODE_CMD} is required but not found." >&2
|
|
341
|
+
exit 1
|
|
506
342
|
fi
|
|
507
343
|
|
|
344
|
+
ISSUE_ID="$issue_id" \
|
|
345
|
+
SOURCE_TITLE="$src_title" \
|
|
346
|
+
LIST_OUTPUT="$list_output" \
|
|
347
|
+
"$NODE_CMD" "$SCRIPT_DIR/dep-guard-keyword-ripple.js"
|
|
508
348
|
return 0
|
|
349
|
+
|
|
509
350
|
}
|
|
510
351
|
|
|
511
352
|
cmd_check_ripple() {
|
|
@@ -529,7 +370,7 @@ cmd_check_ripple() {
|
|
|
529
370
|
fi
|
|
530
371
|
|
|
531
372
|
if [[ -z "$src_title" ]]; then
|
|
532
|
-
echo "
|
|
373
|
+
echo "Warning: could not extract title for ${issue_id} - ripple check skipped" >&2
|
|
533
374
|
return 0
|
|
534
375
|
fi
|
|
535
376
|
|
|
@@ -544,7 +385,7 @@ cmd_check_ripple() {
|
|
|
544
385
|
local task_file=""
|
|
545
386
|
task_file="$(extract_task_file_from_design "$design_text" || true)"
|
|
546
387
|
if [[ -z "$task_file" || ! -f "$task_file" ]]; then
|
|
547
|
-
echo "
|
|
388
|
+
echo "Warning: structured analyzer unavailable, falling back to keyword-only ripple check." >&2
|
|
548
389
|
cmd_check_ripple_keyword_v1 "$issue_id"
|
|
549
390
|
return 0
|
|
550
391
|
fi
|
|
@@ -555,51 +396,23 @@ cmd_check_ripple() {
|
|
|
555
396
|
in_progress_json="$(${BD_CMD:-bd} list --status=in_progress --json 2>/dev/null)" || true
|
|
556
397
|
|
|
557
398
|
if [[ -z "$open_json" && -z "$in_progress_json" ]]; then
|
|
558
|
-
echo "
|
|
399
|
+
echo "Warning: could not fetch active issue list - ripple check skipped" >&2
|
|
559
400
|
return 0
|
|
560
401
|
fi
|
|
561
402
|
|
|
562
|
-
local tmp_dir
|
|
563
|
-
tmp_dir="$(mktemp -d)"
|
|
564
|
-
# On Windows/Git Bash, normalize to a mixed-mode path so both bash file I/O
|
|
565
|
-
# and Node.js resolve to the same physical location (cygpath -m → C:/...).
|
|
566
|
-
# Preserve the original POSIX path if conversion fails or returns empty.
|
|
567
|
-
if command -v cygpath &>/dev/null; then
|
|
568
|
-
local mixed_tmp_dir
|
|
569
|
-
mixed_tmp_dir="$(cygpath -m "$tmp_dir" 2>/dev/null)" || true
|
|
570
|
-
if [[ -n "$mixed_tmp_dir" && -d "$mixed_tmp_dir" ]]; then
|
|
571
|
-
tmp_dir="$mixed_tmp_dir"
|
|
572
|
-
fi
|
|
573
|
-
fi
|
|
574
|
-
trap 'rm -rf "$tmp_dir"' RETURN
|
|
575
|
-
|
|
576
|
-
printf '%s' "$src_json" > "${tmp_dir}/current.json"
|
|
577
|
-
printf '%s' "${open_json:-[]}" > "${tmp_dir}/open.json"
|
|
578
|
-
printf '%s' "${in_progress_json:-[]}" > "${tmp_dir}/in-progress.json"
|
|
579
403
|
|
|
580
404
|
local repository_root="${DEP_GUARD_REPOSITORY_ROOT:-$PWD}"
|
|
581
405
|
if run_phase3_analyzer \
|
|
582
|
-
"$
|
|
583
|
-
"${
|
|
584
|
-
"${
|
|
406
|
+
"$src_json" \
|
|
407
|
+
"${open_json:-[]}" \
|
|
408
|
+
"${in_progress_json:-[]}" \
|
|
585
409
|
"$task_file" \
|
|
586
410
|
"$repository_root" \
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
trap - RETURN
|
|
590
|
-
rm -rf "$tmp_dir"
|
|
591
|
-
return 0
|
|
592
|
-
fi
|
|
593
|
-
echo "⚠️ Phase 3 review rendering failed — falling back to keyword-only ripple check." >&2
|
|
594
|
-
else
|
|
595
|
-
echo "⚠️ Structured analyzer unavailable, falling back to keyword-only ripple check." >&2
|
|
596
|
-
if [[ -s "${tmp_dir}/analysis.err" ]]; then
|
|
597
|
-
cat "${tmp_dir}/analysis.err" >&2
|
|
598
|
-
fi
|
|
411
|
+
| render_phase3_review; then
|
|
412
|
+
return 0
|
|
599
413
|
fi
|
|
600
414
|
|
|
601
|
-
|
|
602
|
-
rm -rf "$tmp_dir"
|
|
415
|
+
echo "Warning: structured analyzer unavailable, falling back to keyword-only ripple check." >&2
|
|
603
416
|
cmd_check_ripple_keyword_v1 "$issue_id"
|
|
604
417
|
}
|
|
605
418
|
|
|
@@ -624,7 +437,7 @@ cmd_apply_decision() {
|
|
|
624
437
|
die "Failed to add dependency ${dependent_issue} -> ${depends_on_issue}"
|
|
625
438
|
}
|
|
626
439
|
|
|
627
|
-
# Check for cycles
|
|
440
|
+
# Check for cycles -- use exit code as primary signal
|
|
628
441
|
if ! ${BD_CMD:-bd} dep cycles &>/dev/null; then
|
|
629
442
|
rollback_dependency "$dependent_issue" "$depends_on_issue"
|
|
630
443
|
die "Cycle detected for ${dependent_issue} -> ${depends_on_issue}"
|
|
@@ -722,7 +535,7 @@ cmd_extract_contracts() {
|
|
|
722
535
|
local all_contracts=""
|
|
723
536
|
|
|
724
537
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
725
|
-
# New task block
|
|
538
|
+
# New task block -- flush previous
|
|
726
539
|
if [[ "$line" =~ ^##\ Task ]]; then
|
|
727
540
|
local _emitted
|
|
728
541
|
_emitted="$(emit_contracts "$current_files" "$current_what")"
|
|
@@ -777,7 +590,7 @@ cmd_extract_contracts() {
|
|
|
777
590
|
printf '%s\n' "$contracts"
|
|
778
591
|
}
|
|
779
592
|
|
|
780
|
-
#
|
|
593
|
+
# ---- Main dispatcher --------------------------------------------------------
|
|
781
594
|
|
|
782
595
|
if [[ $# -lt 1 ]]; then
|
|
783
596
|
usage
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# sync_issue_status <beads-id> <status> — Update status label on GitHub
|
|
8
8
|
# sync_issue_close <beads-id> — Close GitHub issue
|
|
9
9
|
# sync_issue_deps <beads-id-a> <beads-id-b> — Add "Blocked by" comment
|
|
10
|
-
# _get_github_issue_number <beads-id> — Extract
|
|
10
|
+
# _get_github_issue_number <beads-id> — Extract canonical GitHub issue from bd show
|
|
11
11
|
#
|
|
12
12
|
# Env overrides (for testing):
|
|
13
13
|
# GH_CMD — Path to gh binary (default: gh)
|
|
@@ -71,9 +71,132 @@ _safe_sanitize() {
|
|
|
71
71
|
printf '%s' "$input"
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
_extract_issue_number_from_url() {
|
|
75
|
+
local input="${1:-}"
|
|
76
|
+
local suffix="${input##*/issues/}"
|
|
77
|
+
|
|
78
|
+
if [[ "$suffix" == "$input" ]]; then
|
|
79
|
+
return 1
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
suffix="${suffix%%[^0-9]*}"
|
|
83
|
+
if [[ -z "$suffix" ]]; then
|
|
84
|
+
return 1
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
printf '%s' "$suffix"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_extract_issue_title_from_show() {
|
|
91
|
+
local input="${1:-}"
|
|
92
|
+
|
|
93
|
+
printf '%s' "$input" | node -e '
|
|
94
|
+
const fs = require("node:fs");
|
|
95
|
+
const input = fs.readFileSync(0, "utf8");
|
|
96
|
+
const lines = input.split(/\r?\n/);
|
|
97
|
+
const titledLine = lines.find((line) => /^Title:\s*/.test(line));
|
|
98
|
+
|
|
99
|
+
if (titledLine) {
|
|
100
|
+
process.stdout.write(titledLine.replace(/^Title:\s*/, "").trim());
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const summaryLine = lines.find((line) => line.trim().length > 0);
|
|
105
|
+
if (!summaryLine) {
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const separators = [" - ", " \u00B7 ", " \u2022 "];
|
|
110
|
+
for (const separator of separators) {
|
|
111
|
+
const separatorIndex = summaryLine.indexOf(separator);
|
|
112
|
+
if (separatorIndex === -1) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const candidate = summaryLine
|
|
117
|
+
.slice(separatorIndex + separator.length)
|
|
118
|
+
.replace(/\s+\[[^\]]*\]\s*$/u, "")
|
|
119
|
+
.trim();
|
|
120
|
+
|
|
121
|
+
if (candidate.length > 0) {
|
|
122
|
+
process.stdout.write(candidate);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
'
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_sync_mapping_file() {
|
|
130
|
+
local root="${TEAM_MAP_ROOT:-.}"
|
|
131
|
+
printf '%s' "${SYNC_MAPPING_FILE:-$root/.github/beads-mapping.json}"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_persist_issue_mapping() {
|
|
135
|
+
local beads_id="${1:-}"
|
|
136
|
+
local issue_num="${2:-}"
|
|
137
|
+
local mapping_file
|
|
138
|
+
mapping_file="$(_sync_mapping_file)"
|
|
139
|
+
|
|
140
|
+
node - "$mapping_file" "$issue_num" "$beads_id" <<'NODE'
|
|
141
|
+
const fs = require('node:fs');
|
|
142
|
+
const path = require('node:path');
|
|
143
|
+
|
|
144
|
+
const [mappingPath, issueNum, beadsId] = process.argv.slice(2);
|
|
145
|
+
let data = {};
|
|
146
|
+
|
|
147
|
+
if (fs.existsSync(mappingPath)) {
|
|
148
|
+
const raw = fs.readFileSync(mappingPath, 'utf8').trim();
|
|
149
|
+
if (raw) {
|
|
150
|
+
data = JSON.parse(raw);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fs.mkdirSync(path.dirname(mappingPath), { recursive: true });
|
|
155
|
+
data[String(issueNum)] = beadsId;
|
|
156
|
+
fs.writeFileSync(mappingPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
157
|
+
NODE
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_get_issue_number_from_mapping() {
|
|
161
|
+
local beads_id="${1:-}"
|
|
162
|
+
local mapping_file
|
|
163
|
+
mapping_file="$(_sync_mapping_file)"
|
|
164
|
+
|
|
165
|
+
node - "$mapping_file" "$beads_id" <<'NODE'
|
|
166
|
+
const fs = require('node:fs');
|
|
167
|
+
|
|
168
|
+
const [mappingPath, beadsId] = process.argv.slice(2);
|
|
169
|
+
if (!fs.existsSync(mappingPath)) {
|
|
170
|
+
process.exit(0);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const raw = fs.readFileSync(mappingPath, 'utf8').trim();
|
|
174
|
+
if (!raw) {
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const data = JSON.parse(raw);
|
|
179
|
+
let newestIssueNum = null;
|
|
180
|
+
for (const [issueNum, mappedBeadsId] of Object.entries(data)) {
|
|
181
|
+
if (mappedBeadsId !== beadsId) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const parsedIssueNum = Number.parseInt(issueNum, 10);
|
|
185
|
+
if (!Number.isFinite(parsedIssueNum)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (newestIssueNum == null || parsedIssueNum > newestIssueNum) {
|
|
189
|
+
newestIssueNum = parsedIssueNum;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (newestIssueNum != null) {
|
|
193
|
+
process.stdout.write(String(newestIssueNum));
|
|
194
|
+
}
|
|
195
|
+
NODE
|
|
196
|
+
}
|
|
197
|
+
|
|
74
198
|
# ── _get_github_issue_number ─────────────────────────────────────────────
|
|
75
|
-
# Extracts
|
|
76
|
-
# Looks for `github_issue:N` pattern.
|
|
199
|
+
# Extracts the canonical GitHub issue number from `bd show <id> --json`.
|
|
77
200
|
# Returns the number or empty string (exit 1 if not found).
|
|
78
201
|
_get_github_issue_number() {
|
|
79
202
|
local beads_id="${1:-}"
|
|
@@ -84,16 +207,37 @@ _get_github_issue_number() {
|
|
|
84
207
|
|
|
85
208
|
local bd_cmd="${BD_CMD:-bd}"
|
|
86
209
|
local show_output
|
|
87
|
-
show_output="$("$bd_cmd" show "$beads_id" 2>/dev/null)" || {
|
|
210
|
+
show_output="$("$bd_cmd" show "$beads_id" --json 2>/dev/null)" || {
|
|
88
211
|
_sync_error "Failed to get beads info for $beads_id"
|
|
89
212
|
return 1
|
|
90
213
|
}
|
|
91
214
|
|
|
92
215
|
local issue_num
|
|
93
|
-
issue_num="$(printf '%s' "$show_output" |
|
|
216
|
+
issue_num="$(printf '%s' "$show_output" | node -e '
|
|
217
|
+
const fs = require("node:fs");
|
|
218
|
+
const input = fs.readFileSync(0, "utf8");
|
|
219
|
+
try {
|
|
220
|
+
const data = JSON.parse(input);
|
|
221
|
+
const issue = Array.isArray(data) ? data[0] : data;
|
|
222
|
+
const value = issue?.github?.number
|
|
223
|
+
?? issue?.githubNumber
|
|
224
|
+
?? issue?.issueNumber
|
|
225
|
+
?? issue?.github_issue
|
|
226
|
+
?? null;
|
|
227
|
+
if (value != null && value !== "") {
|
|
228
|
+
process.stdout.write(String(value));
|
|
229
|
+
}
|
|
230
|
+
} catch (_err) {
|
|
231
|
+
process.exitCode = 0;
|
|
232
|
+
}
|
|
233
|
+
')"
|
|
94
234
|
|
|
95
235
|
if [[ -z "$issue_num" ]]; then
|
|
96
|
-
|
|
236
|
+
issue_num="$(_get_issue_number_from_mapping "$beads_id" 2>/dev/null || true)"
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
if [[ -z "$issue_num" ]]; then
|
|
240
|
+
_sync_error "No canonical GitHub number found for $beads_id"
|
|
97
241
|
return 1
|
|
98
242
|
fi
|
|
99
243
|
|
|
@@ -111,13 +255,7 @@ _get_issue_title() {
|
|
|
111
255
|
show_output="$("$bd_cmd" show "$beads_id" 2>/dev/null)" || return 1
|
|
112
256
|
|
|
113
257
|
local title
|
|
114
|
-
|
|
115
|
-
title="$(printf '%s' "$show_output" | grep -oP '^Title:\s*\K.*' | head -1)"
|
|
116
|
-
|
|
117
|
-
if [[ -z "$title" ]]; then
|
|
118
|
-
# Fallback: extract from summary line "○ <id> · <title> [...]"
|
|
119
|
-
title="$(printf '%s' "$show_output" | grep -oP '·\s*\K[^\[]+' | head -1 | sed 's/[[:space:]]*$//')"
|
|
120
|
-
fi
|
|
258
|
+
title="$(_extract_issue_title_from_show "$show_output")"
|
|
121
259
|
|
|
122
260
|
if [[ -z "$title" ]]; then
|
|
123
261
|
title="$beads_id"
|
|
@@ -130,7 +268,8 @@ _get_issue_title() {
|
|
|
130
268
|
|
|
131
269
|
# sync_issue_create <beads-id>
|
|
132
270
|
# Creates GitHub issue from Beads issue data.
|
|
133
|
-
#
|
|
271
|
+
# Persists both the canonical mapping file and the legacy github_issue state
|
|
272
|
+
# until downstream hook and verify flows stop reading bd show output.
|
|
134
273
|
# Returns 0 on success, 1 on failure.
|
|
135
274
|
sync_issue_create() {
|
|
136
275
|
local beads_id="${1:-}"
|
|
@@ -162,28 +301,21 @@ sync_issue_create() {
|
|
|
162
301
|
|
|
163
302
|
# Extract issue number from URL (e.g., https://github.com/.../issues/42)
|
|
164
303
|
local issue_num
|
|
165
|
-
issue_num="$(
|
|
304
|
+
issue_num="$(_extract_issue_number_from_url "$gh_output")"
|
|
166
305
|
|
|
167
306
|
if [[ -z "$issue_num" ]]; then
|
|
168
307
|
_sync_error "Could not extract issue number from: $gh_output"
|
|
169
308
|
return 1
|
|
170
309
|
fi
|
|
171
310
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
_sync_error "Failed to store github_issue=$issue_num for $beads_id"
|
|
311
|
+
if ! _persist_issue_mapping "$beads_id" "$issue_num"; then
|
|
312
|
+
_sync_error "Failed to persist GitHub link for $beads_id"
|
|
175
313
|
return 1
|
|
176
|
-
|
|
314
|
+
fi
|
|
177
315
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
local updated
|
|
182
|
-
updated="$(jq --arg num "$issue_num" --arg id "$beads_id" '.[$num] = $id' "$mapping_file")"
|
|
183
|
-
printf '%s\n' "$updated" > "$mapping_file"
|
|
184
|
-
else
|
|
185
|
-
mkdir -p .github
|
|
186
|
-
printf '%s\n' "{\"$issue_num\":\"$beads_id\"}" > "$mapping_file"
|
|
316
|
+
if ! "$bd_cmd" set-state "$beads_id" "github_issue=$issue_num" >/dev/null 2>&1; then
|
|
317
|
+
_sync_error "Failed to store github_issue=$issue_num for $beads_id"
|
|
318
|
+
return 1
|
|
187
319
|
fi
|
|
188
320
|
|
|
189
321
|
return 0
|