@ikunin/sprintpilot 2.2.14 → 2.2.16
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.
|
@@ -1161,6 +1161,96 @@ function acquireAutopilotLock(persisted, profile, projectRoot) {
|
|
|
1161
1161
|
return { acquired: true, id: null, warning: `unrecognized lock state: ${checkOut}` };
|
|
1162
1162
|
}
|
|
1163
1163
|
|
|
1164
|
+
// Worktree health check on boot. Documented in modules/git/config.yaml
|
|
1165
|
+
// as "check for orphaned worktrees from crashed sessions". The script
|
|
1166
|
+
// (scripts/health-check.js) categorizes worktrees as CLEAN_DONE /
|
|
1167
|
+
// COMMITTED / STALE / DIRTY / ORPHAN and writes a SUMMARY line.
|
|
1168
|
+
//
|
|
1169
|
+
// We treat ORPHAN as halt-worthy (a worktree directory exists but
|
|
1170
|
+
// `git rev-parse --git-dir` fails or no branch is checked out — almost
|
|
1171
|
+
// certainly leftover from a crashed session that needs cleanup). DIRTY
|
|
1172
|
+
// is logged but doesn't halt (user may be actively working in it).
|
|
1173
|
+
//
|
|
1174
|
+
// Returns one of:
|
|
1175
|
+
// { ok: true, summary } — no orphans; proceed
|
|
1176
|
+
// { ok: false, prompt, orphans, summary } — halt; caller emits user_prompt
|
|
1177
|
+
// { ok: true, skipped: true } — script missing / no worktrees dir / disabled
|
|
1178
|
+
function runWorktreeHealthCheck(profile, projectRoot) {
|
|
1179
|
+
if (!profile.worktree_health_check_on_boot) {
|
|
1180
|
+
return { ok: true, skipped: true, reason: 'disabled' };
|
|
1181
|
+
}
|
|
1182
|
+
if (!profile.worktree_enabled) {
|
|
1183
|
+
return { ok: true, skipped: true, reason: 'worktrees_disabled' };
|
|
1184
|
+
}
|
|
1185
|
+
const script = path.join(projectRoot, '_Sprintpilot', 'scripts', 'health-check.js');
|
|
1186
|
+
if (!fs.existsSync(script)) {
|
|
1187
|
+
return { ok: true, skipped: true, reason: 'script_missing' };
|
|
1188
|
+
}
|
|
1189
|
+
const worktreesDir = path.join(projectRoot, '.worktrees');
|
|
1190
|
+
if (!fs.existsSync(worktreesDir)) {
|
|
1191
|
+
return { ok: true, skipped: true, reason: 'no_worktrees_dir' };
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const { execFileSync: runFile } = require('node:child_process');
|
|
1195
|
+
let stdout = '';
|
|
1196
|
+
try {
|
|
1197
|
+
stdout = runFile(
|
|
1198
|
+
'node',
|
|
1199
|
+
[
|
|
1200
|
+
script,
|
|
1201
|
+
'--worktrees-dir',
|
|
1202
|
+
worktreesDir,
|
|
1203
|
+
'--base-branch',
|
|
1204
|
+
profile.base_branch || 'main',
|
|
1205
|
+
],
|
|
1206
|
+
{
|
|
1207
|
+
encoding: 'utf8',
|
|
1208
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1209
|
+
cwd: projectRoot,
|
|
1210
|
+
timeout: 60_000,
|
|
1211
|
+
},
|
|
1212
|
+
);
|
|
1213
|
+
} catch (e) {
|
|
1214
|
+
// Health check failure isn't fatal — log and proceed. A broken
|
|
1215
|
+
// script shouldn't gate the autopilot.
|
|
1216
|
+
return {
|
|
1217
|
+
ok: true,
|
|
1218
|
+
skipped: true,
|
|
1219
|
+
reason: 'health_check_error',
|
|
1220
|
+
error: e.message || String(e),
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const lines = stdout.split(/\r?\n/).filter(Boolean);
|
|
1225
|
+
const summaryLine = lines.find((l) => l.startsWith('SUMMARY:')) || '';
|
|
1226
|
+
// SUMMARY:total:cleanDone:committed:stale:dirty:orphan
|
|
1227
|
+
const parts = summaryLine.split(':');
|
|
1228
|
+
const summary = {
|
|
1229
|
+
total: parseInt(parts[1] || '0', 10),
|
|
1230
|
+
clean_done: parseInt(parts[2] || '0', 10),
|
|
1231
|
+
committed: parseInt(parts[3] || '0', 10),
|
|
1232
|
+
stale: parseInt(parts[4] || '0', 10),
|
|
1233
|
+
dirty: parseInt(parts[5] || '0', 10),
|
|
1234
|
+
orphan: parseInt(parts[6] || '0', 10),
|
|
1235
|
+
};
|
|
1236
|
+
const orphans = lines
|
|
1237
|
+
.filter((l) => l.startsWith('ORPHAN:'))
|
|
1238
|
+
.map((l) => l.slice('ORPHAN:'.length));
|
|
1239
|
+
|
|
1240
|
+
if (summary.orphan > 0) {
|
|
1241
|
+
return {
|
|
1242
|
+
ok: false,
|
|
1243
|
+
summary,
|
|
1244
|
+
orphans,
|
|
1245
|
+
prompt:
|
|
1246
|
+
`Found ${summary.orphan} orphaned worktree(s) under .worktrees/ from a previous (possibly crashed) session: ${orphans.join(', ')}. ` +
|
|
1247
|
+
`Run \`git worktree prune\` and remove the leftover directories before resuming, or run \`node _Sprintpilot/scripts/health-check.js --worktrees-dir .worktrees\` to see all categories.`,
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
return { ok: true, summary };
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1164
1254
|
function cmdStart(opts) {
|
|
1165
1255
|
const projectRoot = resolveProjectRoot(opts);
|
|
1166
1256
|
const { typed: profile } = resolveProfile(projectRoot, opts.profile);
|
|
@@ -1256,6 +1346,63 @@ function cmdStart(opts) {
|
|
|
1256
1346
|
);
|
|
1257
1347
|
}
|
|
1258
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
|
+
|
|
1372
|
+
// Worktree health check — once per session, after lock acquire so we
|
|
1373
|
+
// don't compete with another active session for the same .worktrees
|
|
1374
|
+
// directory.
|
|
1375
|
+
const healthOutcome = runWorktreeHealthCheck(profile, projectRoot);
|
|
1376
|
+
ledger.append(
|
|
1377
|
+
{
|
|
1378
|
+
kind: 'worktree_health_check',
|
|
1379
|
+
detail: {
|
|
1380
|
+
ok: healthOutcome.ok,
|
|
1381
|
+
skipped: !!healthOutcome.skipped,
|
|
1382
|
+
reason: healthOutcome.reason || null,
|
|
1383
|
+
summary: healthOutcome.summary || null,
|
|
1384
|
+
},
|
|
1385
|
+
},
|
|
1386
|
+
{ projectRoot },
|
|
1387
|
+
);
|
|
1388
|
+
if (!healthOutcome.ok) {
|
|
1389
|
+
const haltAction = {
|
|
1390
|
+
type: 'user_prompt',
|
|
1391
|
+
reason: 'worktree_orphans_detected',
|
|
1392
|
+
prompt: healthOutcome.prompt,
|
|
1393
|
+
orphans: healthOutcome.orphans,
|
|
1394
|
+
summary: healthOutcome.summary,
|
|
1395
|
+
};
|
|
1396
|
+
ledger.append(
|
|
1397
|
+
{ kind: 'action_emitted', phase: persisted.current_bmad_step || null, action: haltAction },
|
|
1398
|
+
{ projectRoot },
|
|
1399
|
+
);
|
|
1400
|
+
process.stdout.write(
|
|
1401
|
+
`${JSON.stringify({ action: haltAction, phase: persisted.current_bmad_step || null }, null, 2)}\n`,
|
|
1402
|
+
);
|
|
1403
|
+
return 0;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1259
1406
|
// Persist the new queue BEFORE composing runtime state so the queue
|
|
1260
1407
|
// head is visible to composeRuntimeState's resolver.
|
|
1261
1408
|
if (explicitQueue.length > 0) {
|
|
@@ -1564,4 +1711,5 @@ module.exports = {
|
|
|
1564
1711
|
decorateRunScript,
|
|
1565
1712
|
composeRuntimeState,
|
|
1566
1713
|
acquireAutopilotLock,
|
|
1714
|
+
runWorktreeHealthCheck,
|
|
1567
1715
|
};
|
|
@@ -39,6 +39,11 @@ const VALID_KINDS = [
|
|
|
39
39
|
// `--epic`. Logged once per start invocation so resume/audit can see
|
|
40
40
|
// why a queue head differs from sprint-status's natural order.
|
|
41
41
|
'story_queue_set',
|
|
42
|
+
// Worktree health check result, logged once per cmdStart when
|
|
43
|
+
// git.worktree.health_check_on_boot is true (the default). Detail
|
|
44
|
+
// includes `summary` (counts) or `reason` ('disabled' / 'no_worktrees_dir'
|
|
45
|
+
// / 'script_missing' / 'health_check_error' / 'worktrees_disabled').
|
|
46
|
+
'worktree_health_check',
|
|
42
47
|
];
|
|
43
48
|
|
|
44
49
|
function isPlainObject(v) {
|
|
@@ -96,6 +96,15 @@ function flatToProfile(resolved, profileName) {
|
|
|
96
96
|
conditional_boot_work: coerceBool(get(resolved, 'autopilot.conditional_boot_work'), false),
|
|
97
97
|
granularity: coerceEnum(get(resolved, 'git.granularity'), VALID_GRANULARITIES, 'story'),
|
|
98
98
|
worktree_enabled: coerceBool(get(resolved, 'git.worktree.enabled'), true),
|
|
99
|
+
// git.worktree.health_check_on_boot — when true, cmdStart runs
|
|
100
|
+
// scripts/health-check.js once per session and halts if it finds
|
|
101
|
+
// ORPHAN worktrees (left over from crashed sessions). Documented in
|
|
102
|
+
// modules/git/config.yaml ("check for orphaned worktrees from
|
|
103
|
+
// crashed sessions").
|
|
104
|
+
worktree_health_check_on_boot: coerceBool(
|
|
105
|
+
get(resolved, 'git.worktree.health_check_on_boot'),
|
|
106
|
+
true,
|
|
107
|
+
),
|
|
99
108
|
squash_on_merge: coerceBool(get(resolved, 'git.squash_on_merge'), false),
|
|
100
109
|
reuse_user_branch: coerceBool(get(resolved, 'git.reuse_user_branch'), false),
|
|
101
110
|
merge_strategy: coerceEnum(
|
|
@@ -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.16",
|
|
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": {
|