claude-code-session-manager 0.75.3 → 0.77.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/dist/assets/{AgentLibrary-CzQqcObq.js → AgentLibrary-B2ie8bbw.js} +2 -2
- package/dist/assets/{DataModel-Bj_WlLz8.js → DataModel-BIJPYw32.js} +1 -1
- package/dist/assets/{History-DnSi_OHm.js → History-CeY6dk9S.js} +2 -2
- package/dist/assets/{Hooks-0BB0dp3S.js → Hooks-BFH2ocKg.js} +2 -2
- package/dist/assets/{HostBilko-DHpwwsLQ.js → HostBilko-36gj9wLz.js} +1 -1
- package/dist/assets/{Library-CaJVqVvi.js → Library-C-hBct39.js} +1 -1
- package/dist/assets/{ListDetail-C1W2HmC2.js → ListDetail-CNq64VWV.js} +1 -1
- package/dist/assets/{MarkdownEditor-5Ob9FW3z.js → MarkdownEditor-Bh3qt5-1.js} +1 -1
- package/dist/assets/{McpServers-JxCSfm1S.js → McpServers-DpGN0oyz.js} +1 -1
- package/dist/assets/{Memory-BDeqlqwH.js → Memory-D59hUjC4.js} +6 -6
- package/dist/assets/{Panel-Dh9ZHuEj.js → Panel-DCgbaoci.js} +1 -1
- package/dist/assets/{Permissions-DXy-CbEY.js → Permissions-DAmQ0DYV.js} +2 -2
- package/dist/assets/{Plugins-_n1Iuc8T.js → Plugins-Dyfgn6Is.js} +2 -2
- package/dist/assets/{ProvenanceBadge-BP_evfxE.js → ProvenanceBadge-BiYhPO1U.js} +1 -1
- package/dist/assets/SaveBar-RV7B6sOh.js +1 -0
- package/dist/assets/Scheduler-BPaNqx1b.js +14 -0
- package/dist/assets/{ScopeSwitcher-CAWzM6RI.js → ScopeSwitcher-P4mdLGNU.js} +1 -1
- package/dist/assets/{Settings-DRRozLyT.js → Settings-BL4vf5aX.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-DGHDWlz4.js → SkillReferenceGraph-BRBDyi1_.js} +1 -1
- package/dist/assets/{Skills-D8L66eiX.js → Skills-BV08gDUH.js} +2 -2
- package/dist/assets/{SystemPrompt-CYtUsonD.js → SystemPrompt-CLftSsDw.js} +1 -1
- package/dist/assets/TagLibrary-Bp8jGsd5.js +1 -0
- package/dist/assets/{TiptapBody-B2hRgbPE.js → TiptapBody-jCpuB6E5.js} +1 -1
- package/dist/assets/{Toggle-BTwsbxam.js → Toggle-D2paA1xf.js} +1 -1
- package/dist/assets/{index-DijufvkJ.js → index-BDRSqBl3.js} +704 -704
- package/dist/assets/{index-CMLnzdZC.css → index-CYhdtisq.css} +1 -1
- package/dist/assets/{settingsSchema-D6wzxAi6.js → settingsSchema-6IOLjZZN.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +8 -2
- package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
- package/scripts/lib/activeSessions.cjs +116 -6
- package/scripts/project-pages-logic/dist/logic.cjs +4709 -0
- package/scripts/render-project-pages/dist/renderer.cjs +18900 -0
- package/scripts/render-project-pages.cjs +70 -0
- package/scripts/scheduler-mcp-server.cjs +269 -96
- package/scripts/validate-project-pages-summary.cjs +62 -0
- package/src/main/__tests__/agentModelResolve.test.cjs +66 -0
- package/src/main/__tests__/epicStatusMirror.test.cjs +110 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +106 -0
- package/src/main/__tests__/prdAdminRoutes.test.cjs +295 -0
- package/src/main/__tests__/prdAgentType.test.cjs +103 -0
- package/src/main/__tests__/prdCreate.test.cjs +247 -0
- package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +117 -0
- package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +108 -0
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +485 -0
- package/src/main/__tests__/projectPages.test.cjs +73 -1
- package/src/main/__tests__/rcaReport.test.cjs +54 -0
- package/src/main/__tests__/runVerify.test.cjs +94 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +58 -3
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +103 -0
- package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +41 -0
- package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +10 -0
- package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +78 -0
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +242 -0
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +31 -0
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +201 -0
- package/src/main/__tests__/scheduler-leftover-fields.test.cjs +52 -0
- package/src/main/__tests__/scheduler-looks-done.test.cjs +241 -0
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +135 -0
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +222 -0
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +207 -1
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +212 -0
- package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +185 -0
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +194 -0
- package/src/main/__tests__/seedAgentPersonas.test.cjs +75 -14
- package/src/main/__tests__/seedSchedulerMcp.test.cjs +66 -0
- package/src/main/__tests__/uniquePrdNumbers.test.cjs +14 -5
- package/src/main/bilkoHost.cjs +4 -3
- package/src/main/chatRunner.cjs +6 -1
- package/src/main/config.cjs +25 -33
- package/src/main/health.cjs +153 -2
- package/src/main/index.cjs +64 -5
- package/src/main/ipcSchemas.cjs +69 -1
- package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +179 -0
- package/src/main/lib/__tests__/childWithLog.test.cjs +141 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +391 -42
- package/src/main/lib/__tests__/ephemeralCwd.test.cjs +91 -0
- package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +5 -3
- package/src/main/lib/__tests__/fixChainDepth.test.cjs +40 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +290 -5
- package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +107 -0
- package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +153 -0
- package/src/main/lib/__tests__/jobWorktree.test.cjs +6 -4
- package/src/main/lib/__tests__/landedSinceRun.test.cjs +73 -0
- package/src/main/lib/__tests__/launchFailure.test.cjs +220 -0
- package/src/main/lib/__tests__/loadGate.test.cjs +159 -0
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +102 -0
- package/src/main/lib/__tests__/opsOwnership.test.cjs +7 -0
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +151 -0
- package/src/main/lib/__tests__/opsRootResolve.test.cjs +149 -0
- package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +82 -0
- package/src/main/lib/__tests__/projectRootResolve.test.cjs +148 -0
- package/src/main/lib/__tests__/queueHealth.test.cjs +58 -0
- package/src/main/lib/__tests__/quietMachineLease.test.cjs +39 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +133 -0
- package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +19 -9
- package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +213 -0
- package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +125 -0
- package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +127 -0
- package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +109 -0
- package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +71 -0
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +217 -0
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +350 -0
- package/src/main/lib/activeIndexMerge.cjs +15 -0
- package/src/main/lib/activeIndexRebuild.cjs +133 -0
- package/src/main/lib/agentModelResolve.cjs +58 -0
- package/src/main/lib/buildTarget.cjs +3 -2
- package/src/main/lib/childWithLog.cjs +69 -2
- package/src/main/lib/claudeBin.cjs +54 -1
- package/src/main/lib/crossProjectFeedback.cjs +8 -1
- package/src/main/lib/definitionOfDone.cjs +3 -2
- package/src/main/lib/delegationReadiness.cjs +514 -26
- package/src/main/lib/ephemeralCwd.cjs +78 -0
- package/src/main/lib/epicDelegationStats.cjs +2 -1
- package/src/main/lib/epicMint.cjs +17 -1
- package/src/main/lib/epicStatusMirror.cjs +95 -0
- package/src/main/lib/epicValidationHook.cjs +2 -1
- package/src/main/lib/epicWorktreeMint.cjs +5 -2
- package/src/main/lib/fixChainDepth.cjs +45 -0
- package/src/main/lib/gitWorktree.cjs +520 -21
- package/src/main/lib/jobWorktree.cjs +2 -0
- package/src/main/lib/landedSinceRun.cjs +55 -0
- package/src/main/lib/launchFailure.cjs +357 -0
- package/src/main/lib/loadGate.cjs +134 -0
- package/src/main/lib/mcpToolCatalog.cjs +370 -0
- package/src/main/lib/opsErrorLog.cjs +12 -1
- package/src/main/lib/opsOwnership.cjs +106 -0
- package/src/main/lib/prdAdminRoutes.cjs +43 -3
- package/src/main/lib/prdAgentType.cjs +84 -0
- package/src/main/lib/prdCreate.cjs +103 -15
- package/src/main/lib/prdDeclaredPaths.cjs +70 -0
- package/src/main/lib/prdFrontmatter.cjs +17 -3
- package/src/main/lib/prdLocations.cjs +13 -6
- package/src/main/lib/projectHomeAdminRoutes.cjs +402 -0
- package/src/main/lib/projectPageSummarySchema.cjs +181 -0
- package/src/main/lib/projectRootResolve.cjs +134 -0
- package/src/main/lib/promptSessionSchema.cjs +7 -0
- package/src/main/lib/queueHealth.cjs +38 -0
- package/src/main/lib/queueStore.cjs +40 -7
- package/src/main/lib/quietMachineLease.cjs +48 -0
- package/src/main/lib/rcaReport.cjs +54 -4
- package/src/main/lib/reaperHelpers.cjs +64 -1
- package/src/main/lib/scheduleJobSchema.cjs +31 -0
- package/src/main/lib/scheduleJobTransitions.cjs +6 -2
- package/src/main/lib/schedulerBatch.cjs +301 -55
- package/src/main/lib/schedulerConfig.cjs +99 -0
- package/src/main/projectBrief.cjs +3 -2
- package/src/main/projectPages.cjs +162 -3
- package/src/main/promptSessionTranscript.cjs +0 -0
- package/src/main/pty.cjs +5 -0
- package/src/main/queueOps.cjs +15 -8
- package/src/main/runVerify.cjs +50 -9
- package/src/main/scheduler/prdParser.cjs +18 -1
- package/src/main/scheduler.cjs +1701 -130
- package/src/main/seedAgentPersonas.cjs +62 -21
- package/src/main/seedSchedulerMcp.cjs +58 -4
- package/src/main/templates/project-pages-catalog.json +741 -0
- package/src/main/templates/project-pages-pipeline.md +417 -0
- package/src/preload/api.d.ts +187 -3
- package/src/preload/index.cjs +9 -0
- package/src/seed/agents/project-home-builder.md +59 -0
- package/dist/assets/SaveBar-D-gCUx4n.js +0 -1
- package/dist/assets/Scheduler-Bpd4OGju.js +0 -14
- package/dist/assets/TagLibrary-E5CLeuVk.js +0 -1
|
@@ -1197,6 +1197,100 @@ test('no_verdict_sentinel guard: same run but with SCHEDULER_VERDICT: PASS senti
|
|
|
1197
1197
|
}
|
|
1198
1198
|
});
|
|
1199
1199
|
|
|
1200
|
+
// ─── abandoned_background_task: auto-backgrounded Bash + no sentinel/commit ─
|
|
1201
|
+
|
|
1202
|
+
/** A run whose Bash command hit the harness's 120s foreground timeout and was
|
|
1203
|
+
* auto-backgrounded, then the run ended without ever coming back to commit
|
|
1204
|
+
* or emit the finish-protocol sentinel. */
|
|
1205
|
+
function abandonedBackgroundTaskRunEvents(resultText) {
|
|
1206
|
+
return [
|
|
1207
|
+
{
|
|
1208
|
+
type: 'assistant',
|
|
1209
|
+
message: {
|
|
1210
|
+
role: 'assistant',
|
|
1211
|
+
content: [{
|
|
1212
|
+
type: 'tool_use',
|
|
1213
|
+
id: 'toolu_bg_001',
|
|
1214
|
+
name: 'Bash',
|
|
1215
|
+
input: { command: 'npm run test:unit', description: 'Run full unit suite' },
|
|
1216
|
+
}],
|
|
1217
|
+
},
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
type: 'user',
|
|
1221
|
+
message: {
|
|
1222
|
+
role: 'user',
|
|
1223
|
+
content: [{
|
|
1224
|
+
type: 'tool_result',
|
|
1225
|
+
tool_use_id: 'toolu_bg_001',
|
|
1226
|
+
content: 'Command did not complete within its 120s timeout and was moved to the background (ID: bcn1lpvpd). Output is being written to: /tmp/claude-1000/tasks/bcn1lpvpd.output. You will be notified when it completes. To check interim output, use Read on that file path.',
|
|
1227
|
+
is_error: false,
|
|
1228
|
+
}],
|
|
1229
|
+
},
|
|
1230
|
+
},
|
|
1231
|
+
{ type: 'result', subtype: 'success', result: resultText },
|
|
1232
|
+
];
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
test('abandoned_background_task: background marker + no sentinel + no commit → abandoned_background_task', async () => {
|
|
1236
|
+
const tmp = makeTmpDir();
|
|
1237
|
+
try {
|
|
1238
|
+
const slug = '1116-encyclopedia-animations-section';
|
|
1239
|
+
writeLog(tmp, slug, abandonedBackgroundTaskRunEvents('Waiting for the backgrounded test run to finish.'));
|
|
1240
|
+
const prdPath = writePrd(tmp, slug, '# Encyclopedia animations section');
|
|
1241
|
+
const verdict = await verifyRun({
|
|
1242
|
+
runDir: tmp,
|
|
1243
|
+
prdPath,
|
|
1244
|
+
queueEntry: { slug, status: 'running' },
|
|
1245
|
+
allJobs: [],
|
|
1246
|
+
committedDuringRun: false,
|
|
1247
|
+
});
|
|
1248
|
+
assert.equal(verdict.verdict, 'abandoned_background_task', `expected abandoned_background_task, got ${verdict.verdict}: ${verdict.reason}`);
|
|
1249
|
+
assert.equal(verdict.downgradeTo, 'needs_review');
|
|
1250
|
+
} finally {
|
|
1251
|
+
rmdir(tmp);
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
test('abandoned_background_task guard: no background marker, no sentinel, no commit → stays plain no_verdict_sentinel', async () => {
|
|
1256
|
+
const tmp = makeTmpDir();
|
|
1257
|
+
try {
|
|
1258
|
+
const slug = '1116-no-background-marker';
|
|
1259
|
+
writeLog(tmp, slug, noOpRunEvents('I need clarification on which capture API to target before proceeding.'));
|
|
1260
|
+
const prdPath = writePrd(tmp, slug, '# Encyclopedia animations section');
|
|
1261
|
+
const verdict = await verifyRun({
|
|
1262
|
+
runDir: tmp,
|
|
1263
|
+
prdPath,
|
|
1264
|
+
queueEntry: { slug, status: 'running' },
|
|
1265
|
+
allJobs: [],
|
|
1266
|
+
committedDuringRun: false,
|
|
1267
|
+
});
|
|
1268
|
+
assert.equal(verdict.verdict, 'no_verdict_sentinel', `expected plain no_verdict_sentinel, got ${verdict.verdict}: ${verdict.reason}`);
|
|
1269
|
+
} finally {
|
|
1270
|
+
rmdir(tmp);
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
test('abandoned_background_task guard: background marker present but run DID commit → clean', async () => {
|
|
1275
|
+
const tmp = makeTmpDir();
|
|
1276
|
+
try {
|
|
1277
|
+
const slug = '1116-background-marker-but-committed';
|
|
1278
|
+
writeLog(tmp, slug, abandonedBackgroundTaskRunEvents('Backgrounded test run finished green; work committed.'));
|
|
1279
|
+
const prdPath = writePrd(tmp, slug, '# Encyclopedia animations section');
|
|
1280
|
+
const verdict = await verifyRun({
|
|
1281
|
+
runDir: tmp,
|
|
1282
|
+
prdPath,
|
|
1283
|
+
queueEntry: { slug, status: 'running' },
|
|
1284
|
+
allJobs: [],
|
|
1285
|
+
committedDuringRun: true,
|
|
1286
|
+
});
|
|
1287
|
+
assert.equal(verdict.verdict, 'clean', `background marker with a landed commit should stay clean, got ${verdict.verdict}: ${verdict.reason}`);
|
|
1288
|
+
assert.equal(verdict.downgradeTo, null);
|
|
1289
|
+
} finally {
|
|
1290
|
+
rmdir(tmp);
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1200
1294
|
// ─── pass_no_commit: PASS sentinel with no commit is not "clean" ───────────
|
|
1201
1295
|
|
|
1202
1296
|
test('pass_no_commit: SCHEDULER_VERDICT: PASS but no commit landed → needs_review', async () => {
|
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
selectAutoFixTargets, isUnresolvableNeedsReview, isRescanCandidate,
|
|
13
13
|
healTargetForFix, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH,
|
|
14
14
|
isEligibleForImmediateAutoFix, isExhaustedAutoFix, isPlanUnqueued, fixSlugFor,
|
|
15
|
+
applyRcaClassification,
|
|
15
16
|
} = require('../scheduler.cjs');
|
|
16
17
|
|
|
17
18
|
const noSiblingOnDisk = () => false;
|
|
@@ -211,6 +212,12 @@ test('invariant: plan outcome WITH a queue row → not a target and NOT annotate
|
|
|
211
212
|
assert.strictEqual(isAnnotatable, false);
|
|
212
213
|
});
|
|
213
214
|
|
|
215
|
+
test('excludes a job whose rcaRecoveryAction is archive (already-shipped stale re-run must never buy a fix-plan)', () => {
|
|
216
|
+
const jobs = [makeJob({ rcaRecoveryAction: 'archive', rcaFailureClass: 'already-shipped' })];
|
|
217
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
218
|
+
assert.strictEqual(result.length, 0);
|
|
219
|
+
});
|
|
220
|
+
|
|
214
221
|
test('excludes when fix sibling exists on disk', () => {
|
|
215
222
|
const jobs = [makeJob()];
|
|
216
223
|
// fixSlug = '05-fix-my-feature'
|
|
@@ -267,12 +274,18 @@ test('depth-1 (ordinary, no investigationDepth field) needs_review job is eligib
|
|
|
267
274
|
assert.strictEqual(result.length, 1);
|
|
268
275
|
});
|
|
269
276
|
|
|
270
|
-
test('fix-plan job with investigationDepth:
|
|
271
|
-
const jobs = [makeJob({ slug: '05-fix-foo', investigationDepth:
|
|
277
|
+
test('fix-plan job with investigationDepth: 1 is still eligible (its first -fix follow-up)', () => {
|
|
278
|
+
const jobs = [makeJob({ slug: '05-fix-foo', investigationDepth: 1 })];
|
|
272
279
|
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
273
280
|
assert.strictEqual(result.length, 1);
|
|
274
281
|
});
|
|
275
282
|
|
|
283
|
+
test('fix-plan job with investigationDepth: 2 is now NOT eligible (cap lowered to permit at most -fix-fix)', () => {
|
|
284
|
+
const jobs = [makeJob({ slug: '05-fix-foo', investigationDepth: 2 })];
|
|
285
|
+
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
286
|
+
assert.strictEqual(result.length, 0);
|
|
287
|
+
});
|
|
288
|
+
|
|
276
289
|
test('fix-plan job with investigationDepth: 3 is NOT eligible (bound holds)', () => {
|
|
277
290
|
const jobs = [makeJob({ slug: '05-fix-foo', investigationDepth: 3 })];
|
|
278
291
|
const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
|
|
@@ -289,7 +302,7 @@ test('isFixPlanBeyondDepthCap: non-fix-plan slug is never capped regardless of d
|
|
|
289
302
|
assert.strictEqual(isFixPlanBeyondDepthCap('05-my-feature', 99), false);
|
|
290
303
|
});
|
|
291
304
|
|
|
292
|
-
test('isFixPlanBeyondDepthCap: fix-plan at depth 1
|
|
305
|
+
test('isFixPlanBeyondDepthCap: fix-plan at depth 1 is not capped, depth 2 is', () => {
|
|
293
306
|
assert.strictEqual(isFixPlanBeyondDepthCap('05-fix-foo', 1), false);
|
|
294
307
|
assert.strictEqual(isFixPlanBeyondDepthCap('05-fix-foo', MAX_INVESTIGATION_DEPTH), false);
|
|
295
308
|
assert.strictEqual(isFixPlanBeyondDepthCap('05-fix-foo', MAX_INVESTIGATION_DEPTH + 1), true);
|
|
@@ -349,6 +362,12 @@ test('isEligibleForImmediateAutoFix: fix-plan job beyond the depth cap is exclud
|
|
|
349
362
|
assert.strictEqual(result, false);
|
|
350
363
|
});
|
|
351
364
|
|
|
365
|
+
test('isEligibleForImmediateAutoFix: fix-plan job at investigationDepth 2 is now excluded (previously eligible under the old cap)', () => {
|
|
366
|
+
const job = makeJob({ slug: '05-fix-foo', investigationDepth: 2 });
|
|
367
|
+
const result = isEligibleForImmediateAutoFix(job, [job], noSiblingOnDisk);
|
|
368
|
+
assert.strictEqual(result, false);
|
|
369
|
+
});
|
|
370
|
+
|
|
352
371
|
test('isEligibleForImmediateAutoFix: a different job in the same queue does not affect this job\'s eligibility', () => {
|
|
353
372
|
const job = makeJob();
|
|
354
373
|
const other = { slug: '05-fix-my-feature', status: 'pending', runId: null };
|
|
@@ -364,4 +383,40 @@ test('isEligibleForImmediateAutoFix: a job whose status is not needs_review (e.g
|
|
|
364
383
|
assert.strictEqual(result, false);
|
|
365
384
|
});
|
|
366
385
|
|
|
386
|
+
// ---- applyRcaClassification (persists writeRcaReport's verdict onto the job row) ----
|
|
387
|
+
|
|
388
|
+
test('applyRcaClassification: persists failureClass + recoveryAction onto a needs_review job', () => {
|
|
389
|
+
const job = makeJob({ status: 'needs_review' });
|
|
390
|
+
const applied = applyRcaClassification(job, { filed: true, failureClass: 'already-shipped', recoveryAction: 'archive' });
|
|
391
|
+
assert.strictEqual(applied, true);
|
|
392
|
+
assert.strictEqual(job.rcaFailureClass, 'already-shipped');
|
|
393
|
+
assert.strictEqual(job.rcaRecoveryAction, 'archive');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test('applyRcaClassification: no-op when the report was never filed', () => {
|
|
397
|
+
const job = makeJob({ status: 'needs_review' });
|
|
398
|
+
const applied = applyRcaClassification(job, { filed: false, reason: 'disabled' });
|
|
399
|
+
assert.strictEqual(applied, false);
|
|
400
|
+
assert.strictEqual(job.rcaFailureClass, undefined);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('applyRcaClassification: no-op when the job has moved off needs_review', () => {
|
|
404
|
+
const job = makeJob({ status: 'completed' });
|
|
405
|
+
const applied = applyRcaClassification(job, { filed: true, failureClass: 'unknown', recoveryAction: 'investigate' });
|
|
406
|
+
assert.strictEqual(applied, false);
|
|
407
|
+
assert.strictEqual(job.rcaFailureClass, undefined);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('applyRcaClassification: no-op when the job is missing (e.g. slug lookup miss)', () => {
|
|
411
|
+
const applied = applyRcaClassification(undefined, { filed: true, failureClass: 'unknown', recoveryAction: 'investigate' });
|
|
412
|
+
assert.strictEqual(applied, false);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test('applyRcaClassification result feeds selectAutoFixTargets: an archive-classified job is excluded', () => {
|
|
416
|
+
const job = makeJob({ status: 'needs_review' });
|
|
417
|
+
applyRcaClassification(job, { filed: true, failureClass: 'already-shipped', recoveryAction: 'archive' });
|
|
418
|
+
const result = selectAutoFixTargets([job], { fixSlugExists: noSiblingOnDisk });
|
|
419
|
+
assert.strictEqual(result.length, 0);
|
|
420
|
+
});
|
|
421
|
+
|
|
367
422
|
console.log('scheduler-autofix-select tests: PASS');
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-bash-timeout-env.test.cjs — PRD 1097.
|
|
3
|
+
*
|
|
4
|
+
* A headless `claude -p` job inherits the harness's 120s default foreground
|
|
5
|
+
* Bash timeout. Any legit gate command (a test suite, a build) that runs
|
|
6
|
+
* long gets auto-backgrounded, and the tool result promises a notification
|
|
7
|
+
* that a single-shot headless run can never receive — the run dead-ends
|
|
8
|
+
* with no commit and no verdict. The fix is to raise
|
|
9
|
+
* BASH_DEFAULT_TIMEOUT_MS / BASH_MAX_TIMEOUT_MS on the spawned child's env.
|
|
10
|
+
*
|
|
11
|
+
* This proves:
|
|
12
|
+
* 1. executeJob's real spawned child actually receives both env vars, with
|
|
13
|
+
* the exported constant values (not just that the constants exist).
|
|
14
|
+
* 2. BASH_MAX_TIMEOUT_MS stays strictly below IDLE_OUTPUT_KILL_MS — the
|
|
15
|
+
* coupling that makes this more than a one-liner: a long foreground
|
|
16
|
+
* Bash emits no stream-json events, so the log mtime stalls and the
|
|
17
|
+
* idle-tail watchdog would SIGTERM the job mid-gate if the two ever
|
|
18
|
+
* crossed.
|
|
19
|
+
*
|
|
20
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-bash-timeout-env.test.cjs
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const os = require('node:os');
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
|
|
30
|
+
let tmpHome;
|
|
31
|
+
let originalHome;
|
|
32
|
+
let executeJob;
|
|
33
|
+
let BASH_DEFAULT_TIMEOUT_MS;
|
|
34
|
+
let BASH_MAX_TIMEOUT_MS;
|
|
35
|
+
let IDLE_OUTPUT_KILL_MS;
|
|
36
|
+
|
|
37
|
+
beforeAll(() => {
|
|
38
|
+
originalHome = process.env.HOME;
|
|
39
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-bash-timeout-env-'));
|
|
40
|
+
process.env.HOME = tmpHome;
|
|
41
|
+
({ executeJob, BASH_DEFAULT_TIMEOUT_MS, BASH_MAX_TIMEOUT_MS, IDLE_OUTPUT_KILL_MS } = require('../scheduler.cjs'));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterAll(() => {
|
|
45
|
+
process.env.HOME = originalHome;
|
|
46
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
delete process.env.SM_CLAUDE_BIN;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Stub `claude` binary: dumps the two env vars it received into a marker
|
|
54
|
+
// file, then emits a stream-json success result and exits 0.
|
|
55
|
+
function writeClaudeStub() {
|
|
56
|
+
const stubPath = path.join(os.tmpdir(), `sm-claude-stub-env-${process.pid}-${Math.floor(Math.random() * 1e9)}.cjs`);
|
|
57
|
+
const body = `
|
|
58
|
+
const fs = require('fs');
|
|
59
|
+
const path = require('path');
|
|
60
|
+
fs.writeFileSync(path.join(process.cwd(), 'env.marker'), JSON.stringify({
|
|
61
|
+
BASH_DEFAULT_TIMEOUT_MS: process.env.BASH_DEFAULT_TIMEOUT_MS,
|
|
62
|
+
BASH_MAX_TIMEOUT_MS: process.env.BASH_MAX_TIMEOUT_MS,
|
|
63
|
+
}), 'utf8');
|
|
64
|
+
process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok' }) + '\\n');
|
|
65
|
+
process.exit(0);
|
|
66
|
+
`;
|
|
67
|
+
fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
|
|
68
|
+
return stubPath;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test('BASH_MAX_TIMEOUT_MS stays strictly below IDLE_OUTPUT_KILL_MS with real margin', () => {
|
|
72
|
+
expect(typeof BASH_DEFAULT_TIMEOUT_MS).toBe('number');
|
|
73
|
+
expect(typeof BASH_MAX_TIMEOUT_MS).toBe('number');
|
|
74
|
+
expect(typeof IDLE_OUTPUT_KILL_MS).toBe('number');
|
|
75
|
+
expect(BASH_DEFAULT_TIMEOUT_MS).toBeLessThanOrEqual(BASH_MAX_TIMEOUT_MS);
|
|
76
|
+
expect(BASH_MAX_TIMEOUT_MS).toBeLessThan(IDLE_OUTPUT_KILL_MS);
|
|
77
|
+
// Real margin, not a razor's edge.
|
|
78
|
+
expect(IDLE_OUTPUT_KILL_MS - BASH_MAX_TIMEOUT_MS).toBeGreaterThanOrEqual(60_000);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('executeJob spawns the executor child with BASH_DEFAULT_TIMEOUT_MS and BASH_MAX_TIMEOUT_MS set', async () => {
|
|
82
|
+
const mainCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-bash-timeout-main-'));
|
|
83
|
+
fs.mkdirSync(path.join(tmpHome, '.claude'), { recursive: true });
|
|
84
|
+
const runDir = fs.mkdtempSync(path.join(tmpHome, '.claude', 'sm-bash-timeout-run-'));
|
|
85
|
+
try {
|
|
86
|
+
const slug = `1097-test-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
87
|
+
const prdsDir = path.join(mainCwd, 'session-manager-operations', 'scheduler', 'prds');
|
|
88
|
+
fs.mkdirSync(prdsDir, { recursive: true });
|
|
89
|
+
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Verify the env vars are set.', 'utf8');
|
|
90
|
+
process.env.SM_CLAUDE_BIN = writeClaudeStub();
|
|
91
|
+
|
|
92
|
+
const job = { slug, cwd: mainCwd };
|
|
93
|
+
const result = await executeJob(job, runDir, mainCwd, () => Promise.resolve());
|
|
94
|
+
|
|
95
|
+
expect(result.exitCode).toBe(0);
|
|
96
|
+
const marker = JSON.parse(fs.readFileSync(path.join(mainCwd, 'env.marker'), 'utf8'));
|
|
97
|
+
expect(marker.BASH_DEFAULT_TIMEOUT_MS).toBe(String(BASH_DEFAULT_TIMEOUT_MS));
|
|
98
|
+
expect(marker.BASH_MAX_TIMEOUT_MS).toBe(String(BASH_MAX_TIMEOUT_MS));
|
|
99
|
+
} finally {
|
|
100
|
+
fs.rmSync(mainCwd, { recursive: true, force: true });
|
|
101
|
+
fs.rmSync(runDir, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
@@ -165,6 +165,47 @@ test('commitGuardVerdict: self-commit skip still works (regression check)', () =
|
|
|
165
165
|
expect(verdict).toBeNull();
|
|
166
166
|
});
|
|
167
167
|
|
|
168
|
+
test('commitGuardVerdict: ranInWorktree=true defeats siblingRunning — clean tree, no self-commit → silent_no_op (PRD 109 shape)', () => {
|
|
169
|
+
const verdict = commitGuardVerdict({
|
|
170
|
+
newlyDirty: [],
|
|
171
|
+
siblingRunning: true,
|
|
172
|
+
ranInWorktree: true,
|
|
173
|
+
jobSelfCommitted: false,
|
|
174
|
+
legitimateNoOp: false,
|
|
175
|
+
isFixPlanJob: false,
|
|
176
|
+
verifyResult: null,
|
|
177
|
+
});
|
|
178
|
+
expect(verdict).not.toBeNull();
|
|
179
|
+
expect(verdict.verdict).toBe('silent_no_op');
|
|
180
|
+
expect(verdict.downgradeTo).toBe('needs_review');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('commitGuardVerdict: identical inputs with ranInWorktree=false → no flag (legacy shared-tree behaviour unchanged)', () => {
|
|
184
|
+
const verdict = commitGuardVerdict({
|
|
185
|
+
newlyDirty: [],
|
|
186
|
+
siblingRunning: true,
|
|
187
|
+
ranInWorktree: false,
|
|
188
|
+
jobSelfCommitted: false,
|
|
189
|
+
legitimateNoOp: false,
|
|
190
|
+
isFixPlanJob: false,
|
|
191
|
+
verifyResult: null,
|
|
192
|
+
});
|
|
193
|
+
expect(verdict).toBeNull();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('commitGuardVerdict: ranInWorktree=true + siblingRunning=true but jobSelfCommitted=true → no flag (self-commit carve-out still applies)', () => {
|
|
197
|
+
const verdict = commitGuardVerdict({
|
|
198
|
+
newlyDirty: [],
|
|
199
|
+
siblingRunning: true,
|
|
200
|
+
ranInWorktree: true,
|
|
201
|
+
jobSelfCommitted: true,
|
|
202
|
+
legitimateNoOp: false,
|
|
203
|
+
isFixPlanJob: false,
|
|
204
|
+
verifyResult: null,
|
|
205
|
+
});
|
|
206
|
+
expect(verdict).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
168
209
|
test('commitGuardVerdict: carries forward a non-clean prior verdict as an annotation', () => {
|
|
169
210
|
const verifyResult = {
|
|
170
211
|
verdict: 'transcript_errors',
|
|
@@ -69,3 +69,13 @@ test('free is net of held slots — a chat run shrinks it without changing cap',
|
|
|
69
69
|
expect(payload.effectiveConcurrency).toEqual({ cap: 5, free: 4, source: 'env' });
|
|
70
70
|
sessionSlots.release(token);
|
|
71
71
|
});
|
|
72
|
+
|
|
73
|
+
test('a row-level gateOutcome field survives the payload build unchanged (pass-through only, PRD 1109)', () => {
|
|
74
|
+
const state = fakeState();
|
|
75
|
+
state.jobs = [
|
|
76
|
+
{ slug: 'a', status: 'failed', gateOutcome: 'never_ran' },
|
|
77
|
+
{ slug: 'b', status: 'completed', gateOutcome: 'passed' },
|
|
78
|
+
];
|
|
79
|
+
const payload = buildScheduleStatePayload(state);
|
|
80
|
+
expect(payload.jobs.map((j) => j.gateOutcome)).toEqual(['never_ran', 'passed']);
|
|
81
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-foreign-wip-manifest.test.cjs — capDirtyPaths/buildForeignWipSection,
|
|
3
|
+
* the pure helpers behind the pre-run foreign-WIP manifest injected into the
|
|
4
|
+
* executor prompt (see the "starry-night-ships PRD 148" postmortem: a job
|
|
5
|
+
* running in a shared or WIP-carrying tree must be told explicitly which
|
|
6
|
+
* paths it does not own, rather than having to bisect by content).
|
|
7
|
+
*
|
|
8
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
import { test, expect } from 'vitest';
|
|
14
|
+
const {
|
|
15
|
+
capDirtyPaths,
|
|
16
|
+
buildForeignWipSection,
|
|
17
|
+
PRE_RUN_DIRTY_PATHS_CAP,
|
|
18
|
+
FOREIGN_WIP_DELIMITER,
|
|
19
|
+
FOREIGN_WIP_END_DELIMITER,
|
|
20
|
+
} = require('../scheduler.cjs');
|
|
21
|
+
|
|
22
|
+
test('capDirtyPaths: [] for null/undefined/empty input', () => {
|
|
23
|
+
expect(capDirtyPaths(null)).toEqual([]);
|
|
24
|
+
expect(capDirtyPaths(undefined)).toEqual([]);
|
|
25
|
+
expect(capDirtyPaths([])).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('capDirtyPaths: returns the list unchanged when under the cap', () => {
|
|
29
|
+
expect(capDirtyPaths(['a.js', 'b.js'])).toEqual(['a.js', 'b.js']);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('capDirtyPaths: truncates at the cap and appends a "+N more" marker', () => {
|
|
33
|
+
const many = Array.from({ length: PRE_RUN_DIRTY_PATHS_CAP + 7 }, (_, i) => `file-${i}.txt`);
|
|
34
|
+
const capped = capDirtyPaths(many);
|
|
35
|
+
expect(capped).toHaveLength(PRE_RUN_DIRTY_PATHS_CAP + 1);
|
|
36
|
+
expect(capped.slice(0, PRE_RUN_DIRTY_PATHS_CAP)).toEqual(many.slice(0, PRE_RUN_DIRTY_PATHS_CAP));
|
|
37
|
+
expect(capped[PRE_RUN_DIRTY_PATHS_CAP]).toBe('+7 more');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('buildForeignWipSection: "" when both lists are empty/absent (clean spawn) — zero prompt tokens added', () => {
|
|
41
|
+
expect(buildForeignWipSection({})).toBe('');
|
|
42
|
+
expect(buildForeignWipSection({ preRunDirtyPaths: [], carriedPaths: [] })).toBe('');
|
|
43
|
+
expect(buildForeignWipSection()).toBe('');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('buildForeignWipSection: shared dirty tree emits the manifest with the delimiter, paths verbatim, and do-not-touch wording', () => {
|
|
47
|
+
const section = buildForeignWipSection({ preRunDirtyPaths: ['src/foo.ts', 'src/bar.ts'] });
|
|
48
|
+
expect(section).toContain(FOREIGN_WIP_DELIMITER);
|
|
49
|
+
expect(section).toContain(FOREIGN_WIP_END_DELIMITER);
|
|
50
|
+
expect(section).toContain('src/foo.ts');
|
|
51
|
+
expect(section).toContain('src/bar.ts');
|
|
52
|
+
expect(section).toMatch(/SHARED working tree/);
|
|
53
|
+
expect(section).toMatch(/not this job's work/i);
|
|
54
|
+
expect(section).toMatch(/do not stage, commit, revert, or stash/i);
|
|
55
|
+
expect(section).toMatch(/not this job's regression/i);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('buildForeignWipSection: carriedPaths (PRD 1094 base WIP) emits carry-over wording, not the shared-tree wording', () => {
|
|
59
|
+
const section = buildForeignWipSection({ carriedPaths: ['config/settings.json'] });
|
|
60
|
+
expect(section).toContain('config/settings.json');
|
|
61
|
+
expect(section).toMatch(/isolated git worktree/i);
|
|
62
|
+
expect(section).toMatch(/authoritative copy.*main tree/i);
|
|
63
|
+
expect(section).not.toMatch(/SHARED working tree/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('buildForeignWipSection: carriedPaths takes precedence over preRunDirtyPaths when both are somehow present', () => {
|
|
67
|
+
const section = buildForeignWipSection({
|
|
68
|
+
carriedPaths: ['carried.ts'],
|
|
69
|
+
preRunDirtyPaths: ['dirty.ts'],
|
|
70
|
+
});
|
|
71
|
+
expect(section).toContain('carried.ts');
|
|
72
|
+
expect(section).not.toContain('dirty.ts');
|
|
73
|
+
expect(section).toMatch(/isolated git worktree/i);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('buildForeignWipSection: isolated-clean (worktree ok, no carried WIP) emits nothing', () => {
|
|
77
|
+
expect(buildForeignWipSection({ carriedPaths: [] })).toBe('');
|
|
78
|
+
});
|