@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(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikunin/sprintpilot",
|
|
3
|
-
"version": "2.2.
|
|
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": {
|