@ikunin/sprintpilot 2.2.14 → 2.2.15

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,40 @@ function cmdStart(opts) {
1256
1346
  );
1257
1347
  }
1258
1348
 
1349
+ // Worktree health check — once per session, after lock acquire so we
1350
+ // don't compete with another active session for the same .worktrees
1351
+ // directory.
1352
+ const healthOutcome = runWorktreeHealthCheck(profile, projectRoot);
1353
+ ledger.append(
1354
+ {
1355
+ kind: 'worktree_health_check',
1356
+ detail: {
1357
+ ok: healthOutcome.ok,
1358
+ skipped: !!healthOutcome.skipped,
1359
+ reason: healthOutcome.reason || null,
1360
+ summary: healthOutcome.summary || null,
1361
+ },
1362
+ },
1363
+ { projectRoot },
1364
+ );
1365
+ if (!healthOutcome.ok) {
1366
+ const haltAction = {
1367
+ type: 'user_prompt',
1368
+ reason: 'worktree_orphans_detected',
1369
+ prompt: healthOutcome.prompt,
1370
+ orphans: healthOutcome.orphans,
1371
+ summary: healthOutcome.summary,
1372
+ };
1373
+ ledger.append(
1374
+ { kind: 'action_emitted', phase: persisted.current_bmad_step || null, action: haltAction },
1375
+ { projectRoot },
1376
+ );
1377
+ process.stdout.write(
1378
+ `${JSON.stringify({ action: haltAction, phase: persisted.current_bmad_step || null }, null, 2)}\n`,
1379
+ );
1380
+ return 0;
1381
+ }
1382
+
1259
1383
  // Persist the new queue BEFORE composing runtime state so the queue
1260
1384
  // head is visible to composeRuntimeState's resolver.
1261
1385
  if (explicitQueue.length > 0) {
@@ -1564,4 +1688,5 @@ module.exports = {
1564
1688
  decorateRunScript,
1565
1689
  composeRuntimeState,
1566
1690
  acquireAutopilotLock,
1691
+ runWorktreeHealthCheck,
1567
1692
  };
@@ -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.15
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikunin/sprintpilot",
3
- "version": "2.2.14",
3
+ "version": "2.2.15",
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": {