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,577 +0,0 @@
1
- /**
2
- * reaperHelpers.test.cjs — selectReapableJobs: the pure predicate behind
3
- * reapDeadRunningJobs()'s pidless-zombie fix.
4
- *
5
- * Prior behaviour: a 'running' row with no runtime.pid was skipped forever,
6
- * unconditionally ("spawn may be mid-flight; give it a cycle") — no age
7
- * bound. Repro 2026-09-01: a row sat pidless for 464 minutes against a
8
- * 24-minute estimate, empty run dir, no claude process anywhere — nothing
9
- * pid-bound (deadman, idle watchdog, supervisor) could ever see it.
10
- *
11
- * Run: timeout 120 npx vitest run src/main/lib/__tests__/reaperHelpers.test.cjs
12
- */
13
-
14
- 'use strict';
15
-
16
- // vitest, NOT node:test — this repo's suite is vitest-only (CLAUDE.md).
17
- import { test } from 'vitest';
18
- const assert = require('node:assert/strict');
19
- const fs = require('node:fs');
20
- const os = require('node:os');
21
- const path = require('node:path');
22
- const { spawn } = require('node:child_process');
23
- const {
24
- selectReapableJobs, mapOutcomeToGateOutcome, classifyRunOutcome,
25
- findLiveProcessForJob, logHasOutput, resolvePidlessGateOutcome,
26
- isAlreadySatisfiedOnMain, resolvePidlessFailureOverride,
27
- readSpawnedPidFromLog, isLogFresh,
28
- } = require('../reaperHelpers.cjs');
29
- const { detectRateLimitInLog } = require('../rateLimitDetect.cjs');
30
-
31
- const NOW = Date.parse('2026-09-01T12:00:00.000Z');
32
- const agoMin = (m) => new Date(NOW - m * 60_000).toISOString();
33
- const GRACE = 10 * 60_000;
34
- const alwaysDead = () => false;
35
- const alwaysAlive = () => true;
36
-
37
- test('pidless + older than grace → reaped, with a named reason', () => {
38
- const jobs = [{ slug: 'zombie', status: 'running', startedAt: agoMin(464) }];
39
- const { reapable, warnings } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
40
- assert.strictEqual(warnings.length, 0);
41
- assert.strictEqual(reapable.length, 1);
42
- assert.strictEqual(reapable[0].slug, 'zombie');
43
- assert.strictEqual(reapable[0].pid, null);
44
- assert.strictEqual(reapable[0].pidless, true);
45
- assert.match(reapable[0].reason, /no runtime\.pid recorded after 10m/);
46
- });
47
-
48
- test('pidless + within grace → skipped (the genuine mid-flight-spawn case)', () => {
49
- const jobs = [{ slug: 'fresh', status: 'running', startedAt: agoMin(2) }];
50
- const { reapable, warnings } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
51
- assert.deepStrictEqual(reapable, []);
52
- assert.deepStrictEqual(warnings, []);
53
- });
54
-
55
- test('pidless + exactly at grace boundary → reaped (>= not >)', () => {
56
- const jobs = [{ slug: 'boundary', status: 'running', startedAt: agoMin(10) }];
57
- const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
58
- assert.strictEqual(reapable.length, 1);
59
- });
60
-
61
- test('live pid → skipped regardless of age', () => {
62
- const jobs = [{ slug: 'live', status: 'running', runtime: { pid: 4242 }, startedAt: agoMin(500) }];
63
- const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
64
- assert.deepStrictEqual(reapable, []);
65
- });
66
-
67
- // ---------- dispatchPhase breadcrumb (PRD: dispatch-region diagnostic) ----------
68
-
69
- test('pidless + dispatchPhase present → reason names the phase and timestamp', () => {
70
- const jobs = [{
71
- slug: 'zombie-phase',
72
- status: 'running',
73
- startedAt: agoMin(464),
74
- dispatchPhase: 'baseline-persisted',
75
- dispatchPhaseAt: '2026-09-12T02:46:10.000Z',
76
- }];
77
- const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
78
- assert.strictEqual(reapable.length, 1);
79
- assert.strictEqual(
80
- reapable[0].reason,
81
- 'reaped: no runtime.pid recorded after 10m — spawn never completed '
82
- + '(last dispatch phase: baseline-persisted at 2026-09-12T02:46:10.000Z)',
83
- );
84
- });
85
-
86
- test('pidless + no dispatchPhase (older-build row) → reason byte-identical to the pre-breadcrumb message', () => {
87
- const jobs = [{ slug: 'zombie-noph', status: 'running', startedAt: agoMin(464) }];
88
- const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
89
- assert.strictEqual(reapable.length, 1);
90
- assert.strictEqual(
91
- reapable[0].reason,
92
- 'reaped: no runtime.pid recorded after 10m — spawn never completed',
93
- );
94
- });
95
-
96
- test('pidless + dispatchPhase present but dispatchPhaseAt missing/unparseable → phase named, no "at" clause, never throws', () => {
97
- const jobs = [
98
- { slug: 'zombie-noat', status: 'running', startedAt: agoMin(464), dispatchPhase: 'exec-entered' },
99
- { slug: 'zombie-badat', status: 'running', startedAt: agoMin(464), dispatchPhase: 'exec-entered', dispatchPhaseAt: 'not-a-date' },
100
- ];
101
- const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
102
- assert.strictEqual(reapable.length, 2);
103
- for (const r of reapable) {
104
- assert.strictEqual(
105
- r.reason,
106
- 'reaped: no runtime.pid recorded after 10m — spawn never completed (last dispatch phase: exec-entered)',
107
- );
108
- }
109
- });
110
-
111
- test('pidless + dispatchPhase === "spawned" with no pid → still reapable, breadcrumb changes message only', () => {
112
- const jobs = [{
113
- slug: 'torn-write',
114
- status: 'running',
115
- startedAt: agoMin(464),
116
- dispatchPhase: 'spawned',
117
- dispatchPhaseAt: agoMin(465),
118
- }];
119
- const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
120
- assert.strictEqual(reapable.length, 1);
121
- assert.strictEqual(reapable[0].pidless, true);
122
- assert.match(reapable[0].reason, /last dispatch phase: spawned/);
123
- });
124
-
125
- test('dead pid → reaped exactly as before (pidless: false, no reason string)', () => {
126
- const jobs = [{ slug: 'dead', status: 'running', runtime: { pid: 4242 }, startedAt: agoMin(500) }];
127
- const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysDead, grace: GRACE });
128
- assert.strictEqual(reapable.length, 1);
129
- assert.strictEqual(reapable[0].slug, 'dead');
130
- assert.strictEqual(reapable[0].pid, 4242);
131
- assert.strictEqual(reapable[0].pidless, false);
132
- });
133
-
134
- test('pidless + missing startedAt → skipped and warned, not reaped', () => {
135
- const jobs = [{ slug: 'no-started-at', status: 'running' }];
136
- const { reapable, warnings } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
137
- assert.deepStrictEqual(reapable, []);
138
- assert.strictEqual(warnings.length, 1);
139
- assert.strictEqual(warnings[0].slug, 'no-started-at');
140
- });
141
-
142
- test('pidless + unparseable startedAt → skipped and warned, not reaped', () => {
143
- const jobs = [{ slug: 'bad-started-at', status: 'running', startedAt: 'not-a-date' }];
144
- const { reapable, warnings } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
145
- assert.deepStrictEqual(reapable, []);
146
- assert.strictEqual(warnings.length, 1);
147
- assert.strictEqual(warnings[0].slug, 'bad-started-at');
148
- });
149
-
150
- test('pidless + startedAt in the future (clock skew) → skipped, not reaped', () => {
151
- const jobs = [{ slug: 'future', status: 'running', startedAt: agoMin(-60) }];
152
- const { reapable, warnings } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
153
- assert.deepStrictEqual(reapable, []);
154
- assert.deepStrictEqual(warnings, []);
155
- });
156
-
157
- test('non-running rows are never considered, pidless or not', () => {
158
- const jobs = [
159
- { slug: 'a', status: 'pending', startedAt: agoMin(500) },
160
- { slug: 'b', status: 'completed', startedAt: agoMin(500) },
161
- { slug: 'c', status: 'failed', runtime: { pid: 1 }, startedAt: agoMin(500) },
162
- ];
163
- const { reapable, warnings } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysDead, grace: GRACE });
164
- assert.deepStrictEqual(reapable, []);
165
- assert.deepStrictEqual(warnings, []);
166
- });
167
-
168
- test('pidless + past grace + findLiveProcess finds a pid → recovered, never reaped', () => {
169
- const jobs = [{ slug: 'zombie-alive', status: 'running', startedAt: agoMin(464) }];
170
- const findLiveProcess = () => 2174739;
171
- const { reapable, recovered, warnings } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE, findLiveProcess });
172
- assert.deepStrictEqual(reapable, []);
173
- assert.deepStrictEqual(warnings, []);
174
- assert.strictEqual(recovered.length, 1);
175
- assert.strictEqual(recovered[0].slug, 'zombie-alive');
176
- assert.strictEqual(recovered[0].pid, 2174739);
177
- });
178
-
179
- test('pidless + past grace + findLiveProcess finds nothing → reaped exactly as today', () => {
180
- const jobs = [{ slug: 'zombie-dead', status: 'running', startedAt: agoMin(464) }];
181
- const findLiveProcess = () => null;
182
- const { reapable, recovered } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE, findLiveProcess });
183
- assert.deepStrictEqual(recovered, []);
184
- assert.strictEqual(reapable.length, 1);
185
- assert.strictEqual(reapable[0].pidless, true);
186
- });
187
-
188
- test('pidless + past grace + no findLiveProcess supplied → reaped exactly as before this PRD', () => {
189
- const jobs = [{ slug: 'zombie', status: 'running', startedAt: agoMin(464) }];
190
- const { reapable, recovered } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
191
- assert.strictEqual(reapable.length, 1);
192
- assert.deepStrictEqual(recovered, []);
193
- });
194
-
195
- test('mixed batch: each row classified independently', () => {
196
- const jobs = [
197
- { slug: 'zombie', status: 'running', startedAt: agoMin(464) },
198
- { slug: 'fresh', status: 'running', startedAt: agoMin(2) },
199
- { slug: 'live', status: 'running', runtime: { pid: 1 }, startedAt: agoMin(500) },
200
- { slug: 'dead', status: 'running', runtime: { pid: 2 }, startedAt: agoMin(500) },
201
- { slug: 'bad-started-at', status: 'running', startedAt: 'nope' },
202
- ];
203
- const pidAlive = (pid) => pid === 1;
204
- const { reapable, warnings } = selectReapableJobs(jobs, NOW, { pidAlive, grace: GRACE });
205
- assert.deepStrictEqual(reapable.map((r) => r.slug).sort(), ['dead', 'zombie']);
206
- assert.deepStrictEqual(warnings.map((w) => w.slug), ['bad-started-at']);
207
- });
208
-
209
- // mapOutcomeToGateOutcome — classifyRunOutcome's four-way result collapsed
210
- // onto the persisted `gateOutcome` field (PRD 1109). 'no_result' -> 'never_ran'
211
- // is the one non-obvious mapping: a missing verdict event means the gate
212
- // never fired, a distinct fact from 'failed' (a verdict event exists and
213
- // says error).
214
- test('mapOutcomeToGateOutcome: success -> passed', () => {
215
- assert.strictEqual(mapOutcomeToGateOutcome('success'), 'passed');
216
- });
217
-
218
- test('mapOutcomeToGateOutcome: failed -> failed', () => {
219
- assert.strictEqual(mapOutcomeToGateOutcome('failed'), 'failed');
220
- });
221
-
222
- test('mapOutcomeToGateOutcome: no_result -> never_ran', () => {
223
- assert.strictEqual(mapOutcomeToGateOutcome('no_result'), 'never_ran');
224
- });
225
-
226
- test('mapOutcomeToGateOutcome: unknown -> unknown', () => {
227
- assert.strictEqual(mapOutcomeToGateOutcome('unknown'), 'unknown');
228
- });
229
-
230
- // detectRateLimitInLog / classifyRunOutcome — PRD 1117: the reaper's
231
- // rate-limit detection must be the SAME single source of truth spawnJob
232
- // uses (detectRateLimitInLog, shared via lib/rateLimitDetect.cjs), and a
233
- // rate-limited death must classify as a NEW, distinct 'rate_limited'
234
- // outcome — never collapsed into 'failed', and never re-labelling
235
- // 'success'/'failed'/'no_result' (which keep their prior meanings, per the
236
- // mapOutcomeToGateOutcome tests above, unmodified).
237
-
238
- function writeTmpLog(contents) {
239
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-reaper-rate-limit-test-'));
240
- const p = path.join(dir, 'run.log');
241
- fs.writeFileSync(p, contents);
242
- return p;
243
- }
244
-
245
- test('detectRateLimitInLog: rateLimitType":"five_hour" variant', () => {
246
- const p = writeTmpLog('{"type":"result","rate_limit_info":{"rateLimitType":"five_hour"}}\n');
247
- assert.strictEqual(detectRateLimitInLog(p), true);
248
- });
249
-
250
- test('detectRateLimitInLog: rateLimitType":"seven_day" variant (missed pre-PRD-1117)', () => {
251
- const p = writeTmpLog('{"type":"rate_limit_event","rate_limit_info":{"rateLimitType":"seven_day","unifiedWindows":{"five_hour":{"utilization":0}}}}\n');
252
- assert.strictEqual(detectRateLimitInLog(p), true);
253
- });
254
-
255
- test('detectRateLimitInLog: api_error_status":429 variant', () => {
256
- const p = writeTmpLog('{"type":"result","is_error":true,"api_error_status":429,"result":"nope"}\n');
257
- assert.strictEqual(detectRateLimitInLog(p), true);
258
- });
259
-
260
- test('detectRateLimitInLog: "You\'ve reached your <model> limit" variant (missed pre-PRD-1117)', () => {
261
- const p = writeTmpLog('{"type":"result","result":"You\'ve reached your Fable limit. Switch to another model."}\n');
262
- assert.strictEqual(detectRateLimitInLog(p), true);
263
- });
264
-
265
- test('detectRateLimitInLog: real evidence log (2026-09-05 204-mercury-steam-horse 429)', () => {
266
- const evidencePath = '/home/bilko/.claude/session-manager/scheduled-plans/runs/2026-09-05T16-32-18-381Z/204-mercury-steam-horse.log';
267
- if (!fs.existsSync(evidencePath)) return; // machine-local fixture; skip elsewhere
268
- assert.strictEqual(detectRateLimitInLog(evidencePath), true);
269
- });
270
-
271
- test('classifyRunOutcome: a 429 log tail classifies as the new distinct rate_limited outcome, not failed', () => {
272
- const p = writeTmpLog('{"type":"result","subtype":"success","is_error":true,"api_error_status":429,"rateLimitType":"seven_day","result":"You\'ve reached your Fable limit."}\n');
273
- assert.strictEqual(classifyRunOutcome(p), 'rate_limited');
274
- });
275
-
276
- test('classifyRunOutcome: a genuine error with no rate-limit signal still classifies as failed', () => {
277
- const p = writeTmpLog('{"type":"result","subtype":"error","is_error":true,"result":"Error: expected 2 but got 3"}\n');
278
- assert.strictEqual(classifyRunOutcome(p), 'failed');
279
- });
280
-
281
- // REGRESSION GUARD (2026-09-05). The rate-limit check originally ran as the
282
- // FIRST statement of classifyRunOutcome, before the result event was parsed.
283
- // Because the CLI emits an informational rate_limit_event with
284
- // status:"allowed_warning" on nearly every run once utilization is non-zero,
285
- // that misclassified genuinely successful runs as 'rate_limited' — which in
286
- // reapDeadRunningJobs re-queues finished work and spuriously pauses the whole
287
- // machine. The fixture below is the shape of a REAL successful run log, not a
288
- // synthetic one-liner: the allowed_warning event that every run carries, then
289
- // a clean result. If someone moves the check back to the top, this goes red.
290
- test('classifyRunOutcome: a SUCCESSFUL run whose tail carries an allowed_warning rate_limit_event is still success', () => {
291
- const p = writeTmpLog([
292
- '{"type":"rate_limit_event","rate_limit_info":{"status":"allowed_warning","resetsAt":1788643800,"rateLimitType":"five_hour","utilization":0.42,"isUsingOverage":false}}',
293
- '{"type":"rate_limit_event","rate_limit_info":{"status":"allowed_warning","resetsAt":1789059600,"rateLimitType":"seven_day","utilization":0.59,"isUsingOverage":false}}',
294
- '{"type":"result","subtype":"success","is_error":false,"num_turns":55,"result":"SCHEDULER_VERDICT: PASS"}',
295
- ].join('\n') + '\n');
296
- assert.strictEqual(classifyRunOutcome(p), 'success');
297
- assert.strictEqual(mapOutcomeToGateOutcome(classifyRunOutcome(p)), 'passed');
298
- });
299
-
300
- // The same tail, but the run actually errored: NOW the rate-limit signal is
301
- // what it claims to be, and the outcome must be the retryable one.
302
- test('classifyRunOutcome: the same allowed_warning tail on an ERRORED run classifies as rate_limited', () => {
303
- const p = writeTmpLog([
304
- '{"type":"rate_limit_event","rate_limit_info":{"status":"allowed_warning","rateLimitType":"seven_day","utilization":0.59}}',
305
- '{"type":"result","subtype":"success","is_error":true,"api_error_status":429,"result":"You\'ve reached your Fable limit."}',
306
- ].join('\n') + '\n');
307
- assert.strictEqual(classifyRunOutcome(p), 'rate_limited');
308
- });
309
-
310
- test('classifyRunOutcome: a clean success log still classifies as success', () => {
311
- const p = writeTmpLog('{"type":"result","subtype":"success","is_error":false,"result":"done"}\n');
312
- assert.strictEqual(classifyRunOutcome(p), 'success');
313
- });
314
-
315
- // findLiveProcessForJob — the /proc positive liveness scan behind the
316
- // 2026-09-06 stranded-branch incident (234-uranus-eight-tails-ox: marked
317
- // failed/never_ran while its process was alive and still writing to its
318
- // worktree). Linux-only, and a safe no-op everywhere else per its own header.
319
-
320
- test('findLiveProcessForJob: finds a live process via /proc/<pid>/cwd match', async () => {
321
- if (process.platform !== 'linux' || !fs.existsSync('/proc')) return;
322
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-reaper-liveness-'));
323
- const child = spawn('sleep', ['5'], { cwd: dir, stdio: 'ignore' });
324
- try {
325
- await new Promise((resolve) => setTimeout(resolve, 200));
326
- const pid = findLiveProcessForJob({ slug: 'irrelevant-slug' }, { worktreeDir: dir });
327
- assert.strictEqual(pid, child.pid);
328
- } finally {
329
- child.kill('SIGKILL');
330
- fs.rmSync(dir, { recursive: true, force: true });
331
- }
332
- });
333
-
334
- test('findLiveProcessForJob: no live process anywhere → null', () => {
335
- if (process.platform !== 'linux' || !fs.existsSync('/proc')) return;
336
- const pid = findLiveProcessForJob(
337
- { slug: 'definitely-not-a-real-running-slug-xyz123' },
338
- { worktreeDir: '/nonexistent/worktree/dir/xyz123' },
339
- );
340
- assert.strictEqual(pid, null);
341
- });
342
-
343
- // findLiveProcessForJob: runCwd — the 2026-09-13 in-place/shared-tree fix.
344
- // A run with no dedicated worktree (no worktreeDir) and no slug in argv
345
- // (dispatch never puts it there) is invisible to both existing matches; the
346
- // runCwd+cmdline-claude combo match exists for exactly this shape. `sleep`
347
- // is used as the /proc-visible process here (not a real `claude` binary), so
348
- // this exercises the cwd-plus-cmdline-substring code path directly rather
349
- // than depending on a real claude binary being installed.
350
- test('findLiveProcessForJob: runCwd match requires BOTH cwd AND a claude-looking cmdline — bare cwd match is never sufficient', async () => {
351
- if (process.platform !== 'linux' || !fs.existsSync('/proc')) return;
352
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-reaper-runcwd-'));
353
- // Runs in `dir` but its cmdline has no "claude" in it — must NOT match.
354
- const child = spawn('sleep', ['5'], { cwd: dir, stdio: 'ignore' });
355
- try {
356
- await new Promise((resolve) => setTimeout(resolve, 200));
357
- const pid = findLiveProcessForJob({ slug: 'unrelated-slug' }, { runCwd: dir });
358
- assert.strictEqual(pid, null, 'a bare cwd match without a claude-looking cmdline must never match — shared-tree cross-match hazard');
359
- } finally {
360
- child.kill('SIGKILL');
361
- fs.rmSync(dir, { recursive: true, force: true });
362
- }
363
- });
364
-
365
- // logHasOutput — the literal "did the run dir produce any log output" check
366
- // that gates whether a pidless reap may assert gateOutcome: 'never_ran'.
367
-
368
- test('logHasOutput: missing file → false', () => {
369
- assert.strictEqual(logHasOutput('/nonexistent/path/does-not-exist.log'), false);
370
- });
371
-
372
- test('logHasOutput: empty file → false', () => {
373
- const p = writeTmpLog('');
374
- assert.strictEqual(logHasOutput(p), false);
375
- });
376
-
377
- test('logHasOutput: file with content → true', () => {
378
- const p = writeTmpLog('some real log output\n');
379
- assert.strictEqual(logHasOutput(p), true);
380
- });
381
-
382
- test('logHasOutput: null path → false', () => {
383
- assert.strictEqual(logHasOutput(null), false);
384
- });
385
-
386
- // resolvePidlessGateOutcome — never_ran is asserted ONLY when the run dir has
387
- // no log output; a run dir WITH output must not claim never_ran even if no
388
- // clean result event was found in it.
389
-
390
- test('resolvePidlessGateOutcome: no output → never_ran, regardless of raw outcome (terminalizes as today)', () => {
391
- assert.strictEqual(resolvePidlessGateOutcome('no_result', false), 'never_ran');
392
- assert.strictEqual(resolvePidlessGateOutcome('success', false), 'never_ran');
393
- });
394
-
395
- test('resolvePidlessGateOutcome: output present + no_result → failed, never never_ran', () => {
396
- assert.strictEqual(resolvePidlessGateOutcome('no_result', true), 'failed');
397
- });
398
-
399
- test('resolvePidlessGateOutcome: output present + success → passed', () => {
400
- assert.strictEqual(resolvePidlessGateOutcome('success', true), 'passed');
401
- });
402
-
403
- test('resolvePidlessGateOutcome: output present + failed → failed', () => {
404
- assert.strictEqual(resolvePidlessGateOutcome('failed', true), 'failed');
405
- });
406
-
407
- // isAlreadySatisfiedOnMain — PRD 1136. Pure decision layer: given the
408
- // already-git-queried list of satisfying commits, decide whether the
409
- // commit-guard's 'silent_no_op' verdict should be overridden to 'completed'.
410
-
411
- test('isAlreadySatisfiedOnMain: a satisfying commit → completed-shaped verdict naming the sha', () => {
412
- const verdict = isAlreadySatisfiedOnMain(['5de4134abc123', 'a96de98def456']);
413
- assert.strictEqual(verdict.sha, '5de4134abc123');
414
- assert.strictEqual(verdict.verdict, 'already_satisfied_on_main');
415
- assert.match(verdict.reason, /5de4134abc123/);
416
- });
417
-
418
- test('isAlreadySatisfiedOnMain: no satisfying commits → null — the gate must not be weakened into always-passing', () => {
419
- assert.strictEqual(isAlreadySatisfiedOnMain([]), null);
420
- assert.strictEqual(isAlreadySatisfiedOnMain(null), null);
421
- assert.strictEqual(isAlreadySatisfiedOnMain(undefined), null);
422
- });
423
-
424
- // resolvePidlessFailureOverride — PRD 1173. A pidless reap must never stamp
425
- // 'failed' on a row that already carries proof (landedCommit) that its work
426
- // shipped, purely because reproting bookkeeping (runtime.pid) was lost.
427
-
428
- test('resolvePidlessFailureOverride: row with a landedCommit → override naming the recorded commit sha', () => {
429
- const override = resolvePidlessFailureOverride({ slug: 'shipped-but-pidless', landedCommit: 'f3e35a7f9e49f13a9eb8c338b0d086160e32a87f' });
430
- assert.ok(override, 'a row with landedCommit must produce a failure override');
431
- assert.strictEqual(override.verdict, 'pidless_reap_with_landed_commit');
432
- assert.strictEqual(override.landedCommit, 'f3e35a7f9e49f13a9eb8c338b0d086160e32a87f');
433
- assert.match(override.reason, /f3e35a7f9e49f13a9eb8c338b0d086160e32a87f/, 'the reason string must name the recorded commit sha so a human can go straight to `git show <sha>`');
434
- });
435
-
436
- test('resolvePidlessFailureOverride: row with no landedCommit → null, existing failed transition untouched', () => {
437
- assert.strictEqual(resolvePidlessFailureOverride({ slug: 'genuinely-never-ran' }), null);
438
- assert.strictEqual(resolvePidlessFailureOverride({ slug: 'genuinely-never-ran', landedCommit: null }), null);
439
- assert.strictEqual(resolvePidlessFailureOverride({ slug: 'genuinely-never-ran', landedCommit: '' }), null);
440
- });
441
-
442
- // readSpawnedPidFromLog — recovers the pid a pidless row's spawn actually
443
- // produced from the run's own log (2026-09-13 incident: runId
444
- // 2026-09-13T16-15-56-818Z spawned pid=3138458, logged it, but the
445
- // runtime.pid persistence mutate failed silently and the pidless-grace
446
- // reaper terminalized a live job it could have found here).
447
-
448
- test('readSpawnedPidFromLog: parses the scheduler\'s own spawned-pid line', () => {
449
- const p = writeTmpLog('some preamble\n[scheduler] spawned pid=3138458 sessionId=345ca5b1-c7a7-4167-9b55-f4905e3afe01 (process group)\n\nmore output\n');
450
- assert.strictEqual(readSpawnedPidFromLog(p), 3138458);
451
- });
452
-
453
- test('readSpawnedPidFromLog: missing file → null', () => {
454
- assert.strictEqual(readSpawnedPidFromLog('/nonexistent/path/does-not-exist.log'), null);
455
- });
456
-
457
- test('readSpawnedPidFromLog: empty file → null', () => {
458
- const p = writeTmpLog('');
459
- assert.strictEqual(readSpawnedPidFromLog(p), null);
460
- });
461
-
462
- test('readSpawnedPidFromLog: log with no spawned-pid line → null', () => {
463
- const p = writeTmpLog('{"type":"result","subtype":"success","is_error":false,"result":"done"}\n');
464
- assert.strictEqual(readSpawnedPidFromLog(p), null);
465
- });
466
-
467
- test('readSpawnedPidFromLog: null path → null', () => {
468
- assert.strictEqual(readSpawnedPidFromLog(null), null);
469
- });
470
-
471
- // isLogFresh — pure predicate: a log modified within the window is positive
472
- // proof of life, independent of runtime.pid bookkeeping.
473
-
474
- test('isLogFresh: mtime within window → true', () => {
475
- assert.strictEqual(isLogFresh(NOW - 60_000, NOW, 20 * 60_000), true);
476
- });
477
-
478
- test('isLogFresh: mtime outside window → false', () => {
479
- assert.strictEqual(isLogFresh(NOW - 30 * 60_000, NOW, 20 * 60_000), false);
480
- });
481
-
482
- test('isLogFresh: missing/invalid mtime or window → false', () => {
483
- assert.strictEqual(isLogFresh(null, NOW, 20 * 60_000), false);
484
- assert.strictEqual(isLogFresh(NaN, NOW, 20 * 60_000), false);
485
- assert.strictEqual(isLogFresh(NOW - 60_000, NOW, null), false);
486
- });
487
-
488
- // selectReapableJobs — Part B wiring: a pidless row past grace now consults
489
- // the run log for a pid, and a fresh log skips the row entirely, BEFORE
490
- // falling back to findLiveProcess or the existing terminal reap.
491
-
492
- test('selectReapableJobs: pidless + past grace + log yields a LIVE pid → recovered, never reapable, row stays running', () => {
493
- const jobs = [{ slug: 'zombie-log-alive', status: 'running', startedAt: agoMin(464) }];
494
- const pidAlive = (pid) => pid === 3138458;
495
- const getLogPid = () => 3138458;
496
- const { reapable, recovered, warnings } = selectReapableJobs(jobs, NOW, {
497
- pidAlive, grace: GRACE, getLogPid,
498
- });
499
- assert.deepStrictEqual(reapable, []);
500
- assert.deepStrictEqual(warnings, []);
501
- assert.strictEqual(recovered.length, 1);
502
- assert.strictEqual(recovered[0].slug, 'zombie-log-alive');
503
- assert.strictEqual(recovered[0].pid, 3138458);
504
- });
505
-
506
- test('selectReapableJobs: pidless + past grace + log yields a DEAD pid → still reaps (no regression)', () => {
507
- const jobs = [{ slug: 'zombie-log-dead', status: 'running', startedAt: agoMin(464) }];
508
- const getLogPid = () => 3138458;
509
- const { reapable, recovered } = selectReapableJobs(jobs, NOW, {
510
- pidAlive: alwaysDead, grace: GRACE, getLogPid,
511
- });
512
- assert.deepStrictEqual(recovered, []);
513
- assert.strictEqual(reapable.length, 1);
514
- assert.strictEqual(reapable[0].pidless, true);
515
- });
516
-
517
- test('selectReapableJobs: pidless + past grace + FRESH log mtime → skipped entirely (neither reapable nor recovered)', () => {
518
- const jobs = [{ slug: 'chatty-pidless', status: 'running', startedAt: agoMin(464) }];
519
- const getLogMtimeMs = () => NOW - 60_000; // modified 1 minute ago
520
- const { reapable, recovered, warnings } = selectReapableJobs(jobs, NOW, {
521
- pidAlive: alwaysAlive, grace: GRACE, getLogMtimeMs, logFreshWindowMs: 20 * 60_000,
522
- });
523
- assert.deepStrictEqual(reapable, []);
524
- assert.deepStrictEqual(recovered, []);
525
- assert.strictEqual(warnings.length, 1);
526
- assert.strictEqual(warnings[0].slug, 'chatty-pidless');
527
- assert.match(warnings[0].reason, /log evidence/);
528
- });
529
-
530
- test('selectReapableJobs: pidless + past grace + STALE log mtime + no recoverable pid → reaps with the existing reason string, byte-identical', () => {
531
- const jobs = [{ slug: 'zombie', status: 'running', startedAt: agoMin(464) }];
532
- const getLogMtimeMs = () => NOW - 25 * 60_000; // stale relative to a 20m window
533
- const { reapable } = selectReapableJobs(jobs, NOW, {
534
- pidAlive: alwaysAlive, grace: GRACE, getLogMtimeMs, logFreshWindowMs: 20 * 60_000,
535
- });
536
- assert.strictEqual(reapable.length, 1);
537
- assert.strictEqual(reapable[0].reason, 'reaped: no runtime.pid recorded after 10m — spawn never completed');
538
- });
539
-
540
- test('selectReapableJobs: a row WITH a live runtime.pid is untouched by every new code path', () => {
541
- const jobs = [{ slug: 'live', status: 'running', runtime: { pid: 1 }, startedAt: agoMin(500) }];
542
- const getLogPid = () => { throw new Error('must never be called for a live-pid row'); };
543
- const getLogMtimeMs = () => { throw new Error('must never be called for a live-pid row'); };
544
- const { reapable, recovered, warnings } = selectReapableJobs(jobs, NOW, {
545
- pidAlive: alwaysAlive, grace: GRACE, getLogPid, getLogMtimeMs, logFreshWindowMs: 20 * 60_000,
546
- });
547
- assert.deepStrictEqual(reapable, []);
548
- assert.deepStrictEqual(recovered, []);
549
- assert.deepStrictEqual(warnings, []);
550
- });
551
-
552
- test('selectReapableJobs: pidless + past grace + landedCommit on the row → reapable entry carries a failureOverride', () => {
553
- const jobs = [{
554
- slug: 'shipped-but-pidless',
555
- status: 'running',
556
- startedAt: agoMin(464),
557
- landedCommit: 'f3e35a7f9e49f13a9eb8c338b0d086160e32a87f',
558
- }];
559
- const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
560
- assert.strictEqual(reapable.length, 1);
561
- assert.strictEqual(reapable[0].pidless, true);
562
- assert.ok(reapable[0].failureOverride, 'the reapable entry must carry the evidence-based override');
563
- assert.strictEqual(reapable[0].failureOverride.landedCommit, 'f3e35a7f9e49f13a9eb8c338b0d086160e32a87f');
564
- // The existing byte-for-byte reason string is untouched by this PRD — the
565
- // override is carried as a separate field, not folded into `reason`.
566
- assert.strictEqual(
567
- reapable[0].reason,
568
- 'reaped: no runtime.pid recorded after 10m — spawn never completed',
569
- );
570
- });
571
-
572
- test('selectReapableJobs: pidless + past grace + NO landedCommit on the row → failureOverride is null, unchanged from today', () => {
573
- const jobs = [{ slug: 'genuinely-never-ran', status: 'running', startedAt: agoMin(464) }];
574
- const { reapable } = selectReapableJobs(jobs, NOW, { pidAlive: alwaysAlive, grace: GRACE });
575
- assert.strictEqual(reapable.length, 1);
576
- assert.strictEqual(reapable[0].failureOverride, null);
577
- });