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.
Files changed (102) hide show
  1. package/.claude/commands/dev.md +2 -2
  2. package/.claude/commands/plan.md +2 -2
  3. package/.claude/commands/ship.md +2 -2
  4. package/.claude/commands/status.md +4 -4
  5. package/.cline/workflows/dev.md +2 -2
  6. package/.cline/workflows/plan.md +2 -2
  7. package/.cline/workflows/ship.md +2 -2
  8. package/.cline/workflows/status.md +4 -4
  9. package/.codex/skills/dev/SKILL.md +2 -2
  10. package/.codex/skills/plan/SKILL.md +2 -2
  11. package/.codex/skills/ship/SKILL.md +2 -2
  12. package/.codex/skills/status/SKILL.md +4 -4
  13. package/.cursor/commands/dev.md +2 -2
  14. package/.cursor/commands/plan.md +2 -2
  15. package/.cursor/commands/ship.md +2 -2
  16. package/.cursor/commands/status.md +4 -4
  17. package/.github/prompts/dev.prompt.md +2 -2
  18. package/.github/prompts/plan.prompt.md +2 -2
  19. package/.github/prompts/ship.prompt.md +2 -2
  20. package/.github/prompts/status.prompt.md +4 -4
  21. package/.github/workflows/beads-to-github.yml +43 -10
  22. package/.github/workflows/github-to-beads.yml +10 -7
  23. package/.kilocode/workflows/dev.md +2 -2
  24. package/.kilocode/workflows/plan.md +2 -2
  25. package/.kilocode/workflows/ship.md +2 -2
  26. package/.kilocode/workflows/status.md +4 -4
  27. package/.opencode/commands/dev.md +2 -2
  28. package/.opencode/commands/plan.md +2 -2
  29. package/.opencode/commands/ship.md +2 -2
  30. package/.opencode/commands/status.md +4 -4
  31. package/.roo/commands/dev.md +2 -2
  32. package/.roo/commands/plan.md +2 -2
  33. package/.roo/commands/ship.md +2 -2
  34. package/.roo/commands/status.md +4 -4
  35. package/AGENTS.md +1 -0
  36. package/CLAUDE.md +12 -0
  37. package/bin/forge.js +10 -5
  38. package/docs/BEADS_GITHUB_SYNC.md +26 -0
  39. package/docs/TOOLCHAIN.md +130 -148
  40. package/lib/beads-bootstrap.js +225 -0
  41. package/lib/beads-health-check.js +55 -10
  42. package/lib/beads-setup.js +104 -28
  43. package/lib/beads-sync-scaffold.js +11 -6
  44. package/lib/commands/_issue.js +11 -1
  45. package/lib/commands/issues.js +49 -0
  46. package/lib/commands/recommend.js +22 -1
  47. package/lib/commands/setup.js +16 -10
  48. package/lib/commands/status.js +181 -0
  49. package/lib/commands/team.js +11 -1
  50. package/lib/commands/test.js +37 -2
  51. package/lib/commands/validate.js +14 -8
  52. package/lib/commands/worktree.js +27 -54
  53. package/lib/dep-guard/keyword-ripple.js +184 -0
  54. package/lib/detect-worktree.js +9 -10
  55. package/lib/forge-issues.js +326 -0
  56. package/lib/issue-sync/authority.js +100 -0
  57. package/lib/issue-sync/github-pull.js +184 -0
  58. package/lib/issue-sync/import-primitives.js +98 -0
  59. package/lib/issue-sync/legacy-link-bridge.js +436 -0
  60. package/lib/issue-sync/link-store.js +292 -0
  61. package/lib/issue-sync/project-github.js +123 -0
  62. package/lib/issue-sync/reconcile.js +195 -0
  63. package/lib/issue-sync/schema.js +126 -0
  64. package/lib/lefthook-check.js +5 -2
  65. package/lib/project-memory.js +564 -0
  66. package/lib/runtime-health.js +100 -12
  67. package/lib/smart-status/conflicts.js +205 -0
  68. package/lib/smart-status/scoring.js +177 -0
  69. package/lib/status/beads-snapshot.js +102 -0
  70. package/lib/status/presenter.js +65 -0
  71. package/lib/workflow/enforce-stage.js +3 -1
  72. package/lib/workflow/state-manager.js +164 -8
  73. package/package.json +12 -4
  74. package/scripts/beads-context.sh +124 -5
  75. package/scripts/beads-context.test.js +21 -4
  76. package/scripts/beads-migrate-to-dolt.sh +7 -0
  77. package/scripts/beads-upgrade-smoke.sh +263 -0
  78. package/scripts/behavioral-judge.sh +115 -11
  79. package/scripts/benchmark.js +349 -63
  80. package/scripts/dep-guard-analyze.js +52 -17
  81. package/scripts/dep-guard-keyword-ripple.js +29 -0
  82. package/scripts/dep-guard-render-review.js +86 -0
  83. package/scripts/dep-guard.sh +45 -232
  84. package/scripts/forge-team/lib/sync-github.sh +160 -28
  85. package/scripts/forge-team/tests/sync-github.test.sh +195 -58
  86. package/scripts/github-beads-sync/index.mjs +122 -98
  87. package/scripts/github-beads-sync/mapping.mjs +54 -0
  88. package/scripts/github-beads-sync/reverse-sync-cli.mjs +2 -2
  89. package/scripts/github-beads-sync/reverse-sync.mjs +31 -7
  90. package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
  91. package/scripts/preflight.sh +181 -0
  92. package/scripts/smart-status-score.js +31 -0
  93. package/scripts/smart-status-sessions.js +51 -0
  94. package/scripts/smart-status.sh +74 -329
  95. package/scripts/sync-agentic-workflow.js +48 -0
  96. package/scripts/test-ci-shard.js +244 -0
  97. package/scripts/test-dashboard.js +188 -52
  98. package/scripts/test-full-suite.js +186 -0
  99. package/scripts/test-profile.js +278 -0
  100. package/scripts/test.js +219 -28
  101. package/scripts/validate.js +143 -0
  102. package/scripts/validate.sh +18 -1
@@ -10,6 +10,8 @@ const {
10
10
  } = require('./state');
11
11
  const { loadState, WORKFLOW_STATE_FILENAME } = require('./state-manager');
12
12
 
13
+ const STATELESS_ENTRY_STAGES = new Set(['plan', 'dev', 'validate', 'verify']);
14
+
13
15
  function getOverrideInput(flags = {}) {
14
16
  if (Object.hasOwn(flags, 'overrideStage')) {
15
17
  return flags.overrideStage;
@@ -131,7 +133,7 @@ async function enforceStageEntry({ commandName, args = [], flags = {}, projectRo
131
133
  const stateInput = resolveWorkflowStateInput(workflowState, flags, args, projectRoot);
132
134
  const currentState = readWorkflowStateInput(stateInput);
133
135
  if (!currentState) {
134
- if (stageId === 'plan') {
136
+ if (STATELESS_ENTRY_STAGES.has(stageId)) {
135
137
  return { allowed: true, stage: stageId, workflowState: null };
136
138
  }
137
139
 
@@ -9,14 +9,122 @@ const { getWorkflowPath, WORKFLOW_CLASSIFICATIONS, normalizeStageId } = require(
9
9
 
10
10
  const WORKFLOW_STATE_FILENAME = '.forge-state.json';
11
11
 
12
+ function parseRelaxedWorkflowStatePayload(payload = '') {
13
+ const currentStage = String(payload).match(/currentStage:([a-z]+)/)?.[1];
14
+ if (!currentStage) {
15
+ return null;
16
+ }
17
+
18
+ const completedStages = (String(payload).match(/completedStages:\[([^\]]*)\]/)?.[1] || '')
19
+ .split(',')
20
+ .map(stage => stage.trim())
21
+ .filter(Boolean);
22
+ const skippedStages = (String(payload).match(/skippedStages:\[([^\]]*)\]/)?.[1] || '')
23
+ .split(',')
24
+ .map(stage => stage.trim())
25
+ .filter(Boolean);
26
+ const classification = String(payload).match(/classification:([a-z]+)/)?.[1] || 'standard';
27
+ const reason = String(payload).match(/reason:([^,}\]]+)/)?.[1]?.trim() || 'legacy-comment';
28
+
29
+ return readWorkflowState(JSON.stringify({
30
+ currentStage,
31
+ completedStages,
32
+ skippedStages,
33
+ workflowDecisions: {
34
+ classification,
35
+ reason,
36
+ userOverride: false,
37
+ overrides: [],
38
+ },
39
+ parallelTracks: [],
40
+ }));
41
+ }
42
+
12
43
  function extractWorkflowStateFromComments(comments = '') {
13
- const matches = String(comments).match(/^WorkflowState:\s*(\{.*\})$/gm);
44
+ const matches = String(comments)
45
+ .split(/\r?\n/)
46
+ .filter(line => line.startsWith('WorkflowState:'));
14
47
  if (!matches || matches.length === 0) {
15
48
  return null;
16
49
  }
17
50
 
18
- const latest = matches.at(-1).replace(/^WorkflowState:\s*/, '');
19
- return readWorkflowState(latest);
51
+ for (const match of [...matches].reverse()) {
52
+ const payload = match.replace(/^WorkflowState:\s*/, '');
53
+ try {
54
+ return readWorkflowState(payload);
55
+ } catch (_strictError) {
56
+ try {
57
+ const relaxedState = parseRelaxedWorkflowStatePayload(payload);
58
+ if (relaxedState) {
59
+ return relaxedState;
60
+ }
61
+ } catch (_relaxedError) {
62
+ // Continue to any earlier payloads.
63
+ }
64
+ }
65
+ }
66
+
67
+ return null;
68
+ }
69
+
70
+ function extractWorkflowStateFromIssue(issue = {}) {
71
+ if (!issue || typeof issue !== 'object') {
72
+ return null;
73
+ }
74
+
75
+ if (typeof issue.comments === 'string') {
76
+ return extractWorkflowStateFromComments(issue.comments);
77
+ }
78
+
79
+ if (!Array.isArray(issue.comments) || issue.comments.length === 0) {
80
+ return null;
81
+ }
82
+
83
+ const mergedComments = issue.comments
84
+ .map(comment => {
85
+ if (typeof comment === 'string') {
86
+ return comment;
87
+ }
88
+
89
+ return comment && typeof comment === 'object' ? comment.text || '' : '';
90
+ })
91
+ .filter(Boolean)
92
+ .join('\n');
93
+
94
+ if (!mergedComments) {
95
+ return null;
96
+ }
97
+
98
+ return extractWorkflowStateFromComments(mergedComments);
99
+ }
100
+
101
+ function readBeadsIssue(issueId, options = {}) {
102
+ if (!issueId) {
103
+ return null;
104
+ }
105
+
106
+ try {
107
+ const { projectRoot, ...execOptions } = options;
108
+ const raw = secureExecFileSync('bd', ['show', issueId, '--json'], {
109
+ encoding: 'utf8',
110
+ cwd: projectRoot,
111
+ stdio: ['pipe', 'pipe', 'pipe'],
112
+ ...execOptions,
113
+ }).trim();
114
+
115
+ if (!raw) {
116
+ return null;
117
+ }
118
+
119
+ const parsed = JSON.parse(raw);
120
+ if (Array.isArray(parsed)) {
121
+ return parsed[0] || null;
122
+ }
123
+
124
+ return parsed;
125
+ } catch (_error) {
126
+ return null;
127
+ }
20
128
  }
21
129
 
22
130
  function readWorkflowStateFromBeads(issueId, options = {}) {
@@ -24,8 +132,29 @@ function readWorkflowStateFromBeads(issueId, options = {}) {
24
132
  return null;
25
133
  }
26
134
 
27
- const comments = options.comments || secureExecFileSync('bd', ['comments', 'list', issueId], {
135
+ if (options.issue) {
136
+ const state = extractWorkflowStateFromIssue(options.issue);
137
+ if (state) {
138
+ return state;
139
+ }
140
+ }
141
+
142
+ let comments = options.comments;
143
+ if (!comments) {
144
+ try {
145
+ const issue = readBeadsIssue(issueId, { projectRoot: options.projectRoot });
146
+ const state = extractWorkflowStateFromIssue(issue);
147
+ if (state) {
148
+ return state;
149
+ }
150
+ } catch (_error) {
151
+ // Fall through to comments-list fallback.
152
+ }
153
+ }
154
+
155
+ comments = comments || secureExecFileSync('bd', ['comments', 'list', issueId], {
28
156
  encoding: 'utf8',
157
+ cwd: options.projectRoot,
29
158
  stdio: ['pipe', 'pipe', 'pipe'],
30
159
  }).trim();
31
160
 
@@ -36,7 +165,7 @@ function readWorkflowStateFromBeads(issueId, options = {}) {
36
165
  return extractWorkflowStateFromComments(comments);
37
166
  }
38
167
 
39
- function loadStateFromBeads(options) {
168
+ function loadStateFromBeads(options, projectRoot) {
40
169
  if (options.comments) {
41
170
  const state = extractWorkflowStateFromComments(options.comments);
42
171
  if (state) {
@@ -44,8 +173,19 @@ function loadStateFromBeads(options) {
44
173
  }
45
174
  }
46
175
 
176
+ if (options.issue) {
177
+ const state = extractWorkflowStateFromIssue(options.issue);
178
+ if (state) {
179
+ return { state, source: 'beads' };
180
+ }
181
+ }
182
+
47
183
  if (options.issueId) {
48
- const state = readWorkflowStateFromBeads(options.issueId, { comments: options.comments });
184
+ const state = readWorkflowStateFromBeads(options.issueId, {
185
+ comments: options.comments,
186
+ issue: options.issue,
187
+ projectRoot,
188
+ });
49
189
  if (state) {
50
190
  return { state, source: 'beads' };
51
191
  }
@@ -56,10 +196,24 @@ function loadStateFromBeads(options) {
56
196
 
57
197
  function loadState(projectRoot, options = {}) {
58
198
  if (!projectRoot) {
59
- const beadsResult = loadStateFromBeads(options);
199
+ const beadsResult = loadStateFromBeads(options, null);
60
200
  return beadsResult || { state: null, source: null };
61
201
  }
62
202
 
203
+ if (options.preferBeads && (options.comments || options.issue || options.issueId)) {
204
+ try {
205
+ const beadsResult = loadStateFromBeads(options, projectRoot);
206
+ if (beadsResult) {
207
+ return beadsResult;
208
+ }
209
+ } catch (error) {
210
+ if (typeof options.onBeadsError === 'function') {
211
+ options.onBeadsError(error);
212
+ }
213
+ // Fall through to the on-disk state file when Beads cannot be read.
214
+ }
215
+ }
216
+
63
217
  const statePath = path.join(projectRoot, WORKFLOW_STATE_FILENAME);
64
218
  if (fs.existsSync(statePath)) {
65
219
  try {
@@ -70,7 +224,7 @@ function loadState(projectRoot, options = {}) {
70
224
  }
71
225
  }
72
226
 
73
- const beadsResult = loadStateFromBeads(options);
227
+ const beadsResult = loadStateFromBeads(options, projectRoot);
74
228
  if (beadsResult) {
75
229
  return beadsResult;
76
230
  }
@@ -185,8 +339,10 @@ function transitionStage(projectRoot, toStage, options = {}) {
185
339
  module.exports = {
186
340
  WORKFLOW_STATE_FILENAME,
187
341
  extractWorkflowStateFromComments,
342
+ extractWorkflowStateFromIssue,
188
343
  initializeState,
189
344
  loadState,
345
+ readBeadsIssue,
190
346
  readWorkflowStateFromBeads,
191
347
  saveState,
192
348
  transitionStage,
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "forge-workflow",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "7-stage TDD workflow for ALL AI coding agents (Claude, Cursor, Cline, OpenCode, Copilot, Kilo Code, Roo Code, Codex)",
5
5
  "bin": {
6
6
  "forge": "bin/forge.js",
7
+ "forge-workflow": "bin/forge.js",
7
8
  "forge-preflight": "bin/forge-preflight.js"
8
9
  },
9
10
  "workspaces": [
@@ -14,16 +15,23 @@
14
15
  "test:setup": "bash test-env/automation/setup-fixtures.sh",
15
16
  "test:all": "bun run test:setup && bun test",
16
17
  "test:coverage": "c8 --check-coverage bun test",
18
+ "test:ci:shard": "node scripts/test-ci-shard.js",
17
19
  "typecheck": "echo 'No TypeScript in project - skipping type check'",
18
20
  "lint": "eslint . --max-warnings 0",
19
- "check": "bash scripts/validate.sh",
21
+ "check": "node scripts/validate.js",
20
22
  "prepare": "lefthook install || echo 'Note: lefthook not installed. Git hooks disabled. Run: bun add -d lefthook'",
21
23
  "setup": "node ./bin/forge.js setup",
22
24
  "help": "node ./bin/forge.js --help",
23
25
  "validate:yaml": "find . \\( -name '*.yml' -o -name '*.yaml' \\) | grep -v node_modules | while IFS= read -r file; do echo \"Checking $file...\" && bun x js-yaml \"$file\" > /dev/null || exit 1; done",
24
26
  "test:mutation": "npx stryker run",
25
- "test:benchmark": "node scripts/benchmark.js",
26
- "test:dashboard": "node scripts/test-dashboard.js"
27
+ "test:benchmark": "node scripts/benchmark.js --output test-results/benchmark-results.json --profile-dir test-results/benchmark-profiles",
28
+ "test:benchmark:full": "node scripts/benchmark.js --output test-results/benchmark-full-results.json --profile-dir test-results/benchmark-full-profiles --group full-suite --group validate",
29
+ "test:full:parallel": "node scripts/test-full-suite.js",
30
+ "test:dashboard": "node scripts/test-dashboard.js",
31
+ "test:profile": "node scripts/test-profile.js --input-dir test-results --output test-results/test-profile.json --label local",
32
+ "test:randomized": "bun test --randomize",
33
+ "test:rerun": "bun test --rerun-each 2",
34
+ "agentic:sync": "node scripts/sync-agentic-workflow.js"
27
35
  },
28
36
  "peerDependencies": {
29
37
  "lefthook": "^1.0.0"
@@ -41,11 +41,124 @@ die() {
41
41
  exit 1
42
42
  }
43
43
 
44
+ # Resolve a Windows bd.exe installation for bash-based helper flows.
45
+ # This covers WSL/Git Bash cases where PowerShell can run bd.exe but bash PATH
46
+ # does not include the Windows install directory.
47
+ convert_windows_path() {
48
+ local raw="${1%$'\r'}"
49
+
50
+ if [[ -z "$raw" ]]; then
51
+ return 1
52
+ fi
53
+
54
+ if [[ ! "$raw" =~ ^[A-Za-z]:\\ ]]; then
55
+ printf '%s' "$raw"
56
+ return 0
57
+ fi
58
+
59
+ if command -v wslpath >/dev/null 2>&1; then
60
+ wslpath -u "$raw" 2>/dev/null && return 0
61
+ fi
62
+
63
+ if command -v cygpath >/dev/null 2>&1; then
64
+ cygpath -u "$raw" 2>/dev/null && return 0
65
+ fi
66
+
67
+ local drive rest
68
+ drive="$(printf '%s' "$raw" | cut -c1 | tr '[:upper:]' '[:lower:]')"
69
+ rest="${raw:2}"
70
+ rest="${rest//\\//}"
71
+ printf '/mnt/%s%s' "$drive" "$rest"
72
+ }
73
+
74
+ is_runnable_bd_candidate() {
75
+ local candidate="${1:-}"
76
+
77
+ if [[ -z "$candidate" || ! -f "$candidate" ]]; then
78
+ return 1
79
+ fi
80
+
81
+ [[ -x "$candidate" || "$candidate" == *.exe ]]
82
+ }
83
+
84
+ resolve_bd_cmd() {
85
+ local candidate=""
86
+ local converted=""
87
+
88
+ if [[ -n "${BD_CMD:-}" ]]; then
89
+ if [[ "${BD_CMD}" == *"/"* || "${BD_CMD}" == *"\\"* ]]; then
90
+ converted="$(convert_windows_path "$BD_CMD")"
91
+ if is_runnable_bd_candidate "$converted"; then
92
+ printf '%s' "$converted"
93
+ return 0
94
+ fi
95
+
96
+ is_runnable_bd_candidate "$BD_CMD" || return 1
97
+ fi
98
+ printf '%s' "$BD_CMD"
99
+ return 0
100
+ fi
101
+
102
+ if command -v bd >/dev/null 2>&1; then
103
+ printf '%s' "bd"
104
+ return 0
105
+ fi
106
+
107
+ if command -v bd.exe >/dev/null 2>&1; then
108
+ printf '%s' "bd.exe"
109
+ return 0
110
+ fi
111
+
112
+ for candidate in \
113
+ "$HOME/.local/bin/bd" \
114
+ "$HOME/.local/bin/bd.exe" \
115
+ "$HOME/.bun/bin/bd" \
116
+ "$HOME/.bun/bin/bd.exe"
117
+ do
118
+ if is_runnable_bd_candidate "$candidate"; then
119
+ printf '%s' "$candidate"
120
+ return 0
121
+ fi
122
+ done
123
+
124
+ if command -v where.exe >/dev/null 2>&1; then
125
+ while IFS= read -r candidate; do
126
+ candidate="${candidate%$'\r'}"
127
+ [[ -z "$candidate" ]] && continue
128
+
129
+ converted="$(convert_windows_path "$candidate")"
130
+ if is_runnable_bd_candidate "$converted"; then
131
+ printf '%s' "$converted"
132
+ return 0
133
+ fi
134
+
135
+ if is_runnable_bd_candidate "$candidate"; then
136
+ printf '%s' "$candidate"
137
+ return 0
138
+ fi
139
+ done < <(where.exe bd 2>/dev/null || true)
140
+ fi
141
+
142
+ return 1
143
+ }
144
+
145
+ BD=""
146
+
147
+ get_bd_cmd() {
148
+ if [[ -z "$BD" ]]; then
149
+ BD="$(resolve_bd_cmd)" || die "bd is required but not found"
150
+ fi
151
+
152
+ printf '%s' "$BD"
153
+ }
154
+
44
155
  # Run bd update and check for errors in both exit code and output.
45
156
  # bd update exits 0 even for non-existent issues, so we check stdout too.
46
157
  bd_update() {
158
+ local bd_cmd
159
+ bd_cmd="$(get_bd_cmd)"
47
160
  local output
48
- output="$(bd update "$@" 2>&1)"
161
+ output="$("$bd_cmd" update "$@" 2>&1)"
49
162
  local rc=$?
50
163
 
51
164
  if [[ $rc -ne 0 ]]; then
@@ -66,8 +179,10 @@ bd_update() {
66
179
 
67
180
  # Run bd comments add and check for errors similarly.
68
181
  bd_comment() {
182
+ local bd_cmd
183
+ bd_cmd="$(get_bd_cmd)"
69
184
  local output
70
- output="$(bd comments add "$@" 2>&1)"
185
+ output="$("$bd_cmd" comments add "$@" 2>&1)"
71
186
  local rc=$?
72
187
 
73
188
  if [[ $rc -ne 0 ]]; then
@@ -172,10 +287,12 @@ cmd_parse_progress() {
172
287
  fi
173
288
 
174
289
  local issue_id="$1"
290
+ local bd_cmd
291
+ bd_cmd="$(get_bd_cmd)"
175
292
 
176
293
  # Get the issue JSON — detect non-existent issues
177
294
  local json
178
- json="$(bd show "$issue_id" --json 2>&1)" || die "Failed to show issue ${issue_id}"
295
+ json="$("$bd_cmd" show "$issue_id" --json 2>&1)" || die "Failed to show issue ${issue_id}"
179
296
 
180
297
  # bd show may exit 0 but print an error for non-existent issues
181
298
  # Match specific bd error patterns, not the word "error" in data fields
@@ -320,10 +437,12 @@ cmd_validate() {
320
437
 
321
438
  local issue_id="$1"
322
439
  local warnings=0
440
+ local bd_cmd
441
+ bd_cmd="$(get_bd_cmd)"
323
442
 
324
443
  # Get issue JSON
325
444
  local json
326
- json="$(bd show "$issue_id" --json 2>&1)" || {
445
+ json="$("$bd_cmd" show "$issue_id" --json 2>&1)" || {
327
446
  echo "Error: Failed to retrieve issue ${issue_id}" >&2
328
447
  exit 1
329
448
  }
@@ -357,7 +476,7 @@ cmd_validate() {
357
476
 
358
477
  # Get comments to check for stage transitions
359
478
  local comments
360
- comments="$(bd comments list "$issue_id" 2>/dev/null || true)"
479
+ comments="$("$bd_cmd" comments "$issue_id" 2>/dev/null || true)"
361
480
 
362
481
  # Check 2: At least one stage transition exists
363
482
  local has_transition=false
@@ -22,16 +22,24 @@ function isBdAvailable() {
22
22
  }
23
23
  }
24
24
 
25
+ function shouldRunBeadsIntegration() {
26
+ return process.env.RUN_BEADS_INTEGRATION === '1' && isBdAvailable();
27
+ }
28
+
25
29
  /**
26
30
  * Helper: run the beads-context.sh script with given args.
27
31
  * Returns { exitCode, stdout, stderr }.
28
32
  */
29
33
  async function run(...args) {
34
+ return runWithEnv(args);
35
+ }
36
+
37
+ async function runWithEnv(args, envOverrides = {}) {
30
38
  const proc = Bun.spawn([resolveBashCommand(), SCRIPT_PATH, ...args], {
31
39
  cwd: WORKTREE_ROOT,
32
40
  stdout: 'pipe',
33
41
  stderr: 'pipe',
34
- env: { ...process.env },
42
+ env: { ...process.env, ...envOverrides },
35
43
  });
36
44
  const stdout = await new Response(proc.stdout).text();
37
45
  const stderr = await new Response(proc.stderr).text();
@@ -55,7 +63,16 @@ async function bd(...args) {
55
63
  return { exitCode, stdout, stderr };
56
64
  }
57
65
 
58
- describe.skipIf(!isBdAvailable())('scripts/beads-context.sh', () => {
66
+ describe('scripts/beads-context.sh command resolution', () => {
67
+ test('script includes Windows-aware bd resolution fallback', () => {
68
+ const content = fs.readFileSync(SCRIPT_PATH, 'utf8');
69
+ expect(content).toContain('resolve_bd_cmd');
70
+ expect(content).toContain('where.exe');
71
+ expect(content).toContain('$HOME/.local/bin/bd.exe');
72
+ });
73
+ });
74
+
75
+ describe.skipIf(!shouldRunBeadsIntegration())('scripts/beads-context.sh', () => {
59
76
  let testIssueId;
60
77
 
61
78
  // Create a temporary test issue for isolation
@@ -470,7 +487,7 @@ describe('/dev command integration with beads-context.sh', () => {
470
487
  });
471
488
  });
472
489
 
473
- describe('/status command integration with smart-status.sh', () => {
490
+ describe('/status command integration with smart-status', () => {
474
491
  const STATUS_MD_PATH = path.join(
475
492
  __dirname,
476
493
  '..',
@@ -485,7 +502,7 @@ describe('/status command integration with smart-status.sh', () => {
485
502
  });
486
503
 
487
504
  test('status.md should reference smart-status.sh as the primary status command', () => {
488
- expect(statusContent).toContain('smart-status.sh');
505
+ expect(statusContent).toContain('bash scripts/smart-status.sh');
489
506
  });
490
507
 
491
508
  test('status.md should include hint to use bd show for full context', () => {
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ NODE_CMD="${NODE_CMD:-node}"
6
+
7
+ exec "$NODE_CMD" "$SCRIPT_DIR/lib/beads-migrate-to-dolt.mjs" "$@"