claude-code-session-manager 0.98.0 → 0.100.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 (431) hide show
  1. package/README.md +3 -3
  2. package/bin/cli.cjs +16 -4
  3. package/bin/node-floor.cjs +41 -0
  4. package/dist/assets/{DataModel-DCEgMTjH.js → DataModel-HdR0AxFq.js} +1 -1
  5. package/dist/assets/{History-Bfdoywlh.js → History-Dr4Jr_5z.js} +2 -2
  6. package/dist/assets/{Hooks-CKDJjTJV.js → Hooks-NhrvH3JP.js} +3 -3
  7. package/dist/assets/{Library-7akfgy67.js → Library-Dv_EtIMr.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-DDesiqet.js → MarkdownEditor-DTKDWV_n.js} +1 -1
  9. package/dist/assets/{McpServers-D4lZtXof.js → McpServers-M034G9B3.js} +2 -2
  10. package/dist/assets/{Memory-CeI8G1w5.js → Memory-DzF1B4gx.js} +6 -6
  11. package/dist/assets/{Permissions-BGleLja4.js → Permissions-DrgbSGWl.js} +3 -3
  12. package/dist/assets/{Plugins-P4eCXy_S.js → Plugins-Bd3ef35w.js} +2 -2
  13. package/dist/assets/{ProvenanceBadge-BFczzI1B.js → ProvenanceBadge-DcBHT1Iy.js} +1 -1
  14. package/dist/assets/{SaveBar-l9jnEMES.js → SaveBar-rimJem-Y.js} +1 -1
  15. package/dist/assets/Scheduler-DjgFJl2w.js +16 -0
  16. package/dist/assets/{ScopeSwitcher-CjJi8ST5.js → ScopeSwitcher-CnAzTBrl.js} +1 -1
  17. package/dist/assets/Settings-Dc1F_sSg.js +3 -0
  18. package/dist/assets/{SkillReferenceGraph-CjbO01Sg.js → SkillReferenceGraph-B9aZDecM.js} +1 -1
  19. package/dist/assets/{Skills-BxZ9_fCR.js → Skills-DRXd18gu.js} +2 -2
  20. package/dist/assets/{SystemPrompt-D8vfFb8b.js → SystemPrompt-ahy5-Fwe.js} +1 -1
  21. package/dist/assets/{TagLibrary-2vzUo7YL.js → TagLibrary-C_tToDeL.js} +1 -1
  22. package/dist/assets/{TiptapBody-B8ZfbK90.js → TiptapBody-DvdefS6K.js} +1 -1
  23. package/dist/assets/{Toggle-BbW4nGGf.js → Toggle-6Jl5Njc6.js} +1 -1
  24. package/dist/assets/{index-CKH5Uxik.css → index-Bta-hwud.css} +1 -1
  25. package/dist/assets/{index-Cd98Q1lD.js → index-vx8O73l8.js} +497 -502
  26. package/dist/assets/{settingsSchema-IhUfucCF.js → settingsSchema-D6iRguNV.js} +1 -1
  27. package/dist/index.html +2 -2
  28. package/package.json +25 -29
  29. package/plugins/CLAUDE.md +6 -6
  30. package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +21 -19
  31. package/plugins/session-manager-dev/skills/builder/SKILL.md +3 -3
  32. package/plugins/session-manager-dev/skills/develop/SKILL.md +254 -510
  33. package/plugins/session-manager-dev/skills/develop/standards.md +17 -22
  34. package/scripts/README.md +3 -11
  35. package/scripts/audit-ops-hygiene.cjs +3 -3
  36. package/scripts/hooks/lib/guard-prd-writes-policy.cjs +1 -1
  37. package/scripts/hooks/lib/guard-self-schedule-policy.cjs +1 -1
  38. package/scripts/mint-epic.cjs +2 -3
  39. package/scripts/scheduler-mcp-server.cjs +36 -7
  40. package/src/main/agentLibrary.cjs +22 -2
  41. package/src/main/build-info.json +4 -4
  42. package/src/main/config.cjs +41 -38
  43. package/src/main/docEdit.cjs +4 -1
  44. package/src/main/files.cjs +2 -5
  45. package/src/main/health.cjs +1 -1
  46. package/src/main/index.cjs +23 -10
  47. package/src/main/ipcSchemas.cjs +28 -35
  48. package/src/main/lib/agentPersonaSchema.cjs +13 -2
  49. package/src/main/lib/atomicFs.cjs +116 -0
  50. package/src/main/lib/branchSweep.cjs +13 -12
  51. package/src/main/lib/buildTarget.cjs +3 -3
  52. package/src/main/lib/claudeCliCaps.cjs +129 -0
  53. package/src/main/lib/credentials.cjs +2 -4
  54. package/src/main/lib/crossProjectFeedback.cjs +3 -3
  55. package/src/main/lib/cwdClassify.cjs +15 -3
  56. package/src/main/lib/definitionOfDone.cjs +207 -125
  57. package/src/main/lib/dodDrainHook.cjs +11 -5
  58. package/src/main/lib/epicMint.cjs +2 -4
  59. package/src/main/lib/epicStatusMirror.cjs +8 -10
  60. package/src/main/lib/epicWorktreeProjectConfig.cjs +2 -4
  61. package/src/main/lib/gateAuthority.cjs +96 -0
  62. package/src/main/lib/gitExec.cjs +69 -0
  63. package/src/main/lib/gitWorktree.cjs +656 -92
  64. package/src/main/lib/instanceLock.cjs +5 -8
  65. package/src/main/lib/launchFailure.cjs +2 -3
  66. package/src/main/lib/macroLibrary.cjs +386 -0
  67. package/src/main/lib/mcpToolCatalog.cjs +26 -17
  68. package/src/main/lib/needsReviewLedger.cjs +1 -1
  69. package/src/main/lib/opsErrorLog.cjs +11 -1
  70. package/src/main/lib/opsOwnership.cjs +0 -5
  71. package/src/main/lib/pidAlive.cjs +32 -0
  72. package/src/main/lib/prdCreate.cjs +121 -12
  73. package/src/main/lib/prdDisposition.cjs +2 -2
  74. package/src/main/lib/prdGateFiles.cjs +284 -0
  75. package/src/main/lib/prdLocations.cjs +61 -0
  76. package/src/main/lib/prdMigration.cjs +2 -3
  77. package/src/main/lib/prdSizing.cjs +2 -1
  78. package/src/main/lib/promptSessionSchema.cjs +10 -2
  79. package/src/main/lib/rcaReport.cjs +4 -6
  80. package/src/main/lib/reaperHelpers.cjs +8 -1
  81. package/src/main/lib/reservationExpiry.cjs +6 -1
  82. package/src/main/lib/reviewNotice.cjs +263 -0
  83. package/src/main/lib/runClaudeP.cjs +4 -1
  84. package/src/main/lib/schedulerPaths.cjs +22 -2
  85. package/src/main/lib/sessionSlots.cjs +2 -4
  86. package/src/main/lib/shippedPersonaSeeds.cjs +179 -0
  87. package/src/main/lib/timeoutShim.cjs +123 -0
  88. package/src/main/lib/timeoutShimScript.cjs +275 -0
  89. package/src/main/lib/upgradeDrain.cjs +4 -10
  90. package/src/main/lib/watchdogHelpers.cjs +12 -25
  91. package/src/main/lib/workTypeLibrary.cjs +7 -1
  92. package/src/main/promptSessionEvents.cjs +37 -13
  93. package/src/main/scheduler.cjs +910 -198
  94. package/src/main/seedAgentPersonas.cjs +278 -23
  95. package/src/main/supervisor.cjs +4 -1
  96. package/src/main/templates/PRD_AUTHORING.md +126 -406
  97. package/src/preload/__tests__/preload-surface.test.cjs +68 -0
  98. package/src/preload/api.d.ts +41 -94
  99. package/src/preload/index.cjs +14 -9
  100. package/src/seed/agents/architect.md +1 -0
  101. package/src/seed/agents/dev-lead.md +17 -18
  102. package/src/seed/agents/project-home-builder.md +1 -0
  103. package/src/seed/agents/validator.md +10 -4
  104. package/dist/assets/HostBilko-CDVHE0hT.js +0 -1
  105. package/dist/assets/Scheduler-C_phpeUS.js +0 -16
  106. package/dist/assets/Settings-BIwZ3FGQ.js +0 -3
  107. package/src/main/__tests__/activeIndexMerge.test.cjs +0 -235
  108. package/src/main/__tests__/agentEffortResolve.test.cjs +0 -117
  109. package/src/main/__tests__/agentLibrary.test.cjs +0 -276
  110. package/src/main/__tests__/agentModelResolve.test.cjs +0 -311
  111. package/src/main/__tests__/agentOverlayWrite.test.cjs +0 -95
  112. package/src/main/__tests__/bilkoHost-deriveSlug.test.cjs +0 -26
  113. package/src/main/__tests__/bilkoHost-integration.test.cjs +0 -118
  114. package/src/main/__tests__/bilkoHostCore.test.cjs +0 -72
  115. package/src/main/__tests__/broadcastCoalescer.test.cjs +0 -122
  116. package/src/main/__tests__/chat-cancel-terminal.test.cjs +0 -120
  117. package/src/main/__tests__/chat-dead-channels.test.cjs +0 -63
  118. package/src/main/__tests__/chat-exit-close-race.test.cjs +0 -146
  119. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +0 -139
  120. package/src/main/__tests__/chat-preamble-anchors.test.cjs +0 -100
  121. package/src/main/__tests__/chat-queue.test.cjs +0 -97
  122. package/src/main/__tests__/chat-stop-signal.test.cjs +0 -89
  123. package/src/main/__tests__/chatRunner-epic-worktree-execcwd.test.cjs +0 -125
  124. package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +0 -252
  125. package/src/main/__tests__/classifyPromptTicket.test.cjs +0 -101
  126. package/src/main/__tests__/classifyTranscriptLine.test.cjs +0 -201
  127. package/src/main/__tests__/computeDepHistorySatisfaction.test.cjs +0 -66
  128. package/src/main/__tests__/config-readText-bounded.test.cjs +0 -84
  129. package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +0 -59
  130. package/src/main/__tests__/crossProjectFeedback.test.cjs +0 -334
  131. package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +0 -161
  132. package/src/main/__tests__/dep-orphan-archive-health.test.cjs +0 -77
  133. package/src/main/__tests__/develop-skill-failure-modes.test.cjs +0 -70
  134. package/src/main/__tests__/docEdit.test.cjs +0 -244
  135. package/src/main/__tests__/dod-batchkey.test.cjs +0 -183
  136. package/src/main/__tests__/dod-drain-hook.test.cjs +0 -302
  137. package/src/main/__tests__/dod-report.test.cjs +0 -304
  138. package/src/main/__tests__/dod-reverify.test.cjs +0 -285
  139. package/src/main/__tests__/epicContextDigest.test.cjs +0 -174
  140. package/src/main/__tests__/epicMint.test.cjs +0 -332
  141. package/src/main/__tests__/epicMintTelemetryTap.test.cjs +0 -64
  142. package/src/main/__tests__/epicStatusMirror.test.cjs +0 -110
  143. package/src/main/__tests__/epicValidationHook.test.cjs +0 -291
  144. package/src/main/__tests__/exchanges.test.cjs +0 -122
  145. package/src/main/__tests__/exchangesPromptId.test.cjs +0 -61
  146. package/src/main/__tests__/extractJson.test.cjs +0 -51
  147. package/src/main/__tests__/files-reject-credentials.test.cjs +0 -40
  148. package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +0 -556
  149. package/src/main/__tests__/flatPrdTickSweep.test.cjs +0 -110
  150. package/src/main/__tests__/health-build-freshness.test.cjs +0 -39
  151. package/src/main/__tests__/health-claude-md-budget.test.cjs +0 -57
  152. package/src/main/__tests__/health-credentials.test.cjs +0 -81
  153. package/src/main/__tests__/health-delegation-chain.test.cjs +0 -124
  154. package/src/main/__tests__/health-per-project-stall.test.cjs +0 -84
  155. package/src/main/__tests__/health-prd-migration.test.cjs +0 -37
  156. package/src/main/__tests__/health-queue-dispatch.test.cjs +0 -135
  157. package/src/main/__tests__/health-starve-escalation.test.cjs +0 -94
  158. package/src/main/__tests__/health-tick-liveness.test.cjs +0 -179
  159. package/src/main/__tests__/health-usage-poller.test.cjs +0 -144
  160. package/src/main/__tests__/health-worktree-cap-blocked.test.cjs +0 -65
  161. package/src/main/__tests__/heapSnapshot.test.cjs +0 -121
  162. package/src/main/__tests__/historyAggregatorIntraday.test.cjs +0 -313
  163. package/src/main/__tests__/historyDashboard.test.cjs +0 -163
  164. package/src/main/__tests__/historyRollup.test.cjs +0 -333
  165. package/src/main/__tests__/intradayRefresh.test.cjs +0 -39
  166. package/src/main/__tests__/ipcSchemas-dependsOn.test.cjs +0 -39
  167. package/src/main/__tests__/kg-augment.test.cjs +0 -195
  168. package/src/main/__tests__/loadGateDetailTick.test.cjs +0 -31
  169. package/src/main/__tests__/machineProfile.test.cjs +0 -152
  170. package/src/main/__tests__/mcpStatus.test.cjs +0 -61
  171. package/src/main/__tests__/memoryAggregate.test.cjs +0 -109
  172. package/src/main/__tests__/memoryStale.test.cjs +0 -88
  173. package/src/main/__tests__/needsReviewLedger.test.cjs +0 -162
  174. package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +0 -25
  175. package/src/main/__tests__/opsErrorLog.test.cjs +0 -109
  176. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +0 -173
  177. package/src/main/__tests__/personaMerge.test.cjs +0 -169
  178. package/src/main/__tests__/planValidator.test.cjs +0 -125
  179. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +0 -176
  180. package/src/main/__tests__/prd-group-allocator.test.cjs +0 -119
  181. package/src/main/__tests__/prdAdminRouteParity.test.cjs +0 -70
  182. package/src/main/__tests__/prdAdminRoutes.test.cjs +0 -718
  183. package/src/main/__tests__/prdAgentType.test.cjs +0 -103
  184. package/src/main/__tests__/prdAuthoringSeed.test.cjs +0 -39
  185. package/src/main/__tests__/prdCreate.test.cjs +0 -979
  186. package/src/main/__tests__/prdCreateAdoption.test.cjs +0 -205
  187. package/src/main/__tests__/prdCreateDisposition.test.cjs +0 -201
  188. package/src/main/__tests__/prdCreatePlanId.test.cjs +0 -132
  189. package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +0 -117
  190. package/src/main/__tests__/prdFrontmatterDependsOn.test.cjs +0 -136
  191. package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +0 -125
  192. package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +0 -108
  193. package/src/main/__tests__/prdLocations.test.cjs +0 -195
  194. package/src/main/__tests__/prdLocationsArchived.test.cjs +0 -201
  195. package/src/main/__tests__/prdMigration.test.cjs +0 -349
  196. package/src/main/__tests__/prdMigrationLegacyAdopt.test.cjs +0 -91
  197. package/src/main/__tests__/prdParserHighWater.test.cjs +0 -74
  198. package/src/main/__tests__/prdParserSourcePromptId.test.cjs +0 -65
  199. package/src/main/__tests__/prdSetDisposition.test.cjs +0 -222
  200. package/src/main/__tests__/prdSizing.test.cjs +0 -106
  201. package/src/main/__tests__/prdSourcePromptIdBackfill.test.cjs +0 -118
  202. package/src/main/__tests__/prdUpdateDependsOn.test.cjs +0 -160
  203. package/src/main/__tests__/proc-role-env.test.cjs +0 -125
  204. package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +0 -304
  205. package/src/main/__tests__/procname-sm-processes.test.cjs +0 -127
  206. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +0 -177
  207. package/src/main/__tests__/projectPages.test.cjs +0 -137
  208. package/src/main/__tests__/promptSessionEvents.test.cjs +0 -234
  209. package/src/main/__tests__/promptSessionSchema.test.cjs +0 -101
  210. package/src/main/__tests__/promptSessionTranscript.test.cjs +0 -0
  211. package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +0 -159
  212. package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +0 -282
  213. package/src/main/__tests__/pty-session-open-telemetry.test.cjs +0 -96
  214. package/src/main/__tests__/pty-write-result.test.cjs +0 -46
  215. package/src/main/__tests__/queue-health-verdict.test.cjs +0 -170
  216. package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +0 -286
  217. package/src/main/__tests__/queue-starvation-per-project.test.cjs +0 -147
  218. package/src/main/__tests__/queueHistory.test.cjs +0 -355
  219. package/src/main/__tests__/queueOps-interactive-ac-lint.test.cjs +0 -65
  220. package/src/main/__tests__/queueOpsArchiveDestination.test.cjs +0 -65
  221. package/src/main/__tests__/queueOpsAutoArchive.test.cjs +0 -211
  222. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +0 -128
  223. package/src/main/__tests__/rcaReport.test.cjs +0 -266
  224. package/src/main/__tests__/reconcileFlatPrdSweep.test.cjs +0 -119
  225. package/src/main/__tests__/reconcileTiming.test.cjs +0 -135
  226. package/src/main/__tests__/runLogRetention.test.cjs +0 -489
  227. package/src/main/__tests__/runVerify-atomic-verdicts.test.cjs +0 -26
  228. package/src/main/__tests__/runVerify-blocked-by-foreign-wip.test.cjs +0 -58
  229. package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +0 -191
  230. package/src/main/__tests__/runVerify-policy-denial.test.cjs +0 -89
  231. package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +0 -225
  232. package/src/main/__tests__/runVerify.test.cjs +0 -1784
  233. package/src/main/__tests__/scheduleJobSchema.test.cjs +0 -127
  234. package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +0 -65
  235. package/src/main/__tests__/scheduleJobTransitions.test.cjs +0 -277
  236. package/src/main/__tests__/scheduleJobTransitionsGrep.test.cjs +0 -59
  237. package/src/main/__tests__/scheduleJobTransitionsTelemetryTap.test.cjs +0 -72
  238. package/src/main/__tests__/scheduler-admin-routes.test.cjs +0 -199
  239. package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +0 -143
  240. package/src/main/__tests__/scheduler-already-satisfied-on-main.test.cjs +0 -105
  241. package/src/main/__tests__/scheduler-archive-completed-prd.test.cjs +0 -102
  242. package/src/main/__tests__/scheduler-archived-twin-guard.test.cjs +0 -155
  243. package/src/main/__tests__/scheduler-autofix-outcome.test.cjs +0 -188
  244. package/src/main/__tests__/scheduler-autofix-select.test.cjs +0 -439
  245. package/src/main/__tests__/scheduler-autopromote.test.cjs +0 -51
  246. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +0 -101
  247. package/src/main/__tests__/scheduler-blocked-by-foreign-wip.test.cjs +0 -107
  248. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +0 -153
  249. package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +0 -121
  250. package/src/main/__tests__/scheduler-clear-queue-history.test.cjs +0 -134
  251. package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +0 -244
  252. package/src/main/__tests__/scheduler-committed-in-window.test.cjs +0 -182
  253. package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +0 -43
  254. package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +0 -168
  255. package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +0 -58
  256. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +0 -81
  257. package/src/main/__tests__/scheduler-epic-digest.test.cjs +0 -227
  258. package/src/main/__tests__/scheduler-failed-autoreset.test.cjs +0 -121
  259. package/src/main/__tests__/scheduler-finalize-dispatch-guards.test.cjs +0 -229
  260. package/src/main/__tests__/scheduler-find-prd-dir.test.cjs +0 -75
  261. package/src/main/__tests__/scheduler-fix-plan-path.test.cjs +0 -119
  262. package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +0 -46
  263. package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +0 -78
  264. package/src/main/__tests__/scheduler-gate-shadow.test.cjs +0 -119
  265. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +0 -390
  266. package/src/main/__tests__/scheduler-heal-refusal.test.cjs +0 -61
  267. package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +0 -80
  268. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +0 -323
  269. package/src/main/__tests__/scheduler-integration-failure-stamp.test.cjs +0 -41
  270. package/src/main/__tests__/scheduler-investigation-clean-skip.test.cjs +0 -63
  271. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +0 -123
  272. package/src/main/__tests__/scheduler-job-budget.test.cjs +0 -172
  273. package/src/main/__tests__/scheduler-job-overrun.test.cjs +0 -175
  274. package/src/main/__tests__/scheduler-launch-failure.test.cjs +0 -199
  275. package/src/main/__tests__/scheduler-leftover-fields.test.cjs +0 -52
  276. package/src/main/__tests__/scheduler-leftover-quarantine.test.cjs +0 -199
  277. package/src/main/__tests__/scheduler-looks-done.test.cjs +0 -537
  278. package/src/main/__tests__/scheduler-manual-pause.test.cjs +0 -118
  279. package/src/main/__tests__/scheduler-mechanical-recovery.test.cjs +0 -245
  280. package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +0 -46
  281. package/src/main/__tests__/scheduler-mutate-reentrancy.test.cjs +0 -117
  282. package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +0 -197
  283. package/src/main/__tests__/scheduler-never-stop.test.cjs +0 -157
  284. package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +0 -152
  285. package/src/main/__tests__/scheduler-no-orphan-run-dir.test.cjs +0 -81
  286. package/src/main/__tests__/scheduler-notify-originating-tab-transcript.test.cjs +0 -87
  287. package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +0 -343
  288. package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +0 -217
  289. package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +0 -164
  290. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +0 -161
  291. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +0 -178
  292. package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +0 -165
  293. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +0 -257
  294. package/src/main/__tests__/scheduler-rate-limit-cooldown-freshness.test.cjs +0 -123
  295. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +0 -152
  296. package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +0 -156
  297. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +0 -754
  298. package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +0 -87
  299. package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +0 -100
  300. package/src/main/__tests__/scheduler-reconcile-history-backfill.test.cjs +0 -105
  301. package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +0 -203
  302. package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +0 -247
  303. package/src/main/__tests__/scheduler-reset-job-fields-guard.test.cjs +0 -77
  304. package/src/main/__tests__/scheduler-resume-recovery.test.cjs +0 -254
  305. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +0 -115
  306. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +0 -380
  307. package/src/main/__tests__/scheduler-sigterm-commit.test.cjs +0 -43
  308. package/src/main/__tests__/scheduler-stall-per-project.test.cjs +0 -126
  309. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +0 -154
  310. package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +0 -252
  311. package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +0 -185
  312. package/src/main/__tests__/scheduler-stuck-failed-escalation.test.cjs +0 -136
  313. package/src/main/__tests__/scheduler-supervisor-record.test.cjs +0 -81
  314. package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +0 -54
  315. package/src/main/__tests__/scheduler-tick-wedge.test.cjs +0 -172
  316. package/src/main/__tests__/scheduler-transient-failure.test.cjs +0 -141
  317. package/src/main/__tests__/scheduler-unreadable-queue-guard.test.cjs +0 -62
  318. package/src/main/__tests__/scheduler-utilization-hold.test.cjs +0 -89
  319. package/src/main/__tests__/scheduler-verify-prd-path.test.cjs +0 -109
  320. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +0 -234
  321. package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +0 -120
  322. package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +0 -105
  323. package/src/main/__tests__/schedulerBatchRootBlocker.test.cjs +0 -117
  324. package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +0 -110
  325. package/src/main/__tests__/seedAgentPersonas.test.cjs +0 -184
  326. package/src/main/__tests__/seedSchedulerMcp.test.cjs +0 -211
  327. package/src/main/__tests__/seedStatus.test.cjs +0 -100
  328. package/src/main/__tests__/seedValidatorPersona.test.cjs +0 -116
  329. package/src/main/__tests__/stop-signal-anchor.test.cjs +0 -75
  330. package/src/main/__tests__/telemetryClient.test.cjs +0 -1055
  331. package/src/main/__tests__/telemetryContract.test.cjs +0 -930
  332. package/src/main/__tests__/telemetrySettings.test.cjs +0 -210
  333. package/src/main/__tests__/transcripts-batch-flush.test.cjs +0 -249
  334. package/src/main/__tests__/transcripts-doFlush-array.test.cjs +0 -124
  335. package/src/main/__tests__/transcripts-paged-reads.test.cjs +0 -241
  336. package/src/main/__tests__/transcripts-worktree-epic-path.test.cjs +0 -154
  337. package/src/main/__tests__/transcriptsUsageFor.test.cjs +0 -206
  338. package/src/main/__tests__/uniquePrdNumbers.test.cjs +0 -153
  339. package/src/main/__tests__/usageSingleFlight.test.cjs +0 -169
  340. package/src/main/__tests__/validationSentinels.test.cjs +0 -84
  341. package/src/main/__tests__/workTypeLibrary.test.cjs +0 -89
  342. package/src/main/bilkoHost.cjs +0 -314
  343. package/src/main/bilkoHostCore.cjs +0 -89
  344. package/src/main/lib/__tests__/active-sessions.test.cjs +0 -251
  345. package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +0 -179
  346. package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +0 -67
  347. package/src/main/lib/__tests__/auditLog.test.cjs +0 -38
  348. package/src/main/lib/__tests__/bootSelfHeal.test.cjs +0 -107
  349. package/src/main/lib/__tests__/branchSweep.test.cjs +0 -164
  350. package/src/main/lib/__tests__/buildIdentity.test.cjs +0 -121
  351. package/src/main/lib/__tests__/buildTarget.test.cjs +0 -52
  352. package/src/main/lib/__tests__/childWithLog.test.cjs +0 -321
  353. package/src/main/lib/__tests__/coldBootPromptSessionsWrite.test.cjs +0 -87
  354. package/src/main/lib/__tests__/crashTelemetry.test.cjs +0 -103
  355. package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +0 -115
  356. package/src/main/lib/__tests__/cwdClassify.test.cjs +0 -111
  357. package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +0 -95
  358. package/src/main/lib/__tests__/delegationReadiness.test.cjs +0 -1175
  359. package/src/main/lib/__tests__/dispatchLoop.test.cjs +0 -63
  360. package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +0 -244
  361. package/src/main/lib/__tests__/ephemeralCwd.test.cjs +0 -91
  362. package/src/main/lib/__tests__/epicDelegationStats.test.cjs +0 -137
  363. package/src/main/lib/__tests__/epicSpawnCwd.test.cjs +0 -283
  364. package/src/main/lib/__tests__/epicSpawnPlan.test.cjs +0 -196
  365. package/src/main/lib/__tests__/epicTranscriptPath.test.cjs +0 -163
  366. package/src/main/lib/__tests__/epicWorktreeBoot.test.cjs +0 -136
  367. package/src/main/lib/__tests__/epicWorktreeMerge.test.cjs +0 -130
  368. package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +0 -117
  369. package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +0 -137
  370. package/src/main/lib/__tests__/fixChainDepth.test.cjs +0 -40
  371. package/src/main/lib/__tests__/fixtures/204-mercury-steam-horse.log.txt +0 -13
  372. package/src/main/lib/__tests__/fixtures/scheduler-machine.json.corrupt-1789147548 +0 -34
  373. package/src/main/lib/__tests__/gateFixtures.json +0 -20
  374. package/src/main/lib/__tests__/gitCacheBound.test.cjs +0 -69
  375. package/src/main/lib/__tests__/gitWorktree.test.cjs +0 -1478
  376. package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +0 -107
  377. package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +0 -153
  378. package/src/main/lib/__tests__/guardShims.test.cjs +0 -158
  379. package/src/main/lib/__tests__/importReferences.spec.cjs +0 -56
  380. package/src/main/lib/__tests__/instanceLock.test.cjs +0 -173
  381. package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +0 -78
  382. package/src/main/lib/__tests__/jobWorktree.test.cjs +0 -199
  383. package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +0 -82
  384. package/src/main/lib/__tests__/landedSinceRun.test.cjs +0 -133
  385. package/src/main/lib/__tests__/launchFailure.test.cjs +0 -220
  386. package/src/main/lib/__tests__/loadGate.test.cjs +0 -303
  387. package/src/main/lib/__tests__/localAdminHttp.test.cjs +0 -214
  388. package/src/main/lib/__tests__/loopDelay.test.cjs +0 -68
  389. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +0 -107
  390. package/src/main/lib/__tests__/modelCatalog.test.cjs +0 -202
  391. package/src/main/lib/__tests__/opsOwnership.test.cjs +0 -113
  392. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +0 -328
  393. package/src/main/lib/__tests__/opsRootNestedWrite.test.cjs +0 -51
  394. package/src/main/lib/__tests__/opsRootResolve.test.cjs +0 -149
  395. package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +0 -82
  396. package/src/main/lib/__tests__/prdDisposition.test.cjs +0 -224
  397. package/src/main/lib/__tests__/procIdentity.test.cjs +0 -119
  398. package/src/main/lib/__tests__/procName.test.cjs +0 -92
  399. package/src/main/lib/__tests__/projectBriefCore.test.cjs +0 -216
  400. package/src/main/lib/__tests__/projectRootResolve.test.cjs +0 -148
  401. package/src/main/lib/__tests__/queueHealth.test.cjs +0 -58
  402. package/src/main/lib/__tests__/queueStoreAtomicWrite.test.cjs +0 -88
  403. package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +0 -190
  404. package/src/main/lib/__tests__/quietMachineLease.test.cjs +0 -39
  405. package/src/main/lib/__tests__/rateLimitWindow.test.cjs +0 -88
  406. package/src/main/lib/__tests__/reaperHelpers.test.cjs +0 -577
  407. package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +0 -312
  408. package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +0 -213
  409. package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +0 -125
  410. package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +0 -127
  411. package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +0 -109
  412. package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +0 -71
  413. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +0 -216
  414. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +0 -183
  415. package/src/main/lib/__tests__/schedulerPaths.test.cjs +0 -226
  416. package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +0 -133
  417. package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +0 -56
  418. package/src/main/lib/__tests__/sessionSlots.test.cjs +0 -144
  419. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +0 -626
  420. package/src/main/lib/__tests__/telemetryBoot.test.cjs +0 -134
  421. package/src/main/lib/__tests__/telemetryConsent.test.cjs +0 -136
  422. package/src/main/lib/__tests__/telemetryCounters.test.cjs +0 -57
  423. package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +0 -98
  424. package/src/main/lib/__tests__/terminalRunOutcome.test.cjs +0 -200
  425. package/src/main/lib/__tests__/toolUseClassify.test.cjs +0 -53
  426. package/src/main/lib/__tests__/updateCheck.test.cjs +0 -63
  427. package/src/main/lib/__tests__/upgradeDrain.test.cjs +0 -130
  428. package/src/main/lib/__tests__/usageCircuit.test.cjs +0 -354
  429. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +0 -441
  430. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +0 -266
  431. package/src/main/lib/kgExchangePairing.cjs +0 -75
@@ -1,754 +0,0 @@
1
- /**
2
- * scheduler-reap-dead-running-jobs.test.cjs — regression cover for PRD 935
3
- * (reapDeadRunningJobs runningSet desync).
4
- *
5
- * spawnJob()'s completion path can write terminal run artifacts to disk and
6
- * have its child process fully exit, yet still throw between executeJob()
7
- * resolving and the completion mutate() finishing (e.g. writeQueue's
8
- * unreadable guard). That throw is swallowed by spawnJob()'s catch, but its
9
- * `finally` unconditionally deletes the job's slug from the in-memory
10
- * runningSet — leaving queue.json stuck at status:"running" with a dead pid
11
- * while runningSet no longer names it. reapDeadRunningJobs() used to gate its
12
- * entire body on `runningSet.size === 0`, so this exact state made it return
13
- * before ever reading queue.json — permanently invisible to reconciliation.
14
- *
15
- * This test reproduces that desync directly: a project queue.json job row
16
- * with status:"running" and a dead pid, while runningSet (freshly loaded,
17
- * never populated) does not contain its slug. It asserts the job still gets
18
- * reconciled to a terminal status.
19
- *
20
- * HOME is overridden to a tmp dir BEFORE requiring scheduler.cjs, since every
21
- * path this code touches (queueStore's MACHINE_STATE_PATH, activeSessions'
22
- * project scan root, scheduler.cjs's ROOT/RUNS_DIR, schedulerBatch's
23
- * DEFAULT_PROJECT_CWD) is baked into a top-level const from os.homedir() at
24
- * require time — this test must never be able to read or write real state.
25
- *
26
- * Run: timeout 120 npx vitest run src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs
27
- */
28
-
29
- 'use strict';
30
-
31
- const assert = require('node:assert/strict');
32
- const fs = require('node:fs');
33
- const os = require('node:os');
34
- const path = require('node:path');
35
- const { execFileSync } = require('node:child_process');
36
-
37
- const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'reap-dead-running-jobs-test-'));
38
- process.env.HOME = tmpHome;
39
-
40
- const { reapDeadRunningJobs, PIDLESS_SPAWN_GRACE_MS } = require('../scheduler.cjs');
41
- const { auditLogPath } = require('../lib/auditLog.cjs');
42
- // queueStore's cwd discovery is cached for 30s (queueStore.cjs's CACHE_MS) —
43
- // a project registered by THIS test after an earlier test already populated
44
- // that cache (and didn't itself bust it, e.g. because it found nothing
45
- // reapable and returned before ever calling writeQueue) would otherwise be
46
- // invisible to readQueue() for the rest of that window. Bust explicitly
47
- // after registering a new project so each test sees its own fixture.
48
- const { bustCwdCache } = require('../lib/queueStore.cjs');
49
-
50
- function git(args, cwd) {
51
- return execFileSync('git', args, { cwd, encoding: 'utf8' });
52
- }
53
-
54
- function initRepo(dir) {
55
- fs.mkdirSync(dir, { recursive: true });
56
- git(['init', '-q'], dir);
57
- git(['config', 'user.email', 'test@example.com'], dir);
58
- git(['config', 'user.name', 'Test'], dir);
59
- fs.writeFileSync(path.join(dir, 'README.md'), 'hello\n', 'utf8');
60
- git(['add', '-A'], dir);
61
- git(['commit', '-q', '-m', 'initial'], dir);
62
- }
63
-
64
- function registerActiveProject(cwd) {
65
- const projectsDir = path.join(tmpHome, '.claude', 'projects');
66
- const slugDir = path.join(projectsDir, 'fake-project-slug');
67
- fs.mkdirSync(slugDir, { recursive: true });
68
- fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd }) + '\n');
69
- }
70
-
71
- // Every real scheduler row is born from a real PRD .md file — reconcile()'s
72
- // auto-archive-drop path (a terminal job whose slug has no on-disk PRD
73
- // anywhere is treated as "archived on purpose": dropped from queue.json and
74
- // backfilled to history.jsonl, see scheduler-reconcile-history-backfill.
75
- // test.cjs) fires the very next time broadcast({flush:true}) reconciles —
76
- // which reapDeadRunningJobs() itself does on every reap. Without a matching
77
- // PRD source, a row this test just reaped to a terminal status vanishes from
78
- // queue.json before the assertions below ever read it back. Epic-scoped (not
79
- // the legacy flat prds/ dir) so it's never swept by consolidateFlatPrds
80
- // either.
81
- function writeFixturePrd(cwd, slug) {
82
- const prdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
83
- fs.mkdirSync(prdsDir, { recursive: true });
84
- fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Test fixture PRD.', 'utf8');
85
- }
86
-
87
- function writeProjectQueue(cwd, jobs) {
88
- const stateDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'state');
89
- fs.mkdirSync(stateDir, { recursive: true });
90
- fs.writeFileSync(path.join(stateDir, 'queue.json'), JSON.stringify({ jobs }, null, 2));
91
- for (const job of jobs) {
92
- if (job && job.slug) writeFixturePrd(job.cwd || cwd, job.slug);
93
- }
94
- return path.join(stateDir, 'queue.json');
95
- }
96
-
97
- function writeRunLog(runId, slug, lines) {
98
- const runDir = path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId);
99
- fs.mkdirSync(runDir, { recursive: true });
100
- fs.writeFileSync(path.join(runDir, `${slug}.log`), lines.join('\n') + '\n');
101
- }
102
-
103
- test('reapDeadRunningJobs reconciles a queue.json row stuck at status:running with a dead pid, even when runningSet does not name its slug (desync repro)', async () => {
104
- const projectCwd = path.join(tmpHome, 'a-project');
105
- fs.mkdirSync(projectCwd, { recursive: true });
106
- registerActiveProject(projectCwd);
107
-
108
- const queuePath = writeProjectQueue(projectCwd, [
109
- {
110
- slug: 'desynced-job',
111
- status: 'running',
112
- cwd: projectCwd,
113
- runId: 'run-desynced',
114
- runtime: { pid: 999999 }, // guaranteed-dead pid (see reaperHelpers tests)
115
- },
116
- ]);
117
- writeRunLog('run-desynced', 'desynced-job', [
118
- '{"type":"result","subtype":"success","result":"done","is_error":false}',
119
- ]);
120
-
121
- // runningSet is a fresh module-level Set with nothing in it — this is the
122
- // exact desynced state spawnJob()'s finally block can leave behind.
123
- await reapDeadRunningJobs();
124
-
125
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
126
- assert.equal(jobs.length, 1);
127
- assert.equal(jobs[0].status, 'completed', 'job must be reconciled from disk state, not skipped via the runningSet gate');
128
- assert.equal(jobs[0].exitCode, 0);
129
- assert.equal(jobs[0].runtime, undefined);
130
- assert.equal(jobs[0].gateOutcome, 'passed', 'a success result event maps to gateOutcome:passed');
131
- });
132
-
133
- test('reapDeadRunningJobs reaps a pidless row older than PIDLESS_SPAWN_GRACE_MS with an empty run dir as failed, not completed, and audits it', async () => {
134
- const projectCwd = path.join(tmpHome, 'c-project');
135
- fs.mkdirSync(projectCwd, { recursive: true });
136
- registerActiveProject(projectCwd);
137
-
138
- const staleStartedAt = new Date(Date.now() - PIDLESS_SPAWN_GRACE_MS - 60_000).toISOString();
139
- const queuePath = writeProjectQueue(projectCwd, [
140
- {
141
- slug: 'pidless-zombie',
142
- status: 'running',
143
- cwd: projectCwd,
144
- runId: 'run-pidless-zombie',
145
- startedAt: staleStartedAt,
146
- estimateMinutes: 24,
147
- // no runtime key at all — the spawn never got far enough to record one
148
- },
149
- ]);
150
- // Empty run dir: created, but never written to (the exact 2026-09-01 repro).
151
- fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', 'run-pidless-zombie'), { recursive: true });
152
-
153
- const auditSizeBefore = fs.existsSync(auditLogPath()) ? fs.statSync(auditLogPath()).size : 0;
154
-
155
- await reapDeadRunningJobs();
156
-
157
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
158
- assert.equal(jobs.length, 1);
159
- assert.equal(jobs[0].status, 'failed', 'an empty run dir must never be reaped as completed');
160
- assert.match(jobs[0].error, /no runtime\.pid recorded/);
161
- assert.equal(jobs[0].runtime, undefined);
162
- assert.equal(jobs[0].gateOutcome, 'never_ran', 'a pidless reap means the gate never had a chance to run');
163
-
164
- const auditText = fs.readFileSync(auditLogPath(), 'utf8').slice(auditSizeBefore);
165
- const auditLines = auditText.trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
166
- const pidlessEvent = auditLines.find((e) => e.kind === 'job_reaped_pidless' && e.slug === 'pidless-zombie');
167
- assert.ok(pidlessEvent, 'reaping a pidless row must leave an audit trace');
168
- });
169
-
170
- test('reapDeadRunningJobs: a pidless row past grace with a landedCommit already recorded is routed to needs_review, not failed, naming the commit sha (PRD 1173)', async () => {
171
- const projectCwd = path.join(tmpHome, 'c3-project-pidless-with-landed-commit');
172
- fs.mkdirSync(projectCwd, { recursive: true });
173
- registerActiveProject(projectCwd);
174
-
175
- const staleStartedAt = new Date(Date.now() - PIDLESS_SPAWN_GRACE_MS - 60_000).toISOString();
176
- const landedSha = 'f3e35a7f9e49f13a9eb8c338b0d086160e32a87f';
177
- const queuePath = writeProjectQueue(projectCwd, [
178
- {
179
- slug: 'shipped-but-pidless',
180
- status: 'running',
181
- cwd: projectCwd,
182
- runId: 'run-shipped-but-pidless',
183
- startedAt: staleStartedAt,
184
- landedCommit: landedSha,
185
- // no runtime key at all — the spawn never got far enough to record one
186
- },
187
- ]);
188
- // Empty run dir, same shape as the sibling 'failed' test below — the
189
- // landedCommit evidence must be what changes the outcome, not the log.
190
- fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', 'run-shipped-but-pidless'), { recursive: true });
191
-
192
- await reapDeadRunningJobs();
193
-
194
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
195
- const row = jobs.find((j) => j.slug === 'shipped-but-pidless');
196
- assert.equal(row.status, 'needs_review', 'a row with proof of landed work must never be transitioned to failed by the pidless branch');
197
- assert.equal(row.landedCommit, landedSha, 'the pre-existing landedCommit must be preserved on the row');
198
- assert.match(row.error, new RegExp(landedSha), 'the reason must name the recorded commit sha');
199
- assert.equal(row.verifierVerdict, 'pidless_reap_with_landed_commit');
200
-
201
- // Scan the whole audit log for THIS slug's event rather than slicing by a
202
- // byte offset from statSync — a prior test's audit entries can contain
203
- // multi-byte characters (e.g. an em dash in a reaper reason string), and
204
- // slicing a utf8-decoded JS string (UTF-16 code units) at a byte offset
205
- // then silently corrupts the boundary. The slug is unique to this test.
206
- const auditLines = fs.readFileSync(auditLogPath(), 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
207
- const pidlessEvent = auditLines.find((e) => e.kind === 'job_reaped_pidless' && e.slug === 'shipped-but-pidless');
208
- assert.ok(pidlessEvent, 'reaping this row must still leave an audit trace');
209
- assert.equal(pidlessEvent.landedCommit, landedSha, 'the audit event must carry the landed-commit evidence so the decision is reconstructable from the audit log alone');
210
- });
211
-
212
- test('reapDeadRunningJobs clears runId on a pidless reap whose run dir holds only a sibling slug\'s files', async () => {
213
- const projectCwd = path.join(tmpHome, 'c2-project-batch-sibling');
214
- fs.mkdirSync(projectCwd, { recursive: true });
215
- registerActiveProject(projectCwd);
216
-
217
- const staleStartedAt = new Date(Date.now() - PIDLESS_SPAWN_GRACE_MS - 60_000).toISOString();
218
- // Both jobs were dispatched into the SAME batch runId dir (pickRunDir's
219
- // header: "tickQueue hands ONE shared batch dir to every spawnJob in the
220
- // batch"). 'sibling-that-ran' actually spawned and wrote its own log;
221
- // 'never-spawned' never got a pid and never wrote anything of its own.
222
- const queuePath = writeProjectQueue(projectCwd, [
223
- {
224
- slug: 'zzq8712-pidless-batch-row',
225
- status: 'running',
226
- cwd: projectCwd,
227
- runId: 'run-shared-batch',
228
- startedAt: staleStartedAt,
229
- // no runtime key at all — the spawn never got far enough to record one
230
- },
231
- ]);
232
- // Only the sibling's log exists in the shared batch dir.
233
- writeRunLog('run-shared-batch', 'zzq8712-sibling-that-ran', [
234
- '{"type":"result","subtype":"success","result":"done","is_error":false}',
235
- ]);
236
-
237
- await reapDeadRunningJobs();
238
-
239
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
240
- const row = jobs.find((j) => j.slug === 'zzq8712-pidless-batch-row');
241
- assert.equal(row.status, 'failed');
242
- assert.equal(row.runId, null, 'a runId whose dir holds no artifact for this slug must not survive the reap');
243
- });
244
-
245
- test('reapDeadRunningJobs leaves a pidless row alone while it is still within the grace window', async () => {
246
- const projectCwd = path.join(tmpHome, 'd-project');
247
- fs.mkdirSync(projectCwd, { recursive: true });
248
- registerActiveProject(projectCwd);
249
-
250
- const freshStartedAt = new Date(Date.now() - 60_000).toISOString(); // 1 minute ago
251
- const queuePath = writeProjectQueue(projectCwd, [
252
- {
253
- slug: 'mid-flight-spawn',
254
- status: 'running',
255
- cwd: projectCwd,
256
- runId: 'run-mid-flight',
257
- startedAt: freshStartedAt,
258
- },
259
- ]);
260
-
261
- await reapDeadRunningJobs();
262
-
263
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
264
- assert.equal(jobs[0].status, 'running', 'a genuinely mid-flight spawn must not be reaped');
265
- });
266
-
267
- test('reapDeadRunningJobs is a no-op when no job is actually running in queue.json', async () => {
268
- const projectCwd = path.join(tmpHome, 'b-project');
269
- fs.mkdirSync(projectCwd, { recursive: true });
270
- registerActiveProject(projectCwd);
271
- const queuePath = writeProjectQueue(projectCwd, [
272
- { slug: 'already-done', status: 'completed', cwd: projectCwd, exitCode: 0 },
273
- ]);
274
-
275
- await assert.doesNotReject(() => reapDeadRunningJobs());
276
-
277
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
278
- assert.equal(jobs.length, 1);
279
- assert.equal(jobs[0].status, 'completed');
280
- });
281
-
282
- test('reapDeadRunningJobs salvages a delta-scoped in-place patch (PRD 1098) when the row carries a persisted guardBaseline', async () => {
283
- const projectCwd = path.join(tmpHome, 'e-project');
284
- initRepo(projectCwd);
285
- registerActiveProject(projectCwd);
286
-
287
- // Pre-existing WIP present at the recorded pre-run baseline — must never
288
- // appear in the salvaged patch.
289
- fs.writeFileSync(path.join(projectCwd, 'human-wip.txt'), 'human wip\n', 'utf8');
290
-
291
- const runId = 'run-vanished-inplace';
292
- const queuePath = writeProjectQueue(projectCwd, [
293
- {
294
- slug: 'vanished-inplace',
295
- status: 'running',
296
- cwd: projectCwd,
297
- runId,
298
- runtime: { pid: 999999 }, // guaranteed-dead pid
299
- // Includes the untracked session-manager-operations/ dir itself (git
300
- // status --porcelain reports an untracked DIRECTORY, not the file
301
- // inside it) — writeProjectQueue below writes queue.json INSIDE this
302
- // same git repo, so it's part of the true pre-run baseline exactly
303
- // like human-wip.txt.
304
- guardBaseline: ['human-wip.txt', 'session-manager-operations/'],
305
- },
306
- ]);
307
- const runDir = path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId);
308
- fs.mkdirSync(runDir, { recursive: true });
309
- fs.writeFileSync(path.join(runDir, 'vanished-inplace.log'), '{"type":"result","subtype":"success","result":"done","is_error":false}\n');
310
-
311
- // The job's own delta, dirtied before its process vanished.
312
- fs.writeFileSync(path.join(projectCwd, 'README.md'), 'hello\nedited by job\n', 'utf8');
313
- fs.writeFileSync(path.join(projectCwd, 'job-output.txt'), 'job work\n', 'utf8');
314
-
315
- bustCwdCache();
316
- await reapDeadRunningJobs();
317
-
318
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
319
- const row = jobs.find((j) => j.slug === 'vanished-inplace');
320
- // PRD 1133: a successful-looking reap that left everything uncommitted
321
- // (no branch, HEAD never moved) must never read as 'completed' — this is
322
- // exactly the dangerous shape the reaper now proves against.
323
- assert.equal(row.status, 'needs_review');
324
- assert.equal(row.verifierVerdict, 'reaped_without_integration');
325
- assert.ok(row.salvagePatch, 'a salvage patch must be recorded on the row');
326
- assert.ok(fs.existsSync(row.salvagePatch));
327
- const patch = fs.readFileSync(row.salvagePatch, 'utf8');
328
- assert.match(patch, /README\.md/);
329
- assert.match(patch, /edited by job/);
330
- assert.match(patch, /job-output\.txt/);
331
- assert.doesNotMatch(patch, /human wip/, 'baseline WIP must never leak into the salvaged patch');
332
- assert.equal(row.leftoverCount, 2, 'the reaper must attribute the job\'s own delta (README.md + job-output.txt), never the baseline WIP');
333
- assert.deepEqual(new Set(row.leftoverPaths), new Set(['README.md', 'job-output.txt']));
334
- });
335
-
336
- test('reapDeadRunningJobs: a failed reap with a persisted guardBaseline names the leftover count in its transition reason and error', async () => {
337
- const projectCwd = path.join(tmpHome, 'g-project');
338
- initRepo(projectCwd);
339
- registerActiveProject(projectCwd);
340
-
341
- const runId = 'run-vanished-failed';
342
- const queuePath = writeProjectQueue(projectCwd, [
343
- {
344
- slug: 'vanished-failed',
345
- status: 'running',
346
- cwd: projectCwd,
347
- runId,
348
- runtime: { pid: 999999 },
349
- // Includes the untracked session-manager-operations/ dir itself — see
350
- // the comment on the salvage test above for why.
351
- guardBaseline: ['session-manager-operations/'],
352
- },
353
- ]);
354
- // Empty run dir → classifyRunOutcome finds no result event → 'no_result' → failed.
355
- fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId), { recursive: true });
356
-
357
- fs.writeFileSync(path.join(projectCwd, 'partial-work.txt'), 'unfinished\n', 'utf8');
358
-
359
- bustCwdCache();
360
- await reapDeadRunningJobs();
361
-
362
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
363
- const row = jobs.find((j) => j.slug === 'vanished-failed');
364
- assert.equal(row.status, 'failed');
365
- assert.equal(row.leftoverCount, 1);
366
- assert.deepEqual(row.leftoverPaths, ['partial-work.txt']);
367
- assert.match(row.error, /left 1 files uncommitted/, 'the leftover count must be visible in the error string, not just a separate field');
368
- const lastTransition = row.statusHistory[row.statusHistory.length - 1];
369
- assert.match(lastTransition.reason, /left 1 files uncommitted/);
370
- });
371
-
372
- test('reapDeadRunningJobs: a rate-limited death (api_error_status:429) is reset to pending, not stamped failed, and the reason names the rate limit (PRD 1117)', async () => {
373
- const projectCwd = path.join(tmpHome, 'h-project');
374
- fs.mkdirSync(projectCwd, { recursive: true });
375
- registerActiveProject(projectCwd);
376
-
377
- const runId = 'run-rate-limited';
378
- const queuePath = writeProjectQueue(projectCwd, [
379
- {
380
- slug: 'rate-limited-job',
381
- status: 'running',
382
- cwd: projectCwd,
383
- runId,
384
- runtime: { pid: 999999 }, // guaranteed-dead pid — the reaper won the race
385
- },
386
- ]);
387
- // Real-shape tail: a 429 result event, the exact signal the reaper
388
- // previously had no branch for and stamped terminal 'failed'.
389
- writeRunLog(runId, 'rate-limited-job', [
390
- '{"type":"result","subtype":"success","is_error":true,"api_error_status":429,"rateLimitType":"seven_day","result":"You\'ve reached your Fable limit.","uuid":"x"}',
391
- ]);
392
-
393
- await reapDeadRunningJobs();
394
-
395
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
396
- const row = jobs.find((j) => j.slug === 'rate-limited-job');
397
- assert.equal(row.status, 'pending', 'a rate-limited reap must be retryable, never terminal failed');
398
- assert.match(row.error, /rate limit/i, 'the reset reason must name the rate limit, not read as a generic reap failure');
399
- assert.equal(row.runtime, undefined);
400
- });
401
-
402
- test('reapDeadRunningJobs skips in-place salvage (no whole-tree dump) when the row has no persisted guardBaseline', async () => {
403
- const projectCwd = path.join(tmpHome, 'f-project');
404
- initRepo(projectCwd);
405
- registerActiveProject(projectCwd);
406
-
407
- fs.writeFileSync(path.join(projectCwd, 'human-wip.txt'), 'human wip\n', 'utf8');
408
- fs.writeFileSync(path.join(projectCwd, 'README.md'), 'hello\nedited by job\n', 'utf8');
409
-
410
- const runId = 'run-vanished-no-baseline';
411
- const queuePath = writeProjectQueue(projectCwd, [
412
- {
413
- slug: 'vanished-no-baseline',
414
- status: 'running',
415
- cwd: projectCwd,
416
- runId,
417
- runtime: { pid: 999999 },
418
- // No guardBaseline field — simulates a row whose owning process
419
- // vanished before spawnJob's dispatch-time persist-mutate ever ran, so
420
- // there's no safe way to distinguish this job's own dirt from the
421
- // human's pre-existing WIP above.
422
- },
423
- ]);
424
- const runDir = path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId);
425
- fs.mkdirSync(runDir, { recursive: true });
426
- fs.writeFileSync(path.join(runDir, 'vanished-no-baseline.log'), '{"type":"result","subtype":"success","result":"done","is_error":false}\n');
427
-
428
- bustCwdCache();
429
- await reapDeadRunningJobs();
430
-
431
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
432
- const row = jobs.find((j) => j.slug === 'vanished-no-baseline');
433
- // PRD 1133: no commit landed (HEAD never moved) — must not read as 'completed'
434
- // just because the log's own result event looked clean.
435
- assert.equal(row.status, 'needs_review');
436
- assert.equal(row.verifierVerdict, 'reaped_without_integration');
437
- assert.equal(row.salvagePatch, undefined, 'must skip salvage entirely rather than ever dumping the whole tree');
438
- assert.equal(row.leftoverPaths, undefined, 'no baseline means no safe attribution — must not guess');
439
- assert.equal(row.leftoverCount, undefined);
440
- });
441
-
442
- // ---------- PRD 1133: reaper must prove integration before 'completed' ----------
443
-
444
- test('reapDeadRunningJobs: a worktree job shaped like PRD 1118 (branch un-integrated, landedCommit absent) comes out needs_review, naming the branch, and never merges it', async () => {
445
- const projectCwd = path.join(tmpHome, 'i-project-1118-shape');
446
- initRepo(projectCwd);
447
- registerActiveProject(projectCwd);
448
-
449
- const headBefore = git(['rev-parse', 'HEAD'], projectCwd).trim();
450
- const slug = '1118-pause-until-the-binding-rate-limit-window';
451
- const branch = `sm-job/${slug}`;
452
- // Simulate the job's own worktree branch: one real commit, never merged
453
- // back into the main tree (exactly the live 1118 shape — "one real commit,
454
- // +301", stranded on sm-job/1118-... with landedCommit undefined).
455
- git(['checkout', '-b', branch], projectCwd);
456
- fs.writeFileSync(path.join(projectCwd, 'rateLimitWindow.cjs'), 'module.exports = {};\n', 'utf8');
457
- git(['add', '-A'], projectCwd);
458
- git(['commit', '-q', '-m', 'add rateLimitWindow.cjs'], projectCwd);
459
- // Back to the original tip (detached is fine — reap only reads guardCwd's
460
- // current HEAD, it never needs a named branch checked out).
461
- git(['checkout', headBefore], projectCwd);
462
-
463
- const runId = `run-${slug}`;
464
- const queuePath = writeProjectQueue(projectCwd, [
465
- {
466
- slug,
467
- status: 'running',
468
- cwd: projectCwd,
469
- runId,
470
- runtime: { pid: 999999 }, // guaranteed-dead pid
471
- guardHeadBefore: headBefore,
472
- startedAt: new Date(Date.now() - 60_000).toISOString(),
473
- },
474
- ]);
475
- writeRunLog(runId, slug, ['{"type":"result","subtype":"success","result":"done","is_error":false}']);
476
-
477
- bustCwdCache();
478
- await reapDeadRunningJobs();
479
-
480
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
481
- const row = jobs.find((j) => j.slug === slug);
482
- assert.equal(row.status, 'needs_review', 'a successful-looking result event must not be enough on its own');
483
- assert.equal(row.verifierVerdict, 'reaped_without_integration');
484
- assert.match(row.error, new RegExp(branch.replace('/', '\\/')), 'the parked reason must name the branch that still holds the work');
485
- assert.equal(row.landedCommit, undefined, 'nothing was actually merged, so landedCommit must stay unset');
486
-
487
- // The branch itself must survive untouched — this reap must never attempt
488
- // (let alone perform) the merge.
489
- const branchStillExists = git(['rev-parse', '--verify', branch], projectCwd).trim();
490
- assert.ok(branchStillExists, 'the un-integrated branch must be preserved, not merged or deleted, by a mere reap');
491
- assert.equal(git(['rev-parse', 'HEAD'], projectCwd).trim(), headBefore, 'guardCwd HEAD must not have moved — no merge was performed');
492
- });
493
-
494
- test('reapDeadRunningJobs: a worktree job whose branch is already fully integrated into HEAD comes out completed, with landedCommit stamped', async () => {
495
- const projectCwd = path.join(tmpHome, 'j-project-real-success');
496
- initRepo(projectCwd);
497
- registerActiveProject(projectCwd);
498
-
499
- const headBefore = git(['rev-parse', 'HEAD'], projectCwd).trim();
500
- const slug = 'real-successful-worktree-run';
501
- const branch = `sm-job/${slug}`;
502
- git(['checkout', '-b', branch], projectCwd);
503
- fs.writeFileSync(path.join(projectCwd, 'feature.txt'), 'shipped\n', 'utf8');
504
- git(['add', '-A'], projectCwd);
505
- git(['commit', '-q', '-m', 'ship feature'], projectCwd);
506
- git(['checkout', headBefore], projectCwd); // back to the pre-branch tip (detached)
507
- // Simulate a normal completion pass having already integrated the branch
508
- // (integrateJobBranch's own fast-forward path) BEFORE the process vanished
509
- // between that merge and the queue write landing.
510
- git(['merge', '--ff-only', branch], projectCwd);
511
- const headAfterMerge = git(['rev-parse', 'HEAD'], projectCwd).trim();
512
-
513
- const runId = `run-${slug}`;
514
- const queuePath = writeProjectQueue(projectCwd, [
515
- {
516
- slug,
517
- status: 'running',
518
- cwd: projectCwd,
519
- runId,
520
- runtime: { pid: 999999 },
521
- guardHeadBefore: headBefore,
522
- startedAt: new Date(Date.now() - 60_000).toISOString(),
523
- },
524
- ]);
525
- writeRunLog(runId, slug, ['{"type":"result","subtype":"success","result":"done","is_error":false}']);
526
-
527
- bustCwdCache();
528
- await reapDeadRunningJobs();
529
-
530
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
531
- const row = jobs.find((j) => j.slug === slug);
532
- assert.equal(row.status, 'completed', 'the branch is already an ancestor of HEAD — the work genuinely landed');
533
- assert.equal(row.verifierVerdict, undefined);
534
- assert.equal(row.landedCommit, headAfterMerge, 'HEAD advanced during the run window — landedCommit must be stamped');
535
- });
536
-
537
- test('reapDeadRunningJobs: a worktree job whose branch never diverged from its base (legitimate no-op) still completes, not punished', async () => {
538
- const projectCwd = path.join(tmpHome, 'k-project-noop-worktree');
539
- initRepo(projectCwd);
540
- registerActiveProject(projectCwd);
541
-
542
- const headBefore = git(['rev-parse', 'HEAD'], projectCwd).trim();
543
- const slug = 'noop-worktree-job';
544
- const branch = `sm-job/${slug}`;
545
- // Branch created but nothing ever committed on it — a genuine no-op run.
546
- git(['branch', branch], projectCwd);
547
-
548
- const runId = `run-${slug}`;
549
- const queuePath = writeProjectQueue(projectCwd, [
550
- {
551
- slug,
552
- status: 'running',
553
- cwd: projectCwd,
554
- runId,
555
- runtime: { pid: 999999 },
556
- guardHeadBefore: headBefore,
557
- startedAt: new Date(Date.now() - 60_000).toISOString(),
558
- },
559
- ]);
560
- writeRunLog(runId, slug, ['{"type":"result","subtype":"success","result":"done","is_error":false}']);
561
-
562
- bustCwdCache();
563
- await reapDeadRunningJobs();
564
-
565
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
566
- const row = jobs.find((j) => j.slug === slug);
567
- assert.equal(row.status, 'completed', 'a branch with no new commits is a legitimate no-op, not a finish-protocol violation');
568
- assert.equal(row.verifierVerdict, undefined);
569
- assert.equal(row.landedCommit, undefined, 'HEAD never moved — nothing to stamp');
570
- });
571
-
572
- test('reapDeadRunningJobs: an in-place run in a git repo whose HEAD genuinely advanced during the run window completes, with landedCommit stamped', async () => {
573
- const projectCwd = path.join(tmpHome, 'l-project-inplace-success');
574
- initRepo(projectCwd);
575
- registerActiveProject(projectCwd);
576
-
577
- const headBefore = git(['rev-parse', 'HEAD'], projectCwd).trim();
578
- const startedAt = new Date(Date.now() - 60_000).toISOString();
579
- fs.writeFileSync(path.join(projectCwd, 'shipped.txt'), 'done\n', 'utf8');
580
- git(['add', '-A'], projectCwd);
581
- git(['commit', '-q', '-m', 'in-place job commit'], projectCwd);
582
- const headAfter = git(['rev-parse', 'HEAD'], projectCwd).trim();
583
-
584
- const slug = 'inplace-job-real-commit';
585
- const runId = `run-${slug}`;
586
- const queuePath = writeProjectQueue(projectCwd, [
587
- {
588
- slug,
589
- status: 'running',
590
- cwd: projectCwd,
591
- runId,
592
- runtime: { pid: 999999 },
593
- guardHeadBefore: headBefore,
594
- startedAt,
595
- },
596
- ]);
597
- writeRunLog(runId, slug, ['{"type":"result","subtype":"success","result":"done","is_error":false}']);
598
-
599
- bustCwdCache();
600
- await reapDeadRunningJobs();
601
-
602
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
603
- const row = jobs.find((j) => j.slug === slug);
604
- assert.equal(row.status, 'completed');
605
- assert.equal(row.verifierVerdict, undefined);
606
- assert.equal(row.landedCommit, headAfter);
607
- });
608
-
609
- test('reapDeadRunningJobs: a non-git cwd skips the integration check entirely, preserving today\'s behaviour', async () => {
610
- const projectCwd = path.join(tmpHome, 'm-project-non-git');
611
- fs.mkdirSync(projectCwd, { recursive: true });
612
- registerActiveProject(projectCwd);
613
-
614
- const slug = 'non-git-cwd-job';
615
- const runId = `run-${slug}`;
616
- const queuePath = writeProjectQueue(projectCwd, [
617
- {
618
- slug,
619
- status: 'running',
620
- cwd: projectCwd,
621
- runId,
622
- runtime: { pid: 999999 },
623
- // No git repo at all — the integration/HEAD-advance check must be
624
- // skipped, never treated as a failure to prove landing.
625
- },
626
- ]);
627
- writeRunLog(runId, slug, ['{"type":"result","subtype":"success","result":"done","is_error":false}']);
628
-
629
- bustCwdCache();
630
- await reapDeadRunningJobs();
631
-
632
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
633
- const row = jobs.find((j) => j.slug === slug);
634
- assert.equal(row.status, 'completed', 'a non-git cwd must keep today\'s behaviour — the check is meaningless there');
635
- assert.equal(row.verifierVerdict, undefined);
636
- });
637
-
638
- // ---------- Reaper landed-commit evidence gate (job 1192 incident) ----------
639
- //
640
- // job 1192 shipped a real 3-file commit (7bf5fa3f...) and was still
641
- // stamped 'failed' by reapDeadRunningJobs because a `landedCommit` field
642
- // being present was never actually verified against the repo. These cover
643
- // the dead-pid branch specifically (runtime.pid recorded, process
644
- // confirmed dead) rather than the pidless branch: a pidless row with a
645
- // non-empty landedCommit is already fully owned by PRD 1173's
646
- // resolvePidlessFailureOverride (routes to needs_review, tested above) —
647
- // this PRD's gate exists for the shape 1173 doesn't cover, where the
648
- // reaper would otherwise fall straight through to 'failed' with no
649
- // evidence check of any kind.
650
-
651
- test('reapDeadRunningJobs: a dead-pid reap with a git-resolvable landedCommit transitions to completed, naming the reap cause and the evidence, source reapDeadRunningJobs:landed', async () => {
652
- const projectCwd = path.join(tmpHome, 'n-project-landed-commit-evidence');
653
- initRepo(projectCwd);
654
- registerActiveProject(projectCwd);
655
-
656
- fs.writeFileSync(path.join(projectCwd, 'shipped.txt'), 'done\n', 'utf8');
657
- git(['add', '-A'], projectCwd);
658
- git(['commit', '-q', '-m', 'ship the PRD work'], projectCwd);
659
- const landedSha = git(['rev-parse', 'HEAD'], projectCwd).trim();
660
-
661
- const runId = 'run-landed-commit-evidence';
662
- const queuePath = writeProjectQueue(projectCwd, [
663
- {
664
- slug: 'landed-but-reaped',
665
- status: 'running',
666
- cwd: projectCwd,
667
- runId,
668
- runtime: { pid: 999999 }, // guaranteed-dead pid
669
- landedCommit: landedSha,
670
- },
671
- ]);
672
- // Empty run dir → classifyRunOutcome finds no result event → 'no_result'
673
- // → without this PRD's gate this would stamp 'failed' despite the real
674
- // commit already sitting on HEAD.
675
- fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId), { recursive: true });
676
-
677
- bustCwdCache();
678
- await reapDeadRunningJobs();
679
-
680
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
681
- const row = jobs.find((j) => j.slug === 'landed-but-reaped');
682
- assert.equal(row.status, 'completed', 'a resolvable landedCommit must promote the reap to completed, not failed');
683
- assert.equal(row.landedCommit, landedSha);
684
- assert.equal(row.exitCode, 0);
685
- assert.equal(row.error, null);
686
- const lastTransition = row.statusHistory[row.statusHistory.length - 1];
687
- assert.equal(lastTransition.source, 'reapDeadRunningJobs:landed');
688
- assert.match(lastTransition.reason, /process gone/, 'the reap cause must still be named');
689
- assert.match(lastTransition.reason, new RegExp(landedSha), 'the evidence sha must be named');
690
- assert.match(lastTransition.reason, /resolves/);
691
- });
692
-
693
- test('reapDeadRunningJobs: a dead-pid reap with an unresolvable (stale) landedCommit keeps today\'s behaviour exactly — failed, same reason shape, same source', async () => {
694
- const projectCwd = path.join(tmpHome, 'o-project-stale-landed-commit');
695
- initRepo(projectCwd);
696
- registerActiveProject(projectCwd);
697
-
698
- const staleSha = '0123456789abcdef0123456789abcdef01234567'; // well-formed sha, never committed anywhere
699
- const runId = 'run-stale-landed-commit';
700
- const queuePath = writeProjectQueue(projectCwd, [
701
- {
702
- slug: 'stale-landed-commit',
703
- status: 'running',
704
- cwd: projectCwd,
705
- runId,
706
- runtime: { pid: 999999 },
707
- landedCommit: staleSha,
708
- },
709
- ]);
710
- fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId), { recursive: true });
711
-
712
- bustCwdCache();
713
- await reapDeadRunningJobs();
714
-
715
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
716
- const row = jobs.find((j) => j.slug === 'stale-landed-commit');
717
- assert.equal(row.status, 'failed', 'a landedCommit that does not resolve in the repo must never be trusted as evidence');
718
- assert.equal(row.landedCommit, staleSha, 'the stale field is left on the row untouched, never cleared');
719
- assert.match(row.error, /reaped: process gone \(outcome=no_result\)/);
720
- assert.doesNotMatch(row.error, /resolves/);
721
- const lastTransition = row.statusHistory[row.statusHistory.length - 1];
722
- assert.equal(lastTransition.source, 'reapDeadRunningJobs', 'no evidence found — source must stay the plain reaper source, not :landed');
723
- });
724
-
725
- test('reapDeadRunningJobs: a landedCommit that cannot be git-resolved because the row cwd is not a usable git repo falls back to failed without throwing out of the reaper', async () => {
726
- const projectCwd = path.join(tmpHome, 'p-project-unresolvable-cwd');
727
- // Deliberately NOT initRepo(projectCwd) — no `.git` at all, the same
728
- // "cwd git resolution cannot possibly succeed" shape a deleted/torn-down
729
- // worktree checkout would produce (git itself errors identically for
730
- // "not a git repository" and "no such directory" — both must be caught
731
- // and treated as no-evidence, never thrown out of the reaper).
732
- fs.mkdirSync(projectCwd, { recursive: true });
733
- registerActiveProject(projectCwd);
734
-
735
- const runId = 'run-unresolvable-cwd';
736
- const queuePath = writeProjectQueue(projectCwd, [
737
- {
738
- slug: 'unresolvable-cwd-job',
739
- status: 'running',
740
- cwd: projectCwd,
741
- runId,
742
- runtime: { pid: 999999 },
743
- landedCommit: '89abcdef89abcdef89abcdef89abcdef89abcdef',
744
- },
745
- ]);
746
- fs.mkdirSync(path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId), { recursive: true });
747
-
748
- bustCwdCache();
749
- await assert.doesNotReject(() => reapDeadRunningJobs(), 'a bounded git spawn failure/timeout must never escape the reaper as an unhandled rejection');
750
-
751
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
752
- const row = jobs.find((j) => j.slug === 'unresolvable-cwd-job');
753
- assert.equal(row.status, 'failed', 'an unresolvable cwd must fall back to failed, exactly like no evidence at all');
754
- });