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.
Files changed (164) hide show
  1. package/dist/assets/{AgentLibrary-CzQqcObq.js → AgentLibrary-B2ie8bbw.js} +2 -2
  2. package/dist/assets/{DataModel-Bj_WlLz8.js → DataModel-BIJPYw32.js} +1 -1
  3. package/dist/assets/{History-DnSi_OHm.js → History-CeY6dk9S.js} +2 -2
  4. package/dist/assets/{Hooks-0BB0dp3S.js → Hooks-BFH2ocKg.js} +2 -2
  5. package/dist/assets/{HostBilko-DHpwwsLQ.js → HostBilko-36gj9wLz.js} +1 -1
  6. package/dist/assets/{Library-CaJVqVvi.js → Library-C-hBct39.js} +1 -1
  7. package/dist/assets/{ListDetail-C1W2HmC2.js → ListDetail-CNq64VWV.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-5Ob9FW3z.js → MarkdownEditor-Bh3qt5-1.js} +1 -1
  9. package/dist/assets/{McpServers-JxCSfm1S.js → McpServers-DpGN0oyz.js} +1 -1
  10. package/dist/assets/{Memory-BDeqlqwH.js → Memory-D59hUjC4.js} +6 -6
  11. package/dist/assets/{Panel-Dh9ZHuEj.js → Panel-DCgbaoci.js} +1 -1
  12. package/dist/assets/{Permissions-DXy-CbEY.js → Permissions-DAmQ0DYV.js} +2 -2
  13. package/dist/assets/{Plugins-_n1Iuc8T.js → Plugins-Dyfgn6Is.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-BP_evfxE.js → ProvenanceBadge-BiYhPO1U.js} +1 -1
  15. package/dist/assets/SaveBar-RV7B6sOh.js +1 -0
  16. package/dist/assets/Scheduler-BPaNqx1b.js +14 -0
  17. package/dist/assets/{ScopeSwitcher-CAWzM6RI.js → ScopeSwitcher-P4mdLGNU.js} +1 -1
  18. package/dist/assets/{Settings-DRRozLyT.js → Settings-BL4vf5aX.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-DGHDWlz4.js → SkillReferenceGraph-BRBDyi1_.js} +1 -1
  20. package/dist/assets/{Skills-D8L66eiX.js → Skills-BV08gDUH.js} +2 -2
  21. package/dist/assets/{SystemPrompt-CYtUsonD.js → SystemPrompt-CLftSsDw.js} +1 -1
  22. package/dist/assets/TagLibrary-Bp8jGsd5.js +1 -0
  23. package/dist/assets/{TiptapBody-B2hRgbPE.js → TiptapBody-jCpuB6E5.js} +1 -1
  24. package/dist/assets/{Toggle-BTwsbxam.js → Toggle-D2paA1xf.js} +1 -1
  25. package/dist/assets/{index-DijufvkJ.js → index-BDRSqBl3.js} +704 -704
  26. package/dist/assets/{index-CMLnzdZC.css → index-CYhdtisq.css} +1 -1
  27. package/dist/assets/{settingsSchema-D6wzxAi6.js → settingsSchema-6IOLjZZN.js} +1 -1
  28. package/dist/index.html +2 -2
  29. package/package.json +8 -2
  30. package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
  31. package/scripts/lib/activeSessions.cjs +116 -6
  32. package/scripts/project-pages-logic/dist/logic.cjs +4709 -0
  33. package/scripts/render-project-pages/dist/renderer.cjs +18900 -0
  34. package/scripts/render-project-pages.cjs +70 -0
  35. package/scripts/scheduler-mcp-server.cjs +269 -96
  36. package/scripts/validate-project-pages-summary.cjs +62 -0
  37. package/src/main/__tests__/agentModelResolve.test.cjs +66 -0
  38. package/src/main/__tests__/epicStatusMirror.test.cjs +110 -0
  39. package/src/main/__tests__/health-delegation-chain.test.cjs +106 -0
  40. package/src/main/__tests__/prdAdminRoutes.test.cjs +295 -0
  41. package/src/main/__tests__/prdAgentType.test.cjs +103 -0
  42. package/src/main/__tests__/prdCreate.test.cjs +247 -0
  43. package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +117 -0
  44. package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +108 -0
  45. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +485 -0
  46. package/src/main/__tests__/projectPages.test.cjs +73 -1
  47. package/src/main/__tests__/rcaReport.test.cjs +54 -0
  48. package/src/main/__tests__/runVerify.test.cjs +94 -0
  49. package/src/main/__tests__/scheduler-autofix-select.test.cjs +58 -3
  50. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +103 -0
  51. package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +41 -0
  52. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +10 -0
  53. package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +78 -0
  54. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +242 -0
  55. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +31 -0
  56. package/src/main/__tests__/scheduler-launch-failure.test.cjs +201 -0
  57. package/src/main/__tests__/scheduler-leftover-fields.test.cjs +52 -0
  58. package/src/main/__tests__/scheduler-looks-done.test.cjs +241 -0
  59. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +135 -0
  60. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +222 -0
  61. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +207 -1
  62. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +212 -0
  63. package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +185 -0
  64. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +194 -0
  65. package/src/main/__tests__/seedAgentPersonas.test.cjs +75 -14
  66. package/src/main/__tests__/seedSchedulerMcp.test.cjs +66 -0
  67. package/src/main/__tests__/uniquePrdNumbers.test.cjs +14 -5
  68. package/src/main/bilkoHost.cjs +4 -3
  69. package/src/main/chatRunner.cjs +6 -1
  70. package/src/main/config.cjs +25 -33
  71. package/src/main/health.cjs +153 -2
  72. package/src/main/index.cjs +64 -5
  73. package/src/main/ipcSchemas.cjs +69 -1
  74. package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +179 -0
  75. package/src/main/lib/__tests__/childWithLog.test.cjs +141 -0
  76. package/src/main/lib/__tests__/delegationReadiness.test.cjs +391 -42
  77. package/src/main/lib/__tests__/ephemeralCwd.test.cjs +91 -0
  78. package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +5 -3
  79. package/src/main/lib/__tests__/fixChainDepth.test.cjs +40 -0
  80. package/src/main/lib/__tests__/gitWorktree.test.cjs +290 -5
  81. package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +107 -0
  82. package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +153 -0
  83. package/src/main/lib/__tests__/jobWorktree.test.cjs +6 -4
  84. package/src/main/lib/__tests__/landedSinceRun.test.cjs +73 -0
  85. package/src/main/lib/__tests__/launchFailure.test.cjs +220 -0
  86. package/src/main/lib/__tests__/loadGate.test.cjs +159 -0
  87. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +102 -0
  88. package/src/main/lib/__tests__/opsOwnership.test.cjs +7 -0
  89. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +151 -0
  90. package/src/main/lib/__tests__/opsRootResolve.test.cjs +149 -0
  91. package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +82 -0
  92. package/src/main/lib/__tests__/projectRootResolve.test.cjs +148 -0
  93. package/src/main/lib/__tests__/queueHealth.test.cjs +58 -0
  94. package/src/main/lib/__tests__/quietMachineLease.test.cjs +39 -0
  95. package/src/main/lib/__tests__/reaperHelpers.test.cjs +133 -0
  96. package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +19 -9
  97. package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +213 -0
  98. package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +125 -0
  99. package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +127 -0
  100. package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +109 -0
  101. package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +71 -0
  102. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +217 -0
  103. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +350 -0
  104. package/src/main/lib/activeIndexMerge.cjs +15 -0
  105. package/src/main/lib/activeIndexRebuild.cjs +133 -0
  106. package/src/main/lib/agentModelResolve.cjs +58 -0
  107. package/src/main/lib/buildTarget.cjs +3 -2
  108. package/src/main/lib/childWithLog.cjs +69 -2
  109. package/src/main/lib/claudeBin.cjs +54 -1
  110. package/src/main/lib/crossProjectFeedback.cjs +8 -1
  111. package/src/main/lib/definitionOfDone.cjs +3 -2
  112. package/src/main/lib/delegationReadiness.cjs +514 -26
  113. package/src/main/lib/ephemeralCwd.cjs +78 -0
  114. package/src/main/lib/epicDelegationStats.cjs +2 -1
  115. package/src/main/lib/epicMint.cjs +17 -1
  116. package/src/main/lib/epicStatusMirror.cjs +95 -0
  117. package/src/main/lib/epicValidationHook.cjs +2 -1
  118. package/src/main/lib/epicWorktreeMint.cjs +5 -2
  119. package/src/main/lib/fixChainDepth.cjs +45 -0
  120. package/src/main/lib/gitWorktree.cjs +520 -21
  121. package/src/main/lib/jobWorktree.cjs +2 -0
  122. package/src/main/lib/landedSinceRun.cjs +55 -0
  123. package/src/main/lib/launchFailure.cjs +357 -0
  124. package/src/main/lib/loadGate.cjs +134 -0
  125. package/src/main/lib/mcpToolCatalog.cjs +370 -0
  126. package/src/main/lib/opsErrorLog.cjs +12 -1
  127. package/src/main/lib/opsOwnership.cjs +106 -0
  128. package/src/main/lib/prdAdminRoutes.cjs +43 -3
  129. package/src/main/lib/prdAgentType.cjs +84 -0
  130. package/src/main/lib/prdCreate.cjs +103 -15
  131. package/src/main/lib/prdDeclaredPaths.cjs +70 -0
  132. package/src/main/lib/prdFrontmatter.cjs +17 -3
  133. package/src/main/lib/prdLocations.cjs +13 -6
  134. package/src/main/lib/projectHomeAdminRoutes.cjs +402 -0
  135. package/src/main/lib/projectPageSummarySchema.cjs +181 -0
  136. package/src/main/lib/projectRootResolve.cjs +134 -0
  137. package/src/main/lib/promptSessionSchema.cjs +7 -0
  138. package/src/main/lib/queueHealth.cjs +38 -0
  139. package/src/main/lib/queueStore.cjs +40 -7
  140. package/src/main/lib/quietMachineLease.cjs +48 -0
  141. package/src/main/lib/rcaReport.cjs +54 -4
  142. package/src/main/lib/reaperHelpers.cjs +64 -1
  143. package/src/main/lib/scheduleJobSchema.cjs +31 -0
  144. package/src/main/lib/scheduleJobTransitions.cjs +6 -2
  145. package/src/main/lib/schedulerBatch.cjs +301 -55
  146. package/src/main/lib/schedulerConfig.cjs +99 -0
  147. package/src/main/projectBrief.cjs +3 -2
  148. package/src/main/projectPages.cjs +162 -3
  149. package/src/main/promptSessionTranscript.cjs +0 -0
  150. package/src/main/pty.cjs +5 -0
  151. package/src/main/queueOps.cjs +15 -8
  152. package/src/main/runVerify.cjs +50 -9
  153. package/src/main/scheduler/prdParser.cjs +18 -1
  154. package/src/main/scheduler.cjs +1701 -130
  155. package/src/main/seedAgentPersonas.cjs +62 -21
  156. package/src/main/seedSchedulerMcp.cjs +58 -4
  157. package/src/main/templates/project-pages-catalog.json +741 -0
  158. package/src/main/templates/project-pages-pipeline.md +417 -0
  159. package/src/preload/api.d.ts +187 -3
  160. package/src/preload/index.cjs +9 -0
  161. package/src/seed/agents/project-home-builder.md +59 -0
  162. package/dist/assets/SaveBar-D-gCUx4n.js +0 -1
  163. package/dist/assets/Scheduler-Bpd4OGju.js +0 -14
  164. 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: 2 is now ALSO eligible', () => {
271
- const jobs = [makeJob({ slug: '05-fix-foo', investigationDepth: 2 })];
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 or 2 is not capped, depth 3 is', () => {
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
+ });