@ikunin/sprintpilot 2.2.15 → 2.2.17
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.
|
@@ -1346,6 +1346,29 @@ function cmdStart(opts) {
|
|
|
1346
1346
|
);
|
|
1347
1347
|
}
|
|
1348
1348
|
|
|
1349
|
+
// parallel_stories: surface honestly that the documented flag is not
|
|
1350
|
+
// yet wired through the BMad state machine. The supporting pieces
|
|
1351
|
+
// (planBatch, dispatch-layer.js, agent-adapter.js, merge-shards.js)
|
|
1352
|
+
// exist as building blocks but nextAction never emits a parallel_batch
|
|
1353
|
+
// action — every story still flows through the 7-phase cycle one at a
|
|
1354
|
+
// time. A user who sets `ma.parallel_stories: true` and doesn't see
|
|
1355
|
+
// this notice would assume parallelism is happening when it isn't.
|
|
1356
|
+
if (profile.parallel_stories) {
|
|
1357
|
+
ledger.append(
|
|
1358
|
+
{
|
|
1359
|
+
kind: 'state_transition',
|
|
1360
|
+
detail: {
|
|
1361
|
+
parallel_stories_experimental_warning:
|
|
1362
|
+
'ma.parallel_stories=true is honored by the planBatch / dispatch-layer.js building blocks but the BMad state machine still emits one story at a time. Full intra-epic parallel dispatch is tracked for v2.3.0+. Stories continue sequentially in this session.',
|
|
1363
|
+
},
|
|
1364
|
+
},
|
|
1365
|
+
{ projectRoot },
|
|
1366
|
+
);
|
|
1367
|
+
process.stderr.write(
|
|
1368
|
+
'[autopilot] WARN ma.parallel_stories=true but the state machine is not yet wired for parallel dispatch (planned for v2.3.0). Stories will run sequentially this session.\n',
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1349
1372
|
// Worktree health check — once per session, after lock acquire so we
|
|
1350
1373
|
// don't compete with another active session for the same .worktrees
|
|
1351
1374
|
// directory.
|
|
@@ -95,6 +95,113 @@ function readSprintStatus(fs, projectRoot) {
|
|
|
95
95
|
return readFileSafe(fs, p);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// Auto-detect test files added/modified since the story branch started.
|
|
99
|
+
// Used by verifyDevRed when the LLM forgets to echo `test_files` in
|
|
100
|
+
// signal.output — a recurring user pain point: the LLM did the work but
|
|
101
|
+
// signaled `success` with empty output, causing the verifier to halt with
|
|
102
|
+
// "no test_files reported", retry budget exhausted, session dies.
|
|
103
|
+
//
|
|
104
|
+
// Lists git diff vs base-branch + untracked files, filters to test-shaped
|
|
105
|
+
// paths via TEST_FILE_PATTERNS. Returns [] on any error so the caller
|
|
106
|
+
// falls through to the strict rejection rather than silently accepting.
|
|
107
|
+
const TEST_FILE_PATTERNS = [
|
|
108
|
+
/(^|\/)[^/]+\.test\.(?:[mc]?jsx?|[mc]?tsx?)$/i,
|
|
109
|
+
/(^|\/)[^/]+\.spec\.(?:[mc]?jsx?|[mc]?tsx?)$/i,
|
|
110
|
+
/(^|\/)test_[^/]+\.py$/i,
|
|
111
|
+
/(^|\/)[^/]+_test\.py$/i,
|
|
112
|
+
/(^|\/)[^/]+_test\.go$/i,
|
|
113
|
+
/(^|\/)tests?\/[^/]+\.rs$/i,
|
|
114
|
+
/(^|\/)[^/]+Tests?\.swift$/i,
|
|
115
|
+
/(^|\/)[^/]+Test\.(?:kt|java)$/i,
|
|
116
|
+
/(^|\/)[^/]+_test\.rb$/i,
|
|
117
|
+
/(^|\/)[^/]+_spec\.rb$/i,
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
function looksLikeTestFile(p) {
|
|
121
|
+
return TEST_FILE_PATTERNS.some((re) => re.test(p));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function autoDetectTestFiles(ctx, baseBranch) {
|
|
125
|
+
if (!ctx || !ctx.projectRoot) return [];
|
|
126
|
+
const projectRoot = ctx.projectRoot;
|
|
127
|
+
const NUL = String.fromCharCode(0);
|
|
128
|
+
const base = baseBranch || 'main';
|
|
129
|
+
const cp = require('node:child_process');
|
|
130
|
+
function runGit(extra) {
|
|
131
|
+
try {
|
|
132
|
+
return cp.execFileSync(
|
|
133
|
+
'git',
|
|
134
|
+
['-C', projectRoot, ...extra],
|
|
135
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 5_000 },
|
|
136
|
+
);
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Prefer base...HEAD; fall back to a 5-commit window if origin/<base>
|
|
142
|
+
// isn't fetched. -z separates entries with NUL so filenames with spaces
|
|
143
|
+
// survive intact.
|
|
144
|
+
let raw = runGit(['diff', '--name-only', '--no-renames', '-z', base + '...HEAD']);
|
|
145
|
+
if (raw === null) raw = runGit(['diff', '--name-only', '--no-renames', '-z', 'HEAD~5..HEAD']);
|
|
146
|
+
const untracked = runGit(['ls-files', '--others', '--exclude-standard', '-z']);
|
|
147
|
+
const parts = [];
|
|
148
|
+
for (const buf of [raw, untracked]) {
|
|
149
|
+
if (!buf) continue;
|
|
150
|
+
for (const p of buf.split(NUL)) {
|
|
151
|
+
const t = p.trim();
|
|
152
|
+
if (t) parts.push(t);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const seen = new Set();
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const p of parts) {
|
|
158
|
+
if (!looksLikeTestFile(p)) continue;
|
|
159
|
+
const abs = nodePath.isAbsolute(p) ? p : nodePath.join(projectRoot, p);
|
|
160
|
+
if (seen.has(abs)) continue;
|
|
161
|
+
seen.add(abs);
|
|
162
|
+
out.push(abs);
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Probe the underlying git state to confirm a STORY_DONE signal whose
|
|
168
|
+
// `git_steps_completed` flag was omitted. Returns true iff:
|
|
169
|
+
// - commit_sha resolves locally (git cat-file -e <sha>)
|
|
170
|
+
// - origin/<branch> resolves to the same sha (git ls-remote)
|
|
171
|
+
//
|
|
172
|
+
// Both checks must pass; either alone is insufficient (local commit
|
|
173
|
+
// without push, or remote pointing at a different commit, means the
|
|
174
|
+
// story isn't really done).
|
|
175
|
+
//
|
|
176
|
+
// Returns false on any error / missing tooling so the caller falls
|
|
177
|
+
// through to the strict rejection.
|
|
178
|
+
function verifyGitStepsViaProbe(out, ctx) {
|
|
179
|
+
if (!ctx || !ctx.projectRoot) return false;
|
|
180
|
+
if (!out || !out.commit_sha || !out.branch) return false;
|
|
181
|
+
const projectRoot = ctx.projectRoot;
|
|
182
|
+
const cp = require('node:child_process');
|
|
183
|
+
function runGit(args) {
|
|
184
|
+
try {
|
|
185
|
+
return cp.execFileSync(
|
|
186
|
+
'git',
|
|
187
|
+
['-C', projectRoot, ...args],
|
|
188
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 5_000 },
|
|
189
|
+
);
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Local commit exists?
|
|
195
|
+
const local = runGit(['cat-file', '-e', out.commit_sha]);
|
|
196
|
+
if (local === null) return false;
|
|
197
|
+
// Remote tracks the same sha? -h restricts to refs/heads/<branch>.
|
|
198
|
+
const remote = runGit(['ls-remote', '--heads', 'origin', out.branch]);
|
|
199
|
+
if (!remote) return false;
|
|
200
|
+
// ls-remote output is "<sha>\trefs/heads/<branch>\n"
|
|
201
|
+
const remoteSha = remote.split(/\s+/)[0];
|
|
202
|
+
return typeof remoteSha === 'string' && remoteSha === out.commit_sha;
|
|
203
|
+
}
|
|
204
|
+
|
|
98
205
|
// Per-phase verifiers. Each receives (state, signalOutput, context) and
|
|
99
206
|
// returns { ok, issues[] }. `context` carries injected dependencies.
|
|
100
207
|
const VERIFIERS = {
|
|
@@ -199,8 +306,20 @@ function verifyCheckReadiness(state, _out, ctx) {
|
|
|
199
306
|
|
|
200
307
|
function verifyDevRed(state, out, ctx) {
|
|
201
308
|
const issues = [];
|
|
202
|
-
// 1. Test files claimed in output exist.
|
|
203
|
-
|
|
309
|
+
// 1. Test files claimed in output exist. If the LLM omitted test_files
|
|
310
|
+
// (a recurring failure mode), auto-detect from git diff / untracked
|
|
311
|
+
// files in the project tree so the verifier doesn't halt on a
|
|
312
|
+
// cosmetic signaling gap. Detected paths flow through the same
|
|
313
|
+
// fileExists check as LLM-supplied paths.
|
|
314
|
+
let testFiles = isNonEmptyArray(out.test_files) ? out.test_files : [];
|
|
315
|
+
let autodetected = false;
|
|
316
|
+
if (testFiles.length === 0 && ctx && ctx.projectRoot) {
|
|
317
|
+
const detected = autoDetectTestFiles(ctx, state && state.base_branch);
|
|
318
|
+
if (detected.length > 0) {
|
|
319
|
+
testFiles = detected;
|
|
320
|
+
autodetected = true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
204
323
|
if (testFiles.length === 0) issues.push('no test_files reported');
|
|
205
324
|
for (const f of testFiles) {
|
|
206
325
|
if (!fileExists(ctx.fs, f)) issues.push(`test file missing: ${f}`);
|
|
@@ -220,7 +339,9 @@ function verifyDevRed(state, out, ctx) {
|
|
|
220
339
|
`source files changed in RED phase: ${out.source_files_changed.join(',')} — expected tests only`,
|
|
221
340
|
);
|
|
222
341
|
}
|
|
223
|
-
|
|
342
|
+
const result = { ok: issues.length === 0, issues };
|
|
343
|
+
if (autodetected) result.autodetected_test_files = testFiles;
|
|
344
|
+
return result;
|
|
224
345
|
}
|
|
225
346
|
|
|
226
347
|
function verifyDevGreen(state, out, ctx) {
|
|
@@ -247,14 +368,44 @@ function verifyDevGreen(state, out, ctx) {
|
|
|
247
368
|
|
|
248
369
|
function verifyCodeReview(state, out, ctx) {
|
|
249
370
|
const issues = [];
|
|
250
|
-
|
|
371
|
+
// bmad-code-review (.claude/skills/bmad-code-review/steps/step-04-present.md)
|
|
372
|
+
// writes findings as a "### Review Findings" subsection INSIDE the story
|
|
373
|
+
// file's Tasks/Subtasks block — NOT a separate _bmad-output/reviews/<key>.md.
|
|
374
|
+
// The pre-2.2.17 check for that file rejected every real run because the
|
|
375
|
+
// skill never creates one (recurring user pain: "review artifact missing:
|
|
376
|
+
// <path>" halts).
|
|
377
|
+
//
|
|
378
|
+
// Accept any of:
|
|
379
|
+
// - story file contains a `### Review Findings` section
|
|
380
|
+
// - legacy `_bmad-output/reviews/<key>.md` exists (older repos)
|
|
381
|
+
// - legacy `_bmad-output/implementation-artifacts/code-review-<key>.md` exists
|
|
382
|
+
// Reject only when NONE of the above exist AND the LLM didn't supply
|
|
383
|
+
// findings[] inline.
|
|
384
|
+
const storyKey = state.story_key || 'unknown';
|
|
385
|
+
const reviewLegacy = nodePath.join(
|
|
251
386
|
ctx.projectRoot,
|
|
252
387
|
'_bmad-output',
|
|
253
388
|
'reviews',
|
|
254
|
-
`${
|
|
389
|
+
`${storyKey}.md`,
|
|
390
|
+
);
|
|
391
|
+
const reviewArtifact = nodePath.join(
|
|
392
|
+
ctx.projectRoot,
|
|
393
|
+
'_bmad-output',
|
|
394
|
+
'implementation-artifacts',
|
|
395
|
+
`code-review-${storyKey}.md`,
|
|
255
396
|
);
|
|
256
|
-
|
|
257
|
-
|
|
397
|
+
const storyFile = state.story_file_path;
|
|
398
|
+
let foundReview = false;
|
|
399
|
+
if (fileExists(ctx.fs, reviewLegacy) || fileExists(ctx.fs, reviewArtifact)) {
|
|
400
|
+
foundReview = true;
|
|
401
|
+
} else if (storyFile) {
|
|
402
|
+
const text = readFileSafe(ctx.fs, storyFile);
|
|
403
|
+
if (text && /^#{2,4}\s+Review Findings\b/m.test(text)) foundReview = true;
|
|
404
|
+
}
|
|
405
|
+
if (!foundReview) {
|
|
406
|
+
issues.push(
|
|
407
|
+
`review artifact missing: expected one of (a) "### Review Findings" section in ${storyFile || '<story file>'}, (b) ${reviewLegacy}, or (c) ${reviewArtifact}`,
|
|
408
|
+
);
|
|
258
409
|
}
|
|
259
410
|
const findings = Array.isArray(out.findings) ? out.findings : null;
|
|
260
411
|
if (findings === null) {
|
|
@@ -320,10 +471,19 @@ function verifyStoryDone(state, out, ctx) {
|
|
|
320
471
|
// `git commit` and report success — leaving the story branch unpushed.
|
|
321
472
|
// Confirmed live in greenfield e2e: signal had commit_sha+branch but
|
|
322
473
|
// origin/<branch> never appeared on remote.
|
|
474
|
+
//
|
|
475
|
+
// Recovery path (recurring user pain): the LLM did the work but forgot
|
|
476
|
+
// to echo `git_steps_completed: true`. Probe the underlying git state —
|
|
477
|
+
// if the commit_sha exists locally AND origin/<branch> resolves to it,
|
|
478
|
+
// accept the signal. The full audit trail is in the ledger via the
|
|
479
|
+
// verify_result entry, so a false-positive auto-accept stays observable.
|
|
323
480
|
if (out.git_steps_completed !== true) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
481
|
+
const autoConfirmed = verifyGitStepsViaProbe(out, ctx);
|
|
482
|
+
if (!autoConfirmed) {
|
|
483
|
+
issues.push(
|
|
484
|
+
'git_steps_completed must be true — set to true ONLY after every step in action.steps (git add, commit, push) exited 0. Skipping git push is the most common cause.',
|
|
485
|
+
);
|
|
486
|
+
}
|
|
327
487
|
}
|
|
328
488
|
// BMad bookkeeping: sprint-status.yaml MUST record this story as `done`.
|
|
329
489
|
// Without this check, the LLM can claim STORY_DONE while sprint-status
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
# Multi-Agent Configuration
|
|
2
|
-
#
|
|
1
|
+
# Multi-Agent Configuration
|
|
2
|
+
#
|
|
3
|
+
# Top-level key MUST be `ma:` — resolve-profile.js merges this under the
|
|
4
|
+
# `ma` namespace, and profile-rules.js reads `ma.parallel_stories` /
|
|
5
|
+
# `ma.max_parallel_stories`. The legacy `multi_agent:` wrapper used in
|
|
6
|
+
# pre-2.2.16 versions was silently ignored (deep-merge produced
|
|
7
|
+
# `resolved.ma.multi_agent.*` instead of `resolved.ma.*`).
|
|
3
8
|
|
|
4
|
-
|
|
9
|
+
ma:
|
|
5
10
|
enabled: true
|
|
6
11
|
max_parallel_review_layers: 3 # Always 3: blind, edge-case, acceptance
|
|
7
12
|
max_parallel_research: 3 # Max concurrent research agents per batch
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikunin/sprintpilot",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.17",
|
|
4
4
|
"description": "Sprintpilot — autopilot and multi-agent addon for BMad Method v6: git workflow, parallel agents, autonomous story execution",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|