claude-code-session-manager 0.98.0 → 0.101.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 (441) 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-CXaH2GED.js +1 -0
  5. package/dist/assets/{History-Bfdoywlh.js → History-Cerii-HA.js} +2 -2
  6. package/dist/assets/{Hooks-CKDJjTJV.js → Hooks-aXMb5LDy.js} +3 -3
  7. package/dist/assets/{Library-7akfgy67.js → Library-WrJFKEOJ.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-DDesiqet.js → MarkdownEditor-D1BMCXS7.js} +1 -1
  9. package/dist/assets/{McpServers-D4lZtXof.js → McpServers-Cf0davLd.js} +2 -2
  10. package/dist/assets/{Memory-CeI8G1w5.js → Memory-Dn6NHWPe.js} +6 -6
  11. package/dist/assets/Permissions-CTVuFPjg.js +3 -0
  12. package/dist/assets/{Plugins-P4eCXy_S.js → Plugins-C2TyF8Mn.js} +2 -2
  13. package/dist/assets/ProvenanceBadge-F_jwHSwE.js +1 -0
  14. package/dist/assets/{SaveBar-l9jnEMES.js → SaveBar-DA1KVJST.js} +1 -1
  15. package/dist/assets/Scheduler-Bm6Q_hlu.js +16 -0
  16. package/dist/assets/{ScopeSwitcher-CjJi8ST5.js → ScopeSwitcher-BygZlJca.js} +1 -1
  17. package/dist/assets/Settings-BYCiv5KV.js +3 -0
  18. package/dist/assets/{SkillReferenceGraph-CjbO01Sg.js → SkillReferenceGraph-CcLnl9np.js} +1 -1
  19. package/dist/assets/{Skills-BxZ9_fCR.js → Skills-ChffsdTD.js} +2 -2
  20. package/dist/assets/{SystemPrompt-D8vfFb8b.js → SystemPrompt-dNkfHE_M.js} +1 -1
  21. package/dist/assets/{TagLibrary-2vzUo7YL.js → TagLibrary-jVlEuB0m.js} +1 -1
  22. package/dist/assets/{TiptapBody-B8ZfbK90.js → TiptapBody-6_cdwcP0.js} +1 -1
  23. package/dist/assets/{Toggle-BbW4nGGf.js → Toggle-CVXqGpAP.js} +1 -1
  24. package/dist/assets/ViewTabs-DO1YVbpc.js +1 -0
  25. package/dist/assets/{index-CKH5Uxik.css → index-D89EUizB.css} +1 -1
  26. package/dist/assets/{index-Cd98Q1lD.js → index-DbCQPWWY.js} +747 -752
  27. package/dist/assets/{settingsSchema-IhUfucCF.js → settingsSchema-Wrvn-Yz1.js} +1 -1
  28. package/dist/index.html +2 -2
  29. package/package.json +25 -29
  30. package/plugins/CLAUDE.md +6 -6
  31. package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +21 -19
  32. package/plugins/session-manager-dev/skills/builder/SKILL.md +3 -3
  33. package/plugins/session-manager-dev/skills/develop/SKILL.md +254 -510
  34. package/plugins/session-manager-dev/skills/develop/standards.md +17 -22
  35. package/scripts/README.md +3 -11
  36. package/scripts/audit-ops-hygiene.cjs +3 -3
  37. package/scripts/hooks/lib/guard-prd-writes-policy.cjs +1 -1
  38. package/scripts/hooks/lib/guard-self-schedule-policy.cjs +1 -1
  39. package/scripts/mint-epic.cjs +2 -3
  40. package/scripts/scheduler-mcp-server.cjs +64 -7
  41. package/src/main/agentLibrary.cjs +22 -2
  42. package/src/main/build-info.json +4 -4
  43. package/src/main/chatRunner.cjs +4 -3
  44. package/src/main/config.cjs +41 -38
  45. package/src/main/docEdit.cjs +4 -1
  46. package/src/main/files.cjs +2 -5
  47. package/src/main/health.cjs +1 -1
  48. package/src/main/index.cjs +40 -11
  49. package/src/main/ipcSchemas.cjs +35 -34
  50. package/src/main/lib/agentPersonaSchema.cjs +13 -2
  51. package/src/main/lib/atomicFs.cjs +116 -0
  52. package/src/main/lib/branchSweep.cjs +13 -12
  53. package/src/main/lib/buildTarget.cjs +3 -3
  54. package/src/main/lib/builtinMacros.cjs +39 -0
  55. package/src/main/lib/claudeCliCaps.cjs +129 -0
  56. package/src/main/lib/credentials.cjs +2 -4
  57. package/src/main/lib/crossProjectFeedback.cjs +3 -3
  58. package/src/main/lib/cwdClassify.cjs +15 -3
  59. package/src/main/lib/definitionOfDone.cjs +207 -125
  60. package/src/main/lib/dodDrainHook.cjs +11 -5
  61. package/src/main/lib/epicMint.cjs +2 -4
  62. package/src/main/lib/epicStatusMirror.cjs +8 -10
  63. package/src/main/lib/epicWorktreeProjectConfig.cjs +2 -4
  64. package/src/main/lib/gateAuthority.cjs +96 -0
  65. package/src/main/lib/gitExec.cjs +69 -0
  66. package/src/main/lib/gitWorktree.cjs +656 -92
  67. package/src/main/lib/instanceLock.cjs +5 -8
  68. package/src/main/lib/launchFailure.cjs +2 -3
  69. package/src/main/lib/macroLibrary.cjs +480 -0
  70. package/src/main/lib/mcpToolCatalog.cjs +49 -17
  71. package/src/main/lib/needsReviewLedger.cjs +1 -1
  72. package/src/main/lib/opsErrorLog.cjs +11 -1
  73. package/src/main/lib/opsOwnership.cjs +5 -9
  74. package/src/main/lib/pidAlive.cjs +32 -0
  75. package/src/main/lib/prdCreate.cjs +121 -12
  76. package/src/main/lib/prdDisposition.cjs +2 -2
  77. package/src/main/lib/prdGateFiles.cjs +284 -0
  78. package/src/main/lib/prdLocations.cjs +61 -0
  79. package/src/main/lib/prdMigration.cjs +2 -3
  80. package/src/main/lib/prdSizing.cjs +2 -1
  81. package/src/main/lib/projectHomeAdminRoutes.cjs +136 -21
  82. package/src/main/lib/promptSessionSchema.cjs +10 -2
  83. package/src/main/lib/rcaReport.cjs +4 -6
  84. package/src/main/lib/reaperHelpers.cjs +8 -1
  85. package/src/main/lib/reservationExpiry.cjs +6 -1
  86. package/src/main/lib/reviewNotice.cjs +263 -0
  87. package/src/main/lib/runClaudeP.cjs +4 -1
  88. package/src/main/lib/schedulerPaths.cjs +22 -2
  89. package/src/main/lib/sessionSlots.cjs +2 -4
  90. package/src/main/lib/shippedPersonaSeeds.cjs +226 -0
  91. package/src/main/lib/smfileHeaders.cjs +66 -0
  92. package/src/main/lib/timeoutShim.cjs +123 -0
  93. package/src/main/lib/timeoutShimScript.cjs +275 -0
  94. package/src/main/lib/upgradeDrain.cjs +4 -10
  95. package/src/main/lib/watchdogHelpers.cjs +12 -25
  96. package/src/main/lib/workTypeLibrary.cjs +7 -1
  97. package/src/main/projectPages.cjs +43 -13
  98. package/src/main/promptSessionEvents.cjs +37 -13
  99. package/src/main/scheduler.cjs +910 -198
  100. package/src/main/seedAgentPersonas.cjs +279 -24
  101. package/src/main/supervisor.cjs +4 -1
  102. package/src/main/templates/PRD_AUTHORING.md +126 -406
  103. package/src/preload/__tests__/preload-surface.test.cjs +68 -0
  104. package/src/preload/api.d.ts +53 -94
  105. package/src/preload/index.cjs +14 -9
  106. package/src/seed/agents/architect.md +2 -0
  107. package/src/seed/agents/demo-video-builder.md +84 -0
  108. package/src/seed/agents/dev-lead.md +18 -18
  109. package/src/seed/agents/project-home-builder.md +66 -35
  110. package/src/seed/agents/validator.md +10 -4
  111. package/dist/assets/DataModel-DCEgMTjH.js +0 -1
  112. package/dist/assets/HostBilko-CDVHE0hT.js +0 -1
  113. package/dist/assets/Permissions-BGleLja4.js +0 -3
  114. package/dist/assets/ProvenanceBadge-BFczzI1B.js +0 -1
  115. package/dist/assets/Scheduler-C_phpeUS.js +0 -16
  116. package/dist/assets/Settings-BIwZ3FGQ.js +0 -3
  117. package/src/main/__tests__/activeIndexMerge.test.cjs +0 -235
  118. package/src/main/__tests__/agentEffortResolve.test.cjs +0 -117
  119. package/src/main/__tests__/agentLibrary.test.cjs +0 -276
  120. package/src/main/__tests__/agentModelResolve.test.cjs +0 -311
  121. package/src/main/__tests__/agentOverlayWrite.test.cjs +0 -95
  122. package/src/main/__tests__/bilkoHost-deriveSlug.test.cjs +0 -26
  123. package/src/main/__tests__/bilkoHost-integration.test.cjs +0 -118
  124. package/src/main/__tests__/bilkoHostCore.test.cjs +0 -72
  125. package/src/main/__tests__/broadcastCoalescer.test.cjs +0 -122
  126. package/src/main/__tests__/chat-cancel-terminal.test.cjs +0 -120
  127. package/src/main/__tests__/chat-dead-channels.test.cjs +0 -63
  128. package/src/main/__tests__/chat-exit-close-race.test.cjs +0 -146
  129. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +0 -139
  130. package/src/main/__tests__/chat-preamble-anchors.test.cjs +0 -100
  131. package/src/main/__tests__/chat-queue.test.cjs +0 -97
  132. package/src/main/__tests__/chat-stop-signal.test.cjs +0 -89
  133. package/src/main/__tests__/chatRunner-epic-worktree-execcwd.test.cjs +0 -125
  134. package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +0 -252
  135. package/src/main/__tests__/classifyPromptTicket.test.cjs +0 -101
  136. package/src/main/__tests__/classifyTranscriptLine.test.cjs +0 -201
  137. package/src/main/__tests__/computeDepHistorySatisfaction.test.cjs +0 -66
  138. package/src/main/__tests__/config-readText-bounded.test.cjs +0 -84
  139. package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +0 -59
  140. package/src/main/__tests__/crossProjectFeedback.test.cjs +0 -334
  141. package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +0 -161
  142. package/src/main/__tests__/dep-orphan-archive-health.test.cjs +0 -77
  143. package/src/main/__tests__/develop-skill-failure-modes.test.cjs +0 -70
  144. package/src/main/__tests__/docEdit.test.cjs +0 -244
  145. package/src/main/__tests__/dod-batchkey.test.cjs +0 -183
  146. package/src/main/__tests__/dod-drain-hook.test.cjs +0 -302
  147. package/src/main/__tests__/dod-report.test.cjs +0 -304
  148. package/src/main/__tests__/dod-reverify.test.cjs +0 -285
  149. package/src/main/__tests__/epicContextDigest.test.cjs +0 -174
  150. package/src/main/__tests__/epicMint.test.cjs +0 -332
  151. package/src/main/__tests__/epicMintTelemetryTap.test.cjs +0 -64
  152. package/src/main/__tests__/epicStatusMirror.test.cjs +0 -110
  153. package/src/main/__tests__/epicValidationHook.test.cjs +0 -291
  154. package/src/main/__tests__/exchanges.test.cjs +0 -122
  155. package/src/main/__tests__/exchangesPromptId.test.cjs +0 -61
  156. package/src/main/__tests__/extractJson.test.cjs +0 -51
  157. package/src/main/__tests__/files-reject-credentials.test.cjs +0 -40
  158. package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +0 -556
  159. package/src/main/__tests__/flatPrdTickSweep.test.cjs +0 -110
  160. package/src/main/__tests__/health-build-freshness.test.cjs +0 -39
  161. package/src/main/__tests__/health-claude-md-budget.test.cjs +0 -57
  162. package/src/main/__tests__/health-credentials.test.cjs +0 -81
  163. package/src/main/__tests__/health-delegation-chain.test.cjs +0 -124
  164. package/src/main/__tests__/health-per-project-stall.test.cjs +0 -84
  165. package/src/main/__tests__/health-prd-migration.test.cjs +0 -37
  166. package/src/main/__tests__/health-queue-dispatch.test.cjs +0 -135
  167. package/src/main/__tests__/health-starve-escalation.test.cjs +0 -94
  168. package/src/main/__tests__/health-tick-liveness.test.cjs +0 -179
  169. package/src/main/__tests__/health-usage-poller.test.cjs +0 -144
  170. package/src/main/__tests__/health-worktree-cap-blocked.test.cjs +0 -65
  171. package/src/main/__tests__/heapSnapshot.test.cjs +0 -121
  172. package/src/main/__tests__/historyAggregatorIntraday.test.cjs +0 -313
  173. package/src/main/__tests__/historyDashboard.test.cjs +0 -163
  174. package/src/main/__tests__/historyRollup.test.cjs +0 -333
  175. package/src/main/__tests__/intradayRefresh.test.cjs +0 -39
  176. package/src/main/__tests__/ipcSchemas-dependsOn.test.cjs +0 -39
  177. package/src/main/__tests__/kg-augment.test.cjs +0 -195
  178. package/src/main/__tests__/loadGateDetailTick.test.cjs +0 -31
  179. package/src/main/__tests__/machineProfile.test.cjs +0 -152
  180. package/src/main/__tests__/mcpStatus.test.cjs +0 -61
  181. package/src/main/__tests__/memoryAggregate.test.cjs +0 -109
  182. package/src/main/__tests__/memoryStale.test.cjs +0 -88
  183. package/src/main/__tests__/needsReviewLedger.test.cjs +0 -162
  184. package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +0 -25
  185. package/src/main/__tests__/opsErrorLog.test.cjs +0 -109
  186. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +0 -173
  187. package/src/main/__tests__/personaMerge.test.cjs +0 -169
  188. package/src/main/__tests__/planValidator.test.cjs +0 -125
  189. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +0 -176
  190. package/src/main/__tests__/prd-group-allocator.test.cjs +0 -119
  191. package/src/main/__tests__/prdAdminRouteParity.test.cjs +0 -70
  192. package/src/main/__tests__/prdAdminRoutes.test.cjs +0 -718
  193. package/src/main/__tests__/prdAgentType.test.cjs +0 -103
  194. package/src/main/__tests__/prdAuthoringSeed.test.cjs +0 -39
  195. package/src/main/__tests__/prdCreate.test.cjs +0 -979
  196. package/src/main/__tests__/prdCreateAdoption.test.cjs +0 -205
  197. package/src/main/__tests__/prdCreateDisposition.test.cjs +0 -201
  198. package/src/main/__tests__/prdCreatePlanId.test.cjs +0 -132
  199. package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +0 -117
  200. package/src/main/__tests__/prdFrontmatterDependsOn.test.cjs +0 -136
  201. package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +0 -125
  202. package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +0 -108
  203. package/src/main/__tests__/prdLocations.test.cjs +0 -195
  204. package/src/main/__tests__/prdLocationsArchived.test.cjs +0 -201
  205. package/src/main/__tests__/prdMigration.test.cjs +0 -349
  206. package/src/main/__tests__/prdMigrationLegacyAdopt.test.cjs +0 -91
  207. package/src/main/__tests__/prdParserHighWater.test.cjs +0 -74
  208. package/src/main/__tests__/prdParserSourcePromptId.test.cjs +0 -65
  209. package/src/main/__tests__/prdSetDisposition.test.cjs +0 -222
  210. package/src/main/__tests__/prdSizing.test.cjs +0 -106
  211. package/src/main/__tests__/prdSourcePromptIdBackfill.test.cjs +0 -118
  212. package/src/main/__tests__/prdUpdateDependsOn.test.cjs +0 -160
  213. package/src/main/__tests__/proc-role-env.test.cjs +0 -125
  214. package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +0 -304
  215. package/src/main/__tests__/procname-sm-processes.test.cjs +0 -127
  216. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +0 -177
  217. package/src/main/__tests__/projectPages.test.cjs +0 -137
  218. package/src/main/__tests__/promptSessionEvents.test.cjs +0 -234
  219. package/src/main/__tests__/promptSessionSchema.test.cjs +0 -101
  220. package/src/main/__tests__/promptSessionTranscript.test.cjs +0 -0
  221. package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +0 -159
  222. package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +0 -282
  223. package/src/main/__tests__/pty-session-open-telemetry.test.cjs +0 -96
  224. package/src/main/__tests__/pty-write-result.test.cjs +0 -46
  225. package/src/main/__tests__/queue-health-verdict.test.cjs +0 -170
  226. package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +0 -286
  227. package/src/main/__tests__/queue-starvation-per-project.test.cjs +0 -147
  228. package/src/main/__tests__/queueHistory.test.cjs +0 -355
  229. package/src/main/__tests__/queueOps-interactive-ac-lint.test.cjs +0 -65
  230. package/src/main/__tests__/queueOpsArchiveDestination.test.cjs +0 -65
  231. package/src/main/__tests__/queueOpsAutoArchive.test.cjs +0 -211
  232. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +0 -128
  233. package/src/main/__tests__/rcaReport.test.cjs +0 -266
  234. package/src/main/__tests__/reconcileFlatPrdSweep.test.cjs +0 -119
  235. package/src/main/__tests__/reconcileTiming.test.cjs +0 -135
  236. package/src/main/__tests__/runLogRetention.test.cjs +0 -489
  237. package/src/main/__tests__/runVerify-atomic-verdicts.test.cjs +0 -26
  238. package/src/main/__tests__/runVerify-blocked-by-foreign-wip.test.cjs +0 -58
  239. package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +0 -191
  240. package/src/main/__tests__/runVerify-policy-denial.test.cjs +0 -89
  241. package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +0 -225
  242. package/src/main/__tests__/runVerify.test.cjs +0 -1784
  243. package/src/main/__tests__/scheduleJobSchema.test.cjs +0 -127
  244. package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +0 -65
  245. package/src/main/__tests__/scheduleJobTransitions.test.cjs +0 -277
  246. package/src/main/__tests__/scheduleJobTransitionsGrep.test.cjs +0 -59
  247. package/src/main/__tests__/scheduleJobTransitionsTelemetryTap.test.cjs +0 -72
  248. package/src/main/__tests__/scheduler-admin-routes.test.cjs +0 -199
  249. package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +0 -143
  250. package/src/main/__tests__/scheduler-already-satisfied-on-main.test.cjs +0 -105
  251. package/src/main/__tests__/scheduler-archive-completed-prd.test.cjs +0 -102
  252. package/src/main/__tests__/scheduler-archived-twin-guard.test.cjs +0 -155
  253. package/src/main/__tests__/scheduler-autofix-outcome.test.cjs +0 -188
  254. package/src/main/__tests__/scheduler-autofix-select.test.cjs +0 -439
  255. package/src/main/__tests__/scheduler-autopromote.test.cjs +0 -51
  256. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +0 -101
  257. package/src/main/__tests__/scheduler-blocked-by-foreign-wip.test.cjs +0 -107
  258. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +0 -153
  259. package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +0 -121
  260. package/src/main/__tests__/scheduler-clear-queue-history.test.cjs +0 -134
  261. package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +0 -244
  262. package/src/main/__tests__/scheduler-committed-in-window.test.cjs +0 -182
  263. package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +0 -43
  264. package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +0 -168
  265. package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +0 -58
  266. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +0 -81
  267. package/src/main/__tests__/scheduler-epic-digest.test.cjs +0 -227
  268. package/src/main/__tests__/scheduler-failed-autoreset.test.cjs +0 -121
  269. package/src/main/__tests__/scheduler-finalize-dispatch-guards.test.cjs +0 -229
  270. package/src/main/__tests__/scheduler-find-prd-dir.test.cjs +0 -75
  271. package/src/main/__tests__/scheduler-fix-plan-path.test.cjs +0 -119
  272. package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +0 -46
  273. package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +0 -78
  274. package/src/main/__tests__/scheduler-gate-shadow.test.cjs +0 -119
  275. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +0 -390
  276. package/src/main/__tests__/scheduler-heal-refusal.test.cjs +0 -61
  277. package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +0 -80
  278. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +0 -323
  279. package/src/main/__tests__/scheduler-integration-failure-stamp.test.cjs +0 -41
  280. package/src/main/__tests__/scheduler-investigation-clean-skip.test.cjs +0 -63
  281. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +0 -123
  282. package/src/main/__tests__/scheduler-job-budget.test.cjs +0 -172
  283. package/src/main/__tests__/scheduler-job-overrun.test.cjs +0 -175
  284. package/src/main/__tests__/scheduler-launch-failure.test.cjs +0 -199
  285. package/src/main/__tests__/scheduler-leftover-fields.test.cjs +0 -52
  286. package/src/main/__tests__/scheduler-leftover-quarantine.test.cjs +0 -199
  287. package/src/main/__tests__/scheduler-looks-done.test.cjs +0 -537
  288. package/src/main/__tests__/scheduler-manual-pause.test.cjs +0 -118
  289. package/src/main/__tests__/scheduler-mechanical-recovery.test.cjs +0 -245
  290. package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +0 -46
  291. package/src/main/__tests__/scheduler-mutate-reentrancy.test.cjs +0 -117
  292. package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +0 -197
  293. package/src/main/__tests__/scheduler-never-stop.test.cjs +0 -157
  294. package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +0 -152
  295. package/src/main/__tests__/scheduler-no-orphan-run-dir.test.cjs +0 -81
  296. package/src/main/__tests__/scheduler-notify-originating-tab-transcript.test.cjs +0 -87
  297. package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +0 -343
  298. package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +0 -217
  299. package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +0 -164
  300. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +0 -161
  301. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +0 -178
  302. package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +0 -165
  303. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +0 -257
  304. package/src/main/__tests__/scheduler-rate-limit-cooldown-freshness.test.cjs +0 -123
  305. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +0 -152
  306. package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +0 -156
  307. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +0 -754
  308. package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +0 -87
  309. package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +0 -100
  310. package/src/main/__tests__/scheduler-reconcile-history-backfill.test.cjs +0 -105
  311. package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +0 -203
  312. package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +0 -247
  313. package/src/main/__tests__/scheduler-reset-job-fields-guard.test.cjs +0 -77
  314. package/src/main/__tests__/scheduler-resume-recovery.test.cjs +0 -254
  315. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +0 -115
  316. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +0 -380
  317. package/src/main/__tests__/scheduler-sigterm-commit.test.cjs +0 -43
  318. package/src/main/__tests__/scheduler-stall-per-project.test.cjs +0 -126
  319. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +0 -154
  320. package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +0 -252
  321. package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +0 -185
  322. package/src/main/__tests__/scheduler-stuck-failed-escalation.test.cjs +0 -136
  323. package/src/main/__tests__/scheduler-supervisor-record.test.cjs +0 -81
  324. package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +0 -54
  325. package/src/main/__tests__/scheduler-tick-wedge.test.cjs +0 -172
  326. package/src/main/__tests__/scheduler-transient-failure.test.cjs +0 -141
  327. package/src/main/__tests__/scheduler-unreadable-queue-guard.test.cjs +0 -62
  328. package/src/main/__tests__/scheduler-utilization-hold.test.cjs +0 -89
  329. package/src/main/__tests__/scheduler-verify-prd-path.test.cjs +0 -109
  330. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +0 -234
  331. package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +0 -120
  332. package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +0 -105
  333. package/src/main/__tests__/schedulerBatchRootBlocker.test.cjs +0 -117
  334. package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +0 -110
  335. package/src/main/__tests__/seedAgentPersonas.test.cjs +0 -184
  336. package/src/main/__tests__/seedSchedulerMcp.test.cjs +0 -211
  337. package/src/main/__tests__/seedStatus.test.cjs +0 -100
  338. package/src/main/__tests__/seedValidatorPersona.test.cjs +0 -116
  339. package/src/main/__tests__/stop-signal-anchor.test.cjs +0 -75
  340. package/src/main/__tests__/telemetryClient.test.cjs +0 -1055
  341. package/src/main/__tests__/telemetryContract.test.cjs +0 -930
  342. package/src/main/__tests__/telemetrySettings.test.cjs +0 -210
  343. package/src/main/__tests__/transcripts-batch-flush.test.cjs +0 -249
  344. package/src/main/__tests__/transcripts-doFlush-array.test.cjs +0 -124
  345. package/src/main/__tests__/transcripts-paged-reads.test.cjs +0 -241
  346. package/src/main/__tests__/transcripts-worktree-epic-path.test.cjs +0 -154
  347. package/src/main/__tests__/transcriptsUsageFor.test.cjs +0 -206
  348. package/src/main/__tests__/uniquePrdNumbers.test.cjs +0 -153
  349. package/src/main/__tests__/usageSingleFlight.test.cjs +0 -169
  350. package/src/main/__tests__/validationSentinels.test.cjs +0 -84
  351. package/src/main/__tests__/workTypeLibrary.test.cjs +0 -89
  352. package/src/main/bilkoHost.cjs +0 -314
  353. package/src/main/bilkoHostCore.cjs +0 -89
  354. package/src/main/lib/__tests__/active-sessions.test.cjs +0 -251
  355. package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +0 -179
  356. package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +0 -67
  357. package/src/main/lib/__tests__/auditLog.test.cjs +0 -38
  358. package/src/main/lib/__tests__/bootSelfHeal.test.cjs +0 -107
  359. package/src/main/lib/__tests__/branchSweep.test.cjs +0 -164
  360. package/src/main/lib/__tests__/buildIdentity.test.cjs +0 -121
  361. package/src/main/lib/__tests__/buildTarget.test.cjs +0 -52
  362. package/src/main/lib/__tests__/childWithLog.test.cjs +0 -321
  363. package/src/main/lib/__tests__/coldBootPromptSessionsWrite.test.cjs +0 -87
  364. package/src/main/lib/__tests__/crashTelemetry.test.cjs +0 -103
  365. package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +0 -115
  366. package/src/main/lib/__tests__/cwdClassify.test.cjs +0 -111
  367. package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +0 -95
  368. package/src/main/lib/__tests__/delegationReadiness.test.cjs +0 -1175
  369. package/src/main/lib/__tests__/dispatchLoop.test.cjs +0 -63
  370. package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +0 -244
  371. package/src/main/lib/__tests__/ephemeralCwd.test.cjs +0 -91
  372. package/src/main/lib/__tests__/epicDelegationStats.test.cjs +0 -137
  373. package/src/main/lib/__tests__/epicSpawnCwd.test.cjs +0 -283
  374. package/src/main/lib/__tests__/epicSpawnPlan.test.cjs +0 -196
  375. package/src/main/lib/__tests__/epicTranscriptPath.test.cjs +0 -163
  376. package/src/main/lib/__tests__/epicWorktreeBoot.test.cjs +0 -136
  377. package/src/main/lib/__tests__/epicWorktreeMerge.test.cjs +0 -130
  378. package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +0 -117
  379. package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +0 -137
  380. package/src/main/lib/__tests__/fixChainDepth.test.cjs +0 -40
  381. package/src/main/lib/__tests__/fixtures/204-mercury-steam-horse.log.txt +0 -13
  382. package/src/main/lib/__tests__/fixtures/scheduler-machine.json.corrupt-1789147548 +0 -34
  383. package/src/main/lib/__tests__/gateFixtures.json +0 -20
  384. package/src/main/lib/__tests__/gitCacheBound.test.cjs +0 -69
  385. package/src/main/lib/__tests__/gitWorktree.test.cjs +0 -1478
  386. package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +0 -107
  387. package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +0 -153
  388. package/src/main/lib/__tests__/guardShims.test.cjs +0 -158
  389. package/src/main/lib/__tests__/importReferences.spec.cjs +0 -56
  390. package/src/main/lib/__tests__/instanceLock.test.cjs +0 -173
  391. package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +0 -78
  392. package/src/main/lib/__tests__/jobWorktree.test.cjs +0 -199
  393. package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +0 -82
  394. package/src/main/lib/__tests__/landedSinceRun.test.cjs +0 -133
  395. package/src/main/lib/__tests__/launchFailure.test.cjs +0 -220
  396. package/src/main/lib/__tests__/loadGate.test.cjs +0 -303
  397. package/src/main/lib/__tests__/localAdminHttp.test.cjs +0 -214
  398. package/src/main/lib/__tests__/loopDelay.test.cjs +0 -68
  399. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +0 -107
  400. package/src/main/lib/__tests__/modelCatalog.test.cjs +0 -202
  401. package/src/main/lib/__tests__/opsOwnership.test.cjs +0 -113
  402. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +0 -328
  403. package/src/main/lib/__tests__/opsRootNestedWrite.test.cjs +0 -51
  404. package/src/main/lib/__tests__/opsRootResolve.test.cjs +0 -149
  405. package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +0 -82
  406. package/src/main/lib/__tests__/prdDisposition.test.cjs +0 -224
  407. package/src/main/lib/__tests__/procIdentity.test.cjs +0 -119
  408. package/src/main/lib/__tests__/procName.test.cjs +0 -92
  409. package/src/main/lib/__tests__/projectBriefCore.test.cjs +0 -216
  410. package/src/main/lib/__tests__/projectRootResolve.test.cjs +0 -148
  411. package/src/main/lib/__tests__/queueHealth.test.cjs +0 -58
  412. package/src/main/lib/__tests__/queueStoreAtomicWrite.test.cjs +0 -88
  413. package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +0 -190
  414. package/src/main/lib/__tests__/quietMachineLease.test.cjs +0 -39
  415. package/src/main/lib/__tests__/rateLimitWindow.test.cjs +0 -88
  416. package/src/main/lib/__tests__/reaperHelpers.test.cjs +0 -577
  417. package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +0 -312
  418. package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +0 -213
  419. package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +0 -125
  420. package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +0 -127
  421. package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +0 -109
  422. package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +0 -71
  423. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +0 -216
  424. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +0 -183
  425. package/src/main/lib/__tests__/schedulerPaths.test.cjs +0 -226
  426. package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +0 -133
  427. package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +0 -56
  428. package/src/main/lib/__tests__/sessionSlots.test.cjs +0 -144
  429. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +0 -626
  430. package/src/main/lib/__tests__/telemetryBoot.test.cjs +0 -134
  431. package/src/main/lib/__tests__/telemetryConsent.test.cjs +0 -136
  432. package/src/main/lib/__tests__/telemetryCounters.test.cjs +0 -57
  433. package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +0 -98
  434. package/src/main/lib/__tests__/terminalRunOutcome.test.cjs +0 -200
  435. package/src/main/lib/__tests__/toolUseClassify.test.cjs +0 -53
  436. package/src/main/lib/__tests__/updateCheck.test.cjs +0 -63
  437. package/src/main/lib/__tests__/upgradeDrain.test.cjs +0 -130
  438. package/src/main/lib/__tests__/usageCircuit.test.cjs +0 -354
  439. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +0 -441
  440. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +0 -266
  441. 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
- });