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
|
@@ -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
|
|
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)
|
|
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
|
|
19
|
-
|
|
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
|
-
|
|
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, {
|
|
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.
|
|
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": "
|
|
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:
|
|
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"
|
package/scripts/beads-context.sh
CHANGED
|
@@ -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="$(
|
|
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="$(
|
|
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="$(
|
|
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="$(
|
|
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="$(
|
|
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
|
|
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
|
|
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', () => {
|