@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,6 +1,6 @@
1
1
  addon:
2
2
  name: sprintpilot
3
- version: 2.2.14
3
+ version: 2.2.16
4
4
  description: Sprintpilot — autopilot and multi-agent addon for BMad Method (git workflow, parallel agents, autonomous story execution)
5
5
  bmad_compatibility: ">=6.2.0"
6
6
  modules:
@@ -1,7 +1,12 @@
1
- # Multi-Agent Configuration — disabled in v1.0
2
- # Enable in v1.1 for parallel code review
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
- multi_agent:
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.14",
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": {