@ikunin/sprintpilot 2.0.10 → 2.1.0
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/README.md +245 -10
- package/_Sprintpilot/Sprintpilot.md +1 -1
- package/_Sprintpilot/bin/autopilot.js +581 -0
- package/_Sprintpilot/lib/orchestrator/action-ledger.js +148 -0
- package/_Sprintpilot/lib/orchestrator/adapt.js +502 -0
- package/_Sprintpilot/lib/orchestrator/decision-log.js +224 -0
- package/_Sprintpilot/lib/orchestrator/divergence.js +201 -0
- package/_Sprintpilot/lib/orchestrator/git-plan.js +259 -0
- package/_Sprintpilot/lib/orchestrator/impact-classifier.js +108 -0
- package/_Sprintpilot/lib/orchestrator/land.js +155 -0
- package/_Sprintpilot/lib/orchestrator/parallel-batch.js +99 -0
- package/_Sprintpilot/lib/orchestrator/profile-rules.js +167 -0
- package/_Sprintpilot/lib/orchestrator/report.js +95 -0
- package/_Sprintpilot/lib/orchestrator/state-machine.js +402 -0
- package/_Sprintpilot/lib/orchestrator/state-store.js +260 -0
- package/_Sprintpilot/lib/orchestrator/user-command-applier.js +157 -0
- package/_Sprintpilot/lib/orchestrator/user-commands.js +115 -0
- package/_Sprintpilot/lib/orchestrator/verify.js +397 -0
- package/_Sprintpilot/manifest.yaml +1 -1
- package/_Sprintpilot/modules/git/config.yaml +26 -0
- package/_Sprintpilot/scripts/agent-adapter.js +4 -5
- package/_Sprintpilot/scripts/auto-merge-bmad-docs.js +112 -0
- package/_Sprintpilot/scripts/dispatch-layer.js +12 -8
- package/_Sprintpilot/scripts/infer-dependencies.js +78 -21
- package/_Sprintpilot/scripts/inject-tasks-section.js +4 -3
- package/_Sprintpilot/scripts/land-this-pr.js +110 -0
- package/_Sprintpilot/scripts/lint-test-pitfalls.js +133 -0
- package/_Sprintpilot/scripts/list-remaining-stories.js +1 -1
- package/_Sprintpilot/scripts/log-timing.js +12 -3
- package/_Sprintpilot/scripts/merge-shards.js +32 -12
- package/_Sprintpilot/scripts/post-green-gates.js +187 -0
- package/_Sprintpilot/scripts/preflight-merge.js +2 -1
- package/_Sprintpilot/scripts/resolve-dag.js +3 -1
- package/_Sprintpilot/scripts/stack-snapshot.js +128 -0
- package/_Sprintpilot/scripts/state-shard.js +8 -1
- package/_Sprintpilot/scripts/summarize-timings.js +30 -12
- package/_Sprintpilot/scripts/with-retry.js +17 -5
- package/_Sprintpilot/skills/sprint-autopilot-on/SKILL.md +23 -1
- package/_Sprintpilot/skills/sprint-autopilot-on/workflow.orchestrator.md +148 -0
- package/lib/core/update-check.js +11 -1
- package/package.json +1 -1
- package/_Sprintpilot/skills/sprint-autopilot-on/workflow.md +0 -1388
|
@@ -103,7 +103,10 @@ function validatePhase(s) {
|
|
|
103
103
|
|
|
104
104
|
function validateAction(a) {
|
|
105
105
|
if (!VALID_ACTIONS.includes(a)) {
|
|
106
|
-
return {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
error: `invalid action '${a}': must be one of ${VALID_ACTIONS.join(', ')}`,
|
|
109
|
+
};
|
|
107
110
|
}
|
|
108
111
|
return { ok: true, value: a };
|
|
109
112
|
}
|
|
@@ -430,10 +433,16 @@ function main() {
|
|
|
430
433
|
const r = markPhase(projectRoot, story.value, phase.value, meta.value);
|
|
431
434
|
// Emit a brief JSON line so callers can log the duration if useful.
|
|
432
435
|
// Stdout is intentionally separate from the per-story shard.
|
|
433
|
-
process.stdout.write(
|
|
436
|
+
process.stdout.write(
|
|
437
|
+
`${JSON.stringify({ marked: phase.value, prev_phase: r.prev_phase, duration_ms: r.duration_ms })}\n`,
|
|
438
|
+
);
|
|
434
439
|
return;
|
|
435
440
|
}
|
|
436
|
-
appendLine(
|
|
441
|
+
appendLine(
|
|
442
|
+
projectRoot,
|
|
443
|
+
story.value,
|
|
444
|
+
buildEntry(action.value, story.value, phase.value, meta.value),
|
|
445
|
+
);
|
|
437
446
|
} catch (e) {
|
|
438
447
|
log.error(`timing write failed: ${e.message}`);
|
|
439
448
|
process.exit(1);
|
|
@@ -115,7 +115,9 @@ function acquireMergeLock(projectRoot) {
|
|
|
115
115
|
try {
|
|
116
116
|
const st = fs.statSync(file);
|
|
117
117
|
if (Date.now() - st.mtimeMs > STALE_LOCK_AGE_MS) {
|
|
118
|
-
log.warn(
|
|
118
|
+
log.warn(
|
|
119
|
+
`merge-shards: removing stale lock ${file} (older than ${STALE_LOCK_AGE_MS}ms)`,
|
|
120
|
+
);
|
|
119
121
|
fs.unlinkSync(file);
|
|
120
122
|
continue;
|
|
121
123
|
}
|
|
@@ -201,11 +203,7 @@ function shardUnchanged(file, snapshot) {
|
|
|
201
203
|
if (!snapshot) return false;
|
|
202
204
|
try {
|
|
203
205
|
const st = fs.statSync(file);
|
|
204
|
-
return
|
|
205
|
-
st.mtimeMs === snapshot.mtime &&
|
|
206
|
-
st.size === snapshot.size &&
|
|
207
|
-
st.ino === snapshot.ino
|
|
208
|
-
);
|
|
206
|
+
return st.mtimeMs === snapshot.mtime && st.size === snapshot.size && st.ino === snapshot.ino;
|
|
209
207
|
} catch {
|
|
210
208
|
return false;
|
|
211
209
|
}
|
|
@@ -421,7 +419,11 @@ function composeStateYaml(stateMerge) {
|
|
|
421
419
|
if (stateMerge.corrupt.length + stateMerge.invalid.length > 0) {
|
|
422
420
|
doc.shard_problems = [
|
|
423
421
|
...stateMerge.corrupt.map((c) => ({ story: c.story, kind: 'parse-error', detail: c.error })),
|
|
424
|
-
...stateMerge.invalid.map((c) => ({
|
|
422
|
+
...stateMerge.invalid.map((c) => ({
|
|
423
|
+
story: c.story,
|
|
424
|
+
kind: 'invalid-shape',
|
|
425
|
+
detail: c.reason,
|
|
426
|
+
})),
|
|
425
427
|
];
|
|
426
428
|
}
|
|
427
429
|
return `${yamlDump(doc)}\n`;
|
|
@@ -435,8 +437,16 @@ function composeDecisionYaml(decisionMerge) {
|
|
|
435
437
|
};
|
|
436
438
|
if (decisionMerge.corrupt.length + decisionMerge.invalid.length > 0) {
|
|
437
439
|
doc.shard_problems = [
|
|
438
|
-
...decisionMerge.corrupt.map((c) => ({
|
|
439
|
-
|
|
440
|
+
...decisionMerge.corrupt.map((c) => ({
|
|
441
|
+
story: c.story,
|
|
442
|
+
kind: 'parse-error',
|
|
443
|
+
detail: c.error,
|
|
444
|
+
})),
|
|
445
|
+
...decisionMerge.invalid.map((c) => ({
|
|
446
|
+
story: c.story,
|
|
447
|
+
kind: 'invalid-shape',
|
|
448
|
+
detail: c.reason,
|
|
449
|
+
})),
|
|
440
450
|
];
|
|
441
451
|
}
|
|
442
452
|
return `${yamlDump(doc)}\n`;
|
|
@@ -460,7 +470,13 @@ function merge(projectRoot, { layerId, archive, dryRun } = {}) {
|
|
|
460
470
|
archivedCorrupt.push({ kind: 'state', story: c.story, ...arch });
|
|
461
471
|
}
|
|
462
472
|
for (const c of decisions.corrupt.concat(decisions.invalid)) {
|
|
463
|
-
const arch = archiveCorrupt(
|
|
473
|
+
const arch = archiveCorrupt(
|
|
474
|
+
projectRoot,
|
|
475
|
+
'decision-log',
|
|
476
|
+
c.story,
|
|
477
|
+
c.file,
|
|
478
|
+
c.error || c.reason,
|
|
479
|
+
);
|
|
464
480
|
archivedCorrupt.push({ kind: 'decision-log', story: c.story, ...arch });
|
|
465
481
|
}
|
|
466
482
|
}
|
|
@@ -468,8 +484,12 @@ function merge(projectRoot, { layerId, archive, dryRun } = {}) {
|
|
|
468
484
|
const stateBody = composeStateYaml(state);
|
|
469
485
|
const decisionBody = composeDecisionYaml(decisions);
|
|
470
486
|
|
|
471
|
-
const stateWrite = writeAuthoritative(projectRoot, 'autopilot-state.yaml', stateBody, {
|
|
472
|
-
|
|
487
|
+
const stateWrite = writeAuthoritative(projectRoot, 'autopilot-state.yaml', stateBody, {
|
|
488
|
+
dryRun,
|
|
489
|
+
});
|
|
490
|
+
const decisionWrite = writeAuthoritative(projectRoot, 'decision-log.yaml', decisionBody, {
|
|
491
|
+
dryRun,
|
|
492
|
+
});
|
|
473
493
|
|
|
474
494
|
let archiveDir = null;
|
|
475
495
|
let archiveSkipped = [];
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// post-green-gates.js — composed post-GREEN quality pipeline.
|
|
4
|
+
//
|
|
5
|
+
// Called by the orchestrator after a `bmad-dev-story` GREEN phase
|
|
6
|
+
// completes verify. Runs three gates in order; first failing gate causes
|
|
7
|
+
// non-zero exit with a structured JSON report.
|
|
8
|
+
//
|
|
9
|
+
// Gates:
|
|
10
|
+
// 1. lint-changed.js — biome/eslint on changed files only
|
|
11
|
+
// 2. lint-test-pitfalls.js — LLM-test pitfalls (only on test files)
|
|
12
|
+
// 3. scan.js (ci-parity) — search for obvious CI-only failure modes
|
|
13
|
+
// (envs hard-coded to local-only assumptions)
|
|
14
|
+
//
|
|
15
|
+
// Usage:
|
|
16
|
+
// post-green-gates.js [--json] [--changed-files <path>] [--project-root <path>]
|
|
17
|
+
// --changed-files: path to a newline-delimited list of changed files
|
|
18
|
+
// (default: derive from `git diff --name-only HEAD`)
|
|
19
|
+
//
|
|
20
|
+
// Each gate runs in a child process via execFileSync. Argv-only — no shell.
|
|
21
|
+
|
|
22
|
+
const { execFileSync, spawnSync } = require('node:child_process');
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
|
|
26
|
+
const { parseArgs } = require('../lib/runtime/args');
|
|
27
|
+
const log = require('../lib/runtime/log');
|
|
28
|
+
|
|
29
|
+
function help() {
|
|
30
|
+
log.out(
|
|
31
|
+
[
|
|
32
|
+
'Usage: post-green-gates.js [--json] [--changed-files <path>] [--project-root <path>]',
|
|
33
|
+
'',
|
|
34
|
+
'Runs in order:',
|
|
35
|
+
' 1. lint-changed.js — formatter + linter on changed files',
|
|
36
|
+
' 2. lint-test-pitfalls.js — LLM-test pitfall scan (test files only)',
|
|
37
|
+
' 3. scan.js (ci-parity) — CI-only failure mode scan',
|
|
38
|
+
].join('\n'),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function listChangedFiles(projectRoot, override) {
|
|
43
|
+
if (override) {
|
|
44
|
+
return fs
|
|
45
|
+
.readFileSync(override, 'utf8')
|
|
46
|
+
.split(/\r?\n/)
|
|
47
|
+
.map((s) => s.trim())
|
|
48
|
+
.filter((s) => s.length > 0);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const out = execFileSync('git', ['diff', '--name-only', 'HEAD'], {
|
|
52
|
+
cwd: projectRoot,
|
|
53
|
+
encoding: 'utf8',
|
|
54
|
+
});
|
|
55
|
+
return out
|
|
56
|
+
.split(/\r?\n/)
|
|
57
|
+
.map((s) => s.trim())
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
} catch (_e) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function runGate(name, command, args, projectRoot) {
|
|
65
|
+
const r = spawnSync(command, args, { cwd: projectRoot, encoding: 'utf8' });
|
|
66
|
+
return {
|
|
67
|
+
gate: name,
|
|
68
|
+
ok: r.status === 0,
|
|
69
|
+
exit_code: r.status === null ? -1 : r.status,
|
|
70
|
+
stdout: r.stdout || '',
|
|
71
|
+
stderr: r.stderr || '',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isTestFile(p) {
|
|
76
|
+
return /\.test\.(ts|tsx|js|jsx|mts)$/.test(p) || /\.spec\.(ts|tsx|js|jsx|mts)$/.test(p);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isJsTsFile(p) {
|
|
80
|
+
return /\.(ts|tsx|js|jsx|mts|cjs)$/.test(p);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function main(argv) {
|
|
84
|
+
const { opts } = parseArgs(argv, { booleanFlags: ['json', 'help'] });
|
|
85
|
+
if (opts.help) {
|
|
86
|
+
help();
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
const projectRoot = path.resolve(opts['project-root'] || process.cwd());
|
|
90
|
+
const changed = listChangedFiles(projectRoot, opts['changed-files']);
|
|
91
|
+
const jsTs = changed.filter(isJsTsFile);
|
|
92
|
+
const testFiles = changed.filter(isTestFile);
|
|
93
|
+
|
|
94
|
+
const gates = [];
|
|
95
|
+
|
|
96
|
+
// Gate 1: lint-changed.
|
|
97
|
+
const lintChangedPath = path.join(projectRoot, '_Sprintpilot', 'scripts', 'lint-changed.js');
|
|
98
|
+
if (fs.existsSync(lintChangedPath) && jsTs.length > 0) {
|
|
99
|
+
gates.push(
|
|
100
|
+
runGate(
|
|
101
|
+
'lint-changed',
|
|
102
|
+
'node',
|
|
103
|
+
[lintChangedPath, '--project-root', projectRoot],
|
|
104
|
+
projectRoot,
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
} else {
|
|
108
|
+
gates.push({
|
|
109
|
+
gate: 'lint-changed',
|
|
110
|
+
ok: true,
|
|
111
|
+
exit_code: 0,
|
|
112
|
+
stdout: 'skipped (no JS/TS changes or script missing)',
|
|
113
|
+
stderr: '',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Gate 2: lint-test-pitfalls — only run on test files.
|
|
118
|
+
const pitfallsPath = path.join(projectRoot, '_Sprintpilot', 'scripts', 'lint-test-pitfalls.js');
|
|
119
|
+
if (fs.existsSync(pitfallsPath) && testFiles.length > 0) {
|
|
120
|
+
gates.push(runGate('lint-test-pitfalls', 'node', [pitfallsPath, ...testFiles], projectRoot));
|
|
121
|
+
} else {
|
|
122
|
+
gates.push({
|
|
123
|
+
gate: 'lint-test-pitfalls',
|
|
124
|
+
ok: true,
|
|
125
|
+
exit_code: 0,
|
|
126
|
+
stdout: 'skipped (no test files in change set)',
|
|
127
|
+
stderr: '',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Gate 3: ci-parity via scan.js. Pattern set is intentionally conservative
|
|
132
|
+
// — flag obvious CI-only-fail patterns: `if (!process.env.CI)` skips,
|
|
133
|
+
// hardcoded localhost ports, `xit`/`xdescribe`. scan.js does the search;
|
|
134
|
+
// we treat its non-zero exit as a block.
|
|
135
|
+
const scanPath = path.join(projectRoot, '_Sprintpilot', 'scripts', 'scan.js');
|
|
136
|
+
if (fs.existsSync(scanPath) && jsTs.length > 0) {
|
|
137
|
+
gates.push(
|
|
138
|
+
runGate(
|
|
139
|
+
'ci-parity',
|
|
140
|
+
'node',
|
|
141
|
+
[
|
|
142
|
+
scanPath,
|
|
143
|
+
'--pattern',
|
|
144
|
+
'process.env.CI',
|
|
145
|
+
'--pattern',
|
|
146
|
+
'(localhost|127\\.0\\.0\\.1):\\d{4,5}',
|
|
147
|
+
'--paths',
|
|
148
|
+
...jsTs,
|
|
149
|
+
],
|
|
150
|
+
projectRoot,
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
} else {
|
|
154
|
+
gates.push({
|
|
155
|
+
gate: 'ci-parity',
|
|
156
|
+
ok: true,
|
|
157
|
+
exit_code: 0,
|
|
158
|
+
stdout: 'skipped (no JS/TS changes or script missing)',
|
|
159
|
+
stderr: '',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const firstFail = gates.find((g) => !g.ok);
|
|
164
|
+
const overallOk = !firstFail;
|
|
165
|
+
|
|
166
|
+
if (opts.json) {
|
|
167
|
+
process.stdout.write(
|
|
168
|
+
`${JSON.stringify({ ok: overallOk, gates, first_fail: firstFail?.gate || null }, null, 2)}\n`,
|
|
169
|
+
);
|
|
170
|
+
} else {
|
|
171
|
+
for (const g of gates) {
|
|
172
|
+
log.out(`[${g.ok ? '✓' : '✗'}] ${g.gate}: exit=${g.exit_code}`);
|
|
173
|
+
if (!g.ok) {
|
|
174
|
+
if (g.stdout) log.out(g.stdout);
|
|
175
|
+
if (g.stderr) log.err(g.stderr);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
log.out(overallOk ? 'all gates passed' : `failed gate: ${firstFail?.gate}`);
|
|
179
|
+
}
|
|
180
|
+
return overallOk ? 0 : 1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (require.main === module) {
|
|
184
|
+
process.exit(main(process.argv.slice(2)));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = { main, listChangedFiles, runGate, isTestFile, isJsTsFile };
|
|
@@ -196,7 +196,8 @@ function main() {
|
|
|
196
196
|
process.exit(1);
|
|
197
197
|
}
|
|
198
198
|
const projectRoot = opts['project-root'] || process.cwd();
|
|
199
|
-
const branchPrefix =
|
|
199
|
+
const branchPrefix =
|
|
200
|
+
opts['branch-prefix'] !== undefined ? String(opts['branch-prefix']) : 'story/';
|
|
200
201
|
const timeout = opts['lock-timeout-sec']
|
|
201
202
|
? Number.parseInt(String(opts['lock-timeout-sec']), 10)
|
|
202
203
|
: DEFAULT_LOCK_TIMEOUT_SEC;
|
|
@@ -568,7 +568,9 @@ function main() {
|
|
|
568
568
|
}
|
|
569
569
|
|
|
570
570
|
if (command === 'graph') {
|
|
571
|
-
process.stdout.write(
|
|
571
|
+
process.stdout.write(
|
|
572
|
+
`${JSON.stringify({ nodes: dag.nodes, edges: dag.edges, epic: dag.epic })}\n`,
|
|
573
|
+
);
|
|
572
574
|
return;
|
|
573
575
|
}
|
|
574
576
|
if (command === 'layers') {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// stack-snapshot.js — capture a snapshot of the current per-story branch
|
|
4
|
+
// stack so `land-this-pr.js` can land the active PR without losing the
|
|
5
|
+
// rest of the in-flight stack.
|
|
6
|
+
//
|
|
7
|
+
// Output: JSON to stdout (or --output <path>). Shape:
|
|
8
|
+
// {
|
|
9
|
+
// base_branch: string,
|
|
10
|
+
// ts: ISO,
|
|
11
|
+
// branches: [{ name, head, story_key, status, parent? }],
|
|
12
|
+
// active_pr: { branch, number?, story_key } | null,
|
|
13
|
+
// }
|
|
14
|
+
//
|
|
15
|
+
// Argv-only. Uses `git -C <projectRoot>` for every git call.
|
|
16
|
+
|
|
17
|
+
const { execFileSync } = require('node:child_process');
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
|
|
21
|
+
const { parseArgs } = require('../lib/runtime/args');
|
|
22
|
+
const log = require('../lib/runtime/log');
|
|
23
|
+
|
|
24
|
+
function help() {
|
|
25
|
+
log.out(
|
|
26
|
+
[
|
|
27
|
+
'Usage: stack-snapshot.js [--project-root <path>] [--base-branch <name>]',
|
|
28
|
+
' [--output <file>] [--active-branch <name>]',
|
|
29
|
+
' [--story-key <key>] [--pr-number <n>]',
|
|
30
|
+
].join('\n'),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function git(projectRoot, args) {
|
|
35
|
+
try {
|
|
36
|
+
return execFileSync('git', ['-C', projectRoot, ...args], { encoding: 'utf8' }).trim();
|
|
37
|
+
} catch (_e) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function listLocalBranches(projectRoot, prefix) {
|
|
43
|
+
const raw = git(projectRoot, [
|
|
44
|
+
'for-each-ref',
|
|
45
|
+
'--format=%(refname:short)\t%(objectname)',
|
|
46
|
+
`refs/heads/${prefix}`,
|
|
47
|
+
]);
|
|
48
|
+
if (!raw) return [];
|
|
49
|
+
return raw
|
|
50
|
+
.split(/\n/)
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.map((line) => {
|
|
53
|
+
const [name, head] = line.split('\t');
|
|
54
|
+
return { name, head };
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readSprintStatus(projectRoot) {
|
|
59
|
+
const p = path.join(
|
|
60
|
+
projectRoot,
|
|
61
|
+
'_bmad-output',
|
|
62
|
+
'implementation-artifacts',
|
|
63
|
+
'sprint-status.yaml',
|
|
64
|
+
);
|
|
65
|
+
if (!fs.existsSync(p)) return null;
|
|
66
|
+
try {
|
|
67
|
+
return fs.readFileSync(p, 'utf8');
|
|
68
|
+
} catch (_e) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function statusForStory(sprintStatusText, storyKey) {
|
|
74
|
+
if (!sprintStatusText) return 'unknown';
|
|
75
|
+
// Narrow regex — accept either "story_key: status" or block form.
|
|
76
|
+
const re = new RegExp(`^\\s*${storyKey}:\\s*(\\w+)`, 'm');
|
|
77
|
+
const m = sprintStatusText.match(re);
|
|
78
|
+
return m ? m[1] : 'unknown';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function snapshot(opts) {
|
|
82
|
+
const projectRoot = path.resolve(opts['project-root'] || process.cwd());
|
|
83
|
+
const baseBranch = opts['base-branch'] || 'main';
|
|
84
|
+
const branches = listLocalBranches(projectRoot, 'story/').map((b) => ({
|
|
85
|
+
...b,
|
|
86
|
+
story_key: b.name.slice('story/'.length),
|
|
87
|
+
}));
|
|
88
|
+
const sprintStatus = readSprintStatus(projectRoot);
|
|
89
|
+
for (const b of branches) {
|
|
90
|
+
b.status = statusForStory(sprintStatus, b.story_key);
|
|
91
|
+
}
|
|
92
|
+
const active =
|
|
93
|
+
opts['active-branch'] && opts['story-key']
|
|
94
|
+
? {
|
|
95
|
+
branch: opts['active-branch'],
|
|
96
|
+
story_key: opts['story-key'],
|
|
97
|
+
number: opts['pr-number'] ? Number(opts['pr-number']) : undefined,
|
|
98
|
+
}
|
|
99
|
+
: null;
|
|
100
|
+
return {
|
|
101
|
+
base_branch: baseBranch,
|
|
102
|
+
ts: new Date().toISOString(),
|
|
103
|
+
branches,
|
|
104
|
+
active_pr: active,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function main(argv) {
|
|
109
|
+
const { opts } = parseArgs(argv, { booleanFlags: ['help'] });
|
|
110
|
+
if (opts.help) {
|
|
111
|
+
help();
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
const snap = snapshot(opts);
|
|
115
|
+
const out = `${JSON.stringify(snap, null, 2)}\n`;
|
|
116
|
+
if (opts.output) {
|
|
117
|
+
fs.writeFileSync(path.resolve(opts.output), out, 'utf8');
|
|
118
|
+
} else {
|
|
119
|
+
process.stdout.write(out);
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (require.main === module) {
|
|
125
|
+
process.exit(main(process.argv.slice(2)));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { main, snapshot, statusForStory };
|
|
@@ -575,7 +575,14 @@ function deepAssign(target, source) {
|
|
|
575
575
|
for (const k of Object.keys(source)) {
|
|
576
576
|
const sv = source[k];
|
|
577
577
|
const tv = out[k];
|
|
578
|
-
if (
|
|
578
|
+
if (
|
|
579
|
+
sv &&
|
|
580
|
+
typeof sv === 'object' &&
|
|
581
|
+
!Array.isArray(sv) &&
|
|
582
|
+
tv &&
|
|
583
|
+
typeof tv === 'object' &&
|
|
584
|
+
!Array.isArray(tv)
|
|
585
|
+
) {
|
|
579
586
|
out[k] = deepAssign(tv, sv);
|
|
580
587
|
} else {
|
|
581
588
|
out[k] = sv;
|
|
@@ -176,7 +176,12 @@ function pairEvents(events) {
|
|
|
176
176
|
for (const key of Object.keys(openByStoryPhase)) {
|
|
177
177
|
const [story, phase] = key.split('::');
|
|
178
178
|
for (const startMs of openByStoryPhase[key]) {
|
|
179
|
-
orphans.push({
|
|
179
|
+
orphans.push({
|
|
180
|
+
story,
|
|
181
|
+
phase,
|
|
182
|
+
event: 'start-without-end',
|
|
183
|
+
ts: new Date(startMs).toISOString(),
|
|
184
|
+
});
|
|
180
185
|
}
|
|
181
186
|
}
|
|
182
187
|
|
|
@@ -213,14 +218,21 @@ function aggregate(paired) {
|
|
|
213
218
|
withPct.sort((a, b) => b.sum_ms - a.sum_ms);
|
|
214
219
|
const hotspots = withPct.filter((r) => r.pct_of_total > HOTSPOT_THRESHOLD);
|
|
215
220
|
|
|
216
|
-
const stories = Object.keys(paired.stories)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
const stories = Object.keys(paired.stories)
|
|
222
|
+
.sort()
|
|
223
|
+
.map((key) => {
|
|
224
|
+
const s = paired.stories[key];
|
|
225
|
+
const wall_ms = s.first !== null && s.last !== null ? s.last - s.first : 0;
|
|
226
|
+
const phaseSum = Object.values(s.phases)
|
|
227
|
+
.flat()
|
|
228
|
+
.reduce((acc, v) => acc + v, 0);
|
|
229
|
+
return {
|
|
230
|
+
story: key,
|
|
231
|
+
wall_ms,
|
|
232
|
+
phase_sum_ms: phaseSum,
|
|
233
|
+
phase_count: Object.keys(s.phases).length,
|
|
234
|
+
};
|
|
235
|
+
});
|
|
224
236
|
|
|
225
237
|
return {
|
|
226
238
|
total_paired_ms: totalPaired,
|
|
@@ -263,7 +275,9 @@ function renderText(report) {
|
|
|
263
275
|
if (report.phases.length === 0) {
|
|
264
276
|
lines.push(' (no paired start/end events)');
|
|
265
277
|
} else {
|
|
266
|
-
lines.push(
|
|
278
|
+
lines.push(
|
|
279
|
+
' phase count sum p50 p95 max %',
|
|
280
|
+
);
|
|
267
281
|
for (const r of report.phases) {
|
|
268
282
|
lines.push(
|
|
269
283
|
` ${r.phase.padEnd(40)} ${String(r.count).padStart(5)} ${fmtMs(r.sum_ms).padStart(6)} ${fmtMs(r.p50_ms).padStart(6)} ${fmtMs(r.p95_ms).padStart(6)} ${fmtMs(r.max_ms).padStart(6)} ${fmtPct(r.pct_of_total).padStart(6)}`,
|
|
@@ -318,7 +332,9 @@ function renderMarkdown(report) {
|
|
|
318
332
|
lines.push('| Story | Wall | Phase sum | # phases |');
|
|
319
333
|
lines.push('|---|---|---|---|');
|
|
320
334
|
for (const s of report.stories) {
|
|
321
|
-
lines.push(
|
|
335
|
+
lines.push(
|
|
336
|
+
`| ${s.story} | ${fmtMs(s.wall_ms)} | ${fmtMs(s.phase_sum_ms)} | ${s.phase_count} |`,
|
|
337
|
+
);
|
|
322
338
|
}
|
|
323
339
|
}
|
|
324
340
|
lines.push('');
|
|
@@ -360,7 +376,9 @@ function renderMarkdown(report) {
|
|
|
360
376
|
lines.push('');
|
|
361
377
|
lines.push('## Anomalies');
|
|
362
378
|
lines.push('');
|
|
363
|
-
lines.push(
|
|
379
|
+
lines.push(
|
|
380
|
+
"_Excluded from p50/p95/max so a single skew/stale-marker doesn't poison aggregates._",
|
|
381
|
+
);
|
|
364
382
|
lines.push('');
|
|
365
383
|
lines.push('| Phase | clock_skew | over_threshold |');
|
|
366
384
|
lines.push('|---|---:|---:|');
|
|
@@ -27,7 +27,8 @@ const log = require('../lib/runtime/log');
|
|
|
27
27
|
const DEFAULT_ATTEMPTS = 3;
|
|
28
28
|
const DEFAULT_MIN_MS = 500;
|
|
29
29
|
const DEFAULT_MAX_MS = 2000;
|
|
30
|
-
const DEFAULT_REF_LOCK_PATTERN =
|
|
30
|
+
const DEFAULT_REF_LOCK_PATTERN =
|
|
31
|
+
/cannot lock ref|Unable to create.*\.lock|Reference already exists|failed to lock|lock\.ref/i;
|
|
31
32
|
|
|
32
33
|
function help() {
|
|
33
34
|
log.out(
|
|
@@ -79,7 +80,15 @@ function runOnce(cmd, args, inherit = false) {
|
|
|
79
80
|
};
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
function runWithRetry({
|
|
83
|
+
function runWithRetry({
|
|
84
|
+
cmd,
|
|
85
|
+
args,
|
|
86
|
+
attempts = DEFAULT_ATTEMPTS,
|
|
87
|
+
minMs = DEFAULT_MIN_MS,
|
|
88
|
+
maxMs = DEFAULT_MAX_MS,
|
|
89
|
+
pattern = DEFAULT_REF_LOCK_PATTERN,
|
|
90
|
+
onAttempt = null,
|
|
91
|
+
}) {
|
|
83
92
|
const actualAttempts = Math.max(1, attempts | 0);
|
|
84
93
|
let last = null;
|
|
85
94
|
for (let i = 0; i < actualAttempts; i++) {
|
|
@@ -107,9 +116,12 @@ function main() {
|
|
|
107
116
|
help();
|
|
108
117
|
process.exit(opts.help ? 0 : 1);
|
|
109
118
|
}
|
|
110
|
-
const attempts =
|
|
111
|
-
|
|
112
|
-
const
|
|
119
|
+
const attempts =
|
|
120
|
+
opts.attempts !== undefined ? Number.parseInt(String(opts.attempts), 10) : DEFAULT_ATTEMPTS;
|
|
121
|
+
const minMs =
|
|
122
|
+
opts['min-ms'] !== undefined ? Number.parseInt(String(opts['min-ms']), 10) : DEFAULT_MIN_MS;
|
|
123
|
+
const maxMs =
|
|
124
|
+
opts['max-ms'] !== undefined ? Number.parseInt(String(opts['max-ms']), 10) : DEFAULT_MAX_MS;
|
|
113
125
|
let pattern = DEFAULT_REF_LOCK_PATTERN;
|
|
114
126
|
if (opts.pattern) {
|
|
115
127
|
try {
|
|
@@ -3,4 +3,26 @@ name: sprint-autopilot-on
|
|
|
3
3
|
description: 'Engage autonomous story execution for BMad Method with git workflow integration. Implements stories end-to-end with automatic branching (git worktrees), commits, linting, and PR creation. Uses standard git worktree commands for story isolation — works with any coding agent. Falls back to stock BMad behavior when git is disabled. Use when user says "/sprint-autopilot-on" or "start autopilot".'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
## STOP — read this entire file before doing anything
|
|
7
|
+
|
|
8
|
+
Sprintpilot is driven by a deterministic Node.js state machine at
|
|
9
|
+
`_Sprintpilot/bin/autopilot.js`. The LLM owns in-skill execution,
|
|
10
|
+
diagnosis, triage, and small-judgment decisions — not the flow.
|
|
11
|
+
|
|
12
|
+
Follow **`./workflow.orchestrator.md`** verbatim. Flow control lives in
|
|
13
|
+
`_Sprintpilot/bin/autopilot.js` (a Node CLI you call via `autopilot next`
|
|
14
|
+
/ `autopilot record`). The orchestrator emits actions; you execute them.
|
|
15
|
+
|
|
16
|
+
### Never improvise
|
|
17
|
+
|
|
18
|
+
- Never decide which BMad skill runs next yourself — the state machine
|
|
19
|
+
emits an `invoke_skill` action telling you.
|
|
20
|
+
- Never skip the `autopilot next` → `autopilot record` cycle. Even when
|
|
21
|
+
a step feels "obvious," route through the CLI so the ledger, verify,
|
|
22
|
+
and bookkeeping enforcement run.
|
|
23
|
+
- Do not search for `workflow.md` or reconstruct it from memory; do not
|
|
24
|
+
read cached BMad legacy patterns and apply them ahead of the
|
|
25
|
+
orchestrator's state machine.
|
|
26
|
+
|
|
27
|
+
`workflow.orchestrator.md` is the **sole authority** for the rest of the
|
|
28
|
+
session.
|