claude-code-session-manager 0.97.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 (430) 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-D93FoaOm.js → DataModel-HdR0AxFq.js} +1 -1
  5. package/dist/assets/{History-CXxxqNP1.js → History-Dr4Jr_5z.js} +2 -2
  6. package/dist/assets/{Hooks-CzeYFQjq.js → Hooks-NhrvH3JP.js} +3 -3
  7. package/dist/assets/{Library-D2zPkwOC.js → Library-Dv_EtIMr.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-DbinP78A.js → MarkdownEditor-DTKDWV_n.js} +1 -1
  9. package/dist/assets/{McpServers-Bu_sVGLr.js → McpServers-M034G9B3.js} +2 -2
  10. package/dist/assets/{Memory-BgiI6LZ_.js → Memory-DzF1B4gx.js} +6 -6
  11. package/dist/assets/{Permissions-C1MMzZRm.js → Permissions-DrgbSGWl.js} +3 -3
  12. package/dist/assets/{Plugins-Dhu6wAZi.js → Plugins-Bd3ef35w.js} +2 -2
  13. package/dist/assets/{ProvenanceBadge-C3AcWKMG.js → ProvenanceBadge-DcBHT1Iy.js} +1 -1
  14. package/dist/assets/{SaveBar-plwYm0iO.js → SaveBar-rimJem-Y.js} +1 -1
  15. package/dist/assets/Scheduler-DjgFJl2w.js +16 -0
  16. package/dist/assets/{ScopeSwitcher-DFy0vv_o.js → ScopeSwitcher-CnAzTBrl.js} +1 -1
  17. package/dist/assets/Settings-Dc1F_sSg.js +3 -0
  18. package/dist/assets/{SkillReferenceGraph-DsI7hRPB.js → SkillReferenceGraph-B9aZDecM.js} +1 -1
  19. package/dist/assets/{Skills-3E748UfB.js → Skills-DRXd18gu.js} +2 -2
  20. package/dist/assets/{SystemPrompt-BqmfUJOd.js → SystemPrompt-ahy5-Fwe.js} +1 -1
  21. package/dist/assets/{TagLibrary-CPz6fO8Q.js → TagLibrary-C_tToDeL.js} +1 -1
  22. package/dist/assets/{TiptapBody-D9EoBQ1P.js → TiptapBody-DvdefS6K.js} +1 -1
  23. package/dist/assets/{Toggle-BHAmZNDn.js → Toggle-6Jl5Njc6.js} +1 -1
  24. package/dist/assets/{index-CKH5Uxik.css → index-Bta-hwud.css} +1 -1
  25. package/dist/assets/{index-tn5JIVPj.js → index-vx8O73l8.js} +497 -502
  26. package/dist/assets/{settingsSchema-Crqy_aNz.js → settingsSchema-D6iRguNV.js} +1 -1
  27. package/dist/index.html +2 -2
  28. package/package.json +26 -29
  29. package/plugins/CLAUDE.md +6 -6
  30. package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +23 -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 +153 -19
  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 +33 -30
  91. package/src/main/lib/workTypeLibrary.cjs +7 -1
  92. package/src/main/promptSessionEvents.cjs +37 -13
  93. package/src/main/queueOps.cjs +17 -6
  94. package/src/main/scheduler.cjs +975 -203
  95. package/src/main/seedAgentPersonas.cjs +278 -23
  96. package/src/main/supervisor.cjs +4 -1
  97. package/src/main/templates/PRD_AUTHORING.md +126 -406
  98. package/src/preload/__tests__/preload-surface.test.cjs +68 -0
  99. package/src/preload/api.d.ts +41 -94
  100. package/src/preload/index.cjs +14 -9
  101. package/src/seed/agents/architect.md +1 -0
  102. package/src/seed/agents/dev-lead.md +17 -18
  103. package/src/seed/agents/project-home-builder.md +1 -0
  104. package/src/seed/agents/validator.md +10 -4
  105. package/dist/assets/HostBilko-nESrTRIg.js +0 -1
  106. package/dist/assets/Scheduler-BVYTeh39.js +0 -16
  107. package/dist/assets/Settings-DIhGgdfs.js +0 -3
  108. package/src/main/__tests__/activeIndexMerge.test.cjs +0 -235
  109. package/src/main/__tests__/agentEffortResolve.test.cjs +0 -117
  110. package/src/main/__tests__/agentLibrary.test.cjs +0 -276
  111. package/src/main/__tests__/agentModelResolve.test.cjs +0 -311
  112. package/src/main/__tests__/agentOverlayWrite.test.cjs +0 -95
  113. package/src/main/__tests__/bilkoHost-deriveSlug.test.cjs +0 -26
  114. package/src/main/__tests__/bilkoHost-integration.test.cjs +0 -118
  115. package/src/main/__tests__/bilkoHostCore.test.cjs +0 -72
  116. package/src/main/__tests__/broadcastCoalescer.test.cjs +0 -122
  117. package/src/main/__tests__/chat-cancel-terminal.test.cjs +0 -120
  118. package/src/main/__tests__/chat-dead-channels.test.cjs +0 -63
  119. package/src/main/__tests__/chat-exit-close-race.test.cjs +0 -146
  120. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +0 -139
  121. package/src/main/__tests__/chat-preamble-anchors.test.cjs +0 -100
  122. package/src/main/__tests__/chat-queue.test.cjs +0 -97
  123. package/src/main/__tests__/chat-stop-signal.test.cjs +0 -89
  124. package/src/main/__tests__/chatRunner-epic-worktree-execcwd.test.cjs +0 -125
  125. package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +0 -252
  126. package/src/main/__tests__/classifyPromptTicket.test.cjs +0 -101
  127. package/src/main/__tests__/classifyTranscriptLine.test.cjs +0 -201
  128. package/src/main/__tests__/computeDepHistorySatisfaction.test.cjs +0 -66
  129. package/src/main/__tests__/config-readText-bounded.test.cjs +0 -84
  130. package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +0 -59
  131. package/src/main/__tests__/crossProjectFeedback.test.cjs +0 -334
  132. package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +0 -161
  133. package/src/main/__tests__/dep-orphan-archive-health.test.cjs +0 -77
  134. package/src/main/__tests__/develop-skill-failure-modes.test.cjs +0 -70
  135. package/src/main/__tests__/docEdit.test.cjs +0 -244
  136. package/src/main/__tests__/dod-batchkey.test.cjs +0 -183
  137. package/src/main/__tests__/dod-drain-hook.test.cjs +0 -302
  138. package/src/main/__tests__/dod-report.test.cjs +0 -304
  139. package/src/main/__tests__/dod-reverify.test.cjs +0 -285
  140. package/src/main/__tests__/epicContextDigest.test.cjs +0 -174
  141. package/src/main/__tests__/epicMint.test.cjs +0 -332
  142. package/src/main/__tests__/epicMintTelemetryTap.test.cjs +0 -64
  143. package/src/main/__tests__/epicStatusMirror.test.cjs +0 -110
  144. package/src/main/__tests__/epicValidationHook.test.cjs +0 -291
  145. package/src/main/__tests__/exchanges.test.cjs +0 -122
  146. package/src/main/__tests__/exchangesPromptId.test.cjs +0 -61
  147. package/src/main/__tests__/extractJson.test.cjs +0 -51
  148. package/src/main/__tests__/files-reject-credentials.test.cjs +0 -40
  149. package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +0 -556
  150. package/src/main/__tests__/flatPrdTickSweep.test.cjs +0 -110
  151. package/src/main/__tests__/health-build-freshness.test.cjs +0 -39
  152. package/src/main/__tests__/health-claude-md-budget.test.cjs +0 -57
  153. package/src/main/__tests__/health-credentials.test.cjs +0 -81
  154. package/src/main/__tests__/health-delegation-chain.test.cjs +0 -124
  155. package/src/main/__tests__/health-per-project-stall.test.cjs +0 -84
  156. package/src/main/__tests__/health-prd-migration.test.cjs +0 -37
  157. package/src/main/__tests__/health-queue-dispatch.test.cjs +0 -135
  158. package/src/main/__tests__/health-starve-escalation.test.cjs +0 -94
  159. package/src/main/__tests__/health-tick-liveness.test.cjs +0 -179
  160. package/src/main/__tests__/health-usage-poller.test.cjs +0 -144
  161. package/src/main/__tests__/health-worktree-cap-blocked.test.cjs +0 -65
  162. package/src/main/__tests__/heapSnapshot.test.cjs +0 -121
  163. package/src/main/__tests__/historyAggregatorIntraday.test.cjs +0 -313
  164. package/src/main/__tests__/historyDashboard.test.cjs +0 -163
  165. package/src/main/__tests__/historyRollup.test.cjs +0 -333
  166. package/src/main/__tests__/intradayRefresh.test.cjs +0 -39
  167. package/src/main/__tests__/ipcSchemas-dependsOn.test.cjs +0 -39
  168. package/src/main/__tests__/kg-augment.test.cjs +0 -195
  169. package/src/main/__tests__/loadGateDetailTick.test.cjs +0 -31
  170. package/src/main/__tests__/machineProfile.test.cjs +0 -152
  171. package/src/main/__tests__/mcpStatus.test.cjs +0 -61
  172. package/src/main/__tests__/memoryAggregate.test.cjs +0 -109
  173. package/src/main/__tests__/memoryStale.test.cjs +0 -88
  174. package/src/main/__tests__/needsReviewLedger.test.cjs +0 -162
  175. package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +0 -25
  176. package/src/main/__tests__/opsErrorLog.test.cjs +0 -109
  177. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +0 -173
  178. package/src/main/__tests__/personaMerge.test.cjs +0 -169
  179. package/src/main/__tests__/planValidator.test.cjs +0 -125
  180. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +0 -176
  181. package/src/main/__tests__/prd-group-allocator.test.cjs +0 -119
  182. package/src/main/__tests__/prdAdminRouteParity.test.cjs +0 -70
  183. package/src/main/__tests__/prdAdminRoutes.test.cjs +0 -718
  184. package/src/main/__tests__/prdAgentType.test.cjs +0 -103
  185. package/src/main/__tests__/prdAuthoringSeed.test.cjs +0 -39
  186. package/src/main/__tests__/prdCreate.test.cjs +0 -979
  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 -154
  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-needs-review-autoresolve.test.cjs +0 -197
  282. package/src/main/__tests__/scheduler-never-stop.test.cjs +0 -157
  283. package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +0 -152
  284. package/src/main/__tests__/scheduler-no-orphan-run-dir.test.cjs +0 -81
  285. package/src/main/__tests__/scheduler-notify-originating-tab-transcript.test.cjs +0 -87
  286. package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +0 -343
  287. package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +0 -217
  288. package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +0 -164
  289. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +0 -161
  290. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +0 -178
  291. package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +0 -165
  292. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +0 -257
  293. package/src/main/__tests__/scheduler-rate-limit-cooldown-freshness.test.cjs +0 -123
  294. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +0 -152
  295. package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +0 -156
  296. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +0 -754
  297. package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +0 -87
  298. package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +0 -100
  299. package/src/main/__tests__/scheduler-reconcile-history-backfill.test.cjs +0 -105
  300. package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +0 -203
  301. package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +0 -247
  302. package/src/main/__tests__/scheduler-reset-job-fields-guard.test.cjs +0 -77
  303. package/src/main/__tests__/scheduler-resume-recovery.test.cjs +0 -254
  304. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +0 -115
  305. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +0 -380
  306. package/src/main/__tests__/scheduler-sigterm-commit.test.cjs +0 -43
  307. package/src/main/__tests__/scheduler-stall-per-project.test.cjs +0 -126
  308. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +0 -154
  309. package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +0 -252
  310. package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +0 -185
  311. package/src/main/__tests__/scheduler-stuck-failed-escalation.test.cjs +0 -136
  312. package/src/main/__tests__/scheduler-supervisor-record.test.cjs +0 -81
  313. package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +0 -54
  314. package/src/main/__tests__/scheduler-tick-wedge.test.cjs +0 -172
  315. package/src/main/__tests__/scheduler-transient-failure.test.cjs +0 -141
  316. package/src/main/__tests__/scheduler-unreadable-queue-guard.test.cjs +0 -62
  317. package/src/main/__tests__/scheduler-utilization-hold.test.cjs +0 -89
  318. package/src/main/__tests__/scheduler-verify-prd-path.test.cjs +0 -109
  319. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +0 -234
  320. package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +0 -120
  321. package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +0 -105
  322. package/src/main/__tests__/schedulerBatchRootBlocker.test.cjs +0 -117
  323. package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +0 -110
  324. package/src/main/__tests__/seedAgentPersonas.test.cjs +0 -184
  325. package/src/main/__tests__/seedSchedulerMcp.test.cjs +0 -211
  326. package/src/main/__tests__/seedStatus.test.cjs +0 -100
  327. package/src/main/__tests__/seedValidatorPersona.test.cjs +0 -116
  328. package/src/main/__tests__/stop-signal-anchor.test.cjs +0 -75
  329. package/src/main/__tests__/telemetryClient.test.cjs +0 -1055
  330. package/src/main/__tests__/telemetryContract.test.cjs +0 -930
  331. package/src/main/__tests__/telemetrySettings.test.cjs +0 -210
  332. package/src/main/__tests__/transcripts-batch-flush.test.cjs +0 -249
  333. package/src/main/__tests__/transcripts-doFlush-array.test.cjs +0 -124
  334. package/src/main/__tests__/transcripts-paged-reads.test.cjs +0 -241
  335. package/src/main/__tests__/transcripts-worktree-epic-path.test.cjs +0 -154
  336. package/src/main/__tests__/transcriptsUsageFor.test.cjs +0 -206
  337. package/src/main/__tests__/uniquePrdNumbers.test.cjs +0 -153
  338. package/src/main/__tests__/usageSingleFlight.test.cjs +0 -169
  339. package/src/main/__tests__/validationSentinels.test.cjs +0 -84
  340. package/src/main/__tests__/workTypeLibrary.test.cjs +0 -89
  341. package/src/main/bilkoHost.cjs +0 -314
  342. package/src/main/bilkoHostCore.cjs +0 -89
  343. package/src/main/lib/__tests__/active-sessions.test.cjs +0 -251
  344. package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +0 -179
  345. package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +0 -67
  346. package/src/main/lib/__tests__/auditLog.test.cjs +0 -38
  347. package/src/main/lib/__tests__/bootSelfHeal.test.cjs +0 -107
  348. package/src/main/lib/__tests__/branchSweep.test.cjs +0 -164
  349. package/src/main/lib/__tests__/buildIdentity.test.cjs +0 -121
  350. package/src/main/lib/__tests__/buildTarget.test.cjs +0 -52
  351. package/src/main/lib/__tests__/childWithLog.test.cjs +0 -321
  352. package/src/main/lib/__tests__/coldBootPromptSessionsWrite.test.cjs +0 -87
  353. package/src/main/lib/__tests__/crashTelemetry.test.cjs +0 -103
  354. package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +0 -115
  355. package/src/main/lib/__tests__/cwdClassify.test.cjs +0 -111
  356. package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +0 -95
  357. package/src/main/lib/__tests__/delegationReadiness.test.cjs +0 -1175
  358. package/src/main/lib/__tests__/dispatchLoop.test.cjs +0 -63
  359. package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +0 -244
  360. package/src/main/lib/__tests__/ephemeralCwd.test.cjs +0 -91
  361. package/src/main/lib/__tests__/epicDelegationStats.test.cjs +0 -137
  362. package/src/main/lib/__tests__/epicSpawnCwd.test.cjs +0 -283
  363. package/src/main/lib/__tests__/epicSpawnPlan.test.cjs +0 -196
  364. package/src/main/lib/__tests__/epicTranscriptPath.test.cjs +0 -163
  365. package/src/main/lib/__tests__/epicWorktreeBoot.test.cjs +0 -136
  366. package/src/main/lib/__tests__/epicWorktreeMerge.test.cjs +0 -130
  367. package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +0 -117
  368. package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +0 -137
  369. package/src/main/lib/__tests__/fixChainDepth.test.cjs +0 -40
  370. package/src/main/lib/__tests__/fixtures/204-mercury-steam-horse.log.txt +0 -13
  371. package/src/main/lib/__tests__/fixtures/scheduler-machine.json.corrupt-1789147548 +0 -34
  372. package/src/main/lib/__tests__/gateFixtures.json +0 -20
  373. package/src/main/lib/__tests__/gitCacheBound.test.cjs +0 -69
  374. package/src/main/lib/__tests__/gitWorktree.test.cjs +0 -1478
  375. package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +0 -107
  376. package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +0 -153
  377. package/src/main/lib/__tests__/guardShims.test.cjs +0 -158
  378. package/src/main/lib/__tests__/importReferences.spec.cjs +0 -56
  379. package/src/main/lib/__tests__/instanceLock.test.cjs +0 -173
  380. package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +0 -78
  381. package/src/main/lib/__tests__/jobWorktree.test.cjs +0 -199
  382. package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +0 -82
  383. package/src/main/lib/__tests__/landedSinceRun.test.cjs +0 -133
  384. package/src/main/lib/__tests__/launchFailure.test.cjs +0 -220
  385. package/src/main/lib/__tests__/loadGate.test.cjs +0 -303
  386. package/src/main/lib/__tests__/localAdminHttp.test.cjs +0 -214
  387. package/src/main/lib/__tests__/loopDelay.test.cjs +0 -68
  388. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +0 -107
  389. package/src/main/lib/__tests__/modelCatalog.test.cjs +0 -202
  390. package/src/main/lib/__tests__/opsOwnership.test.cjs +0 -113
  391. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +0 -328
  392. package/src/main/lib/__tests__/opsRootNestedWrite.test.cjs +0 -51
  393. package/src/main/lib/__tests__/opsRootResolve.test.cjs +0 -149
  394. package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +0 -82
  395. package/src/main/lib/__tests__/prdDisposition.test.cjs +0 -224
  396. package/src/main/lib/__tests__/procIdentity.test.cjs +0 -119
  397. package/src/main/lib/__tests__/procName.test.cjs +0 -92
  398. package/src/main/lib/__tests__/projectBriefCore.test.cjs +0 -216
  399. package/src/main/lib/__tests__/projectRootResolve.test.cjs +0 -148
  400. package/src/main/lib/__tests__/queueHealth.test.cjs +0 -58
  401. package/src/main/lib/__tests__/queueStoreAtomicWrite.test.cjs +0 -88
  402. package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +0 -190
  403. package/src/main/lib/__tests__/quietMachineLease.test.cjs +0 -39
  404. package/src/main/lib/__tests__/rateLimitWindow.test.cjs +0 -88
  405. package/src/main/lib/__tests__/reaperHelpers.test.cjs +0 -577
  406. package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +0 -312
  407. package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +0 -213
  408. package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +0 -125
  409. package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +0 -127
  410. package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +0 -109
  411. package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +0 -71
  412. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +0 -216
  413. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +0 -183
  414. package/src/main/lib/__tests__/schedulerPaths.test.cjs +0 -226
  415. package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +0 -133
  416. package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +0 -56
  417. package/src/main/lib/__tests__/sessionSlots.test.cjs +0 -144
  418. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +0 -626
  419. package/src/main/lib/__tests__/telemetryBoot.test.cjs +0 -134
  420. package/src/main/lib/__tests__/telemetryConsent.test.cjs +0 -136
  421. package/src/main/lib/__tests__/telemetryCounters.test.cjs +0 -57
  422. package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +0 -98
  423. package/src/main/lib/__tests__/terminalRunOutcome.test.cjs +0 -200
  424. package/src/main/lib/__tests__/toolUseClassify.test.cjs +0 -53
  425. package/src/main/lib/__tests__/updateCheck.test.cjs +0 -63
  426. package/src/main/lib/__tests__/upgradeDrain.test.cjs +0 -130
  427. package/src/main/lib/__tests__/usageCircuit.test.cjs +0 -354
  428. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +0 -375
  429. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +0 -266
  430. 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
- });