claude-code-session-manager 0.86.0 → 0.88.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 (335) hide show
  1. package/README.md +26 -53
  2. package/dist/assets/AgentLibrary-BZ6IY2g1.js +3 -0
  3. package/dist/assets/{DataModel-Q4jhl24R.js → DataModel-dAoGAODX.js} +1 -1
  4. package/dist/assets/{History-Cj2FejEo.js → History-CF157HyX.js} +2 -2
  5. package/dist/assets/{Hooks-CaelQI6t.js → Hooks-CCPPipBk.js} +3 -3
  6. package/dist/assets/{HostBilko--v7cMR8I.js → HostBilko-Dlmz2m06.js} +1 -1
  7. package/dist/assets/{Library-DgI9oCCZ.js → Library-BaXcboWX.js} +1 -1
  8. package/dist/assets/{ListDetail-DYUZN-x-.js → ListDetail-CoUCzG5S.js} +1 -1
  9. package/dist/assets/MarkdownEditor-DTg3JlOT.js +1 -0
  10. package/dist/assets/{McpServers-ypCYURh3.js → McpServers-DdjY1sA3.js} +2 -2
  11. package/dist/assets/{Memory-C2qYp-3M.js → Memory-BJ3Jlw0C.js} +4 -4
  12. package/dist/assets/{Panel-Cj2kw-Zv.js → Panel-ygRSgc41.js} +1 -1
  13. package/dist/assets/Permissions-m0KAnccF.js +3 -0
  14. package/dist/assets/{Plugins-C1Vj8_dU.js → Plugins-CJRl6B3Q.js} +2 -2
  15. package/dist/assets/{ProvenanceBadge-DczPNM5U.js → ProvenanceBadge-w8cnlVvW.js} +1 -1
  16. package/dist/assets/{SaveBar-Cd_7U6Gb.js → SaveBar-BpyPcUIk.js} +1 -1
  17. package/dist/assets/Scheduler-D2ppxSfy.js +14 -0
  18. package/dist/assets/{ScopeSwitcher-DVSyI44-.js → ScopeSwitcher-EyuIwt-j.js} +1 -1
  19. package/dist/assets/Settings-BHSGGFEc.js +3 -0
  20. package/dist/assets/{SkillReferenceGraph-CUv1_Q2c.js → SkillReferenceGraph-BNaZ2JCF.js} +1 -1
  21. package/dist/assets/{Skills-C_YHkAy-.js → Skills-Dl3q75QV.js} +2 -2
  22. package/dist/assets/{SystemPrompt-B8R7T9xn.js → SystemPrompt-Bg6F-Cyp.js} +1 -1
  23. package/dist/assets/{TagLibrary-dj9YHWyy.js → TagLibrary-BSutflCy.js} +1 -1
  24. package/dist/assets/{TiptapBody-DnSBUjHE.js → TiptapBody-CXih4xrc.js} +1 -1
  25. package/dist/assets/{Toggle-CjV_BJn6.js → Toggle-2qtUHhA9.js} +1 -1
  26. package/dist/assets/{index-CXFQIPhO.js → index-B3emy2oI.js} +583 -583
  27. package/dist/assets/{index-CDo9xBR9.css → index-BHTX4OTc.css} +1 -1
  28. package/dist/assets/settingsSchema-CdzxaqwM.js +3 -0
  29. package/dist/assets/{whisperWorker-Dbia1OpC.js → whisperWorker-C7ZGQwKg.js} +7 -7
  30. package/dist/index.html +2 -2
  31. package/dist/vad/ort-wasm-simd-threaded.asyncify.mjs +106 -110
  32. package/dist/vad/ort-wasm-simd-threaded.asyncify.wasm +0 -0
  33. package/dist/vad/ort-wasm-simd-threaded.jsep.mjs +98 -98
  34. package/dist/vad/ort-wasm-simd-threaded.jsep.wasm +0 -0
  35. package/dist/vad/ort-wasm-simd-threaded.jspi.mjs +99 -102
  36. package/dist/vad/ort-wasm-simd-threaded.jspi.wasm +0 -0
  37. package/dist/vad/ort-wasm-simd-threaded.mjs +46 -46
  38. package/dist/vad/ort-wasm-simd-threaded.wasm +0 -0
  39. package/package.json +14 -17
  40. package/plugins/CLAUDE.md +61 -0
  41. package/plugins/session-manager-dev/.claude-plugin/plugin.json +1 -1
  42. package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +1 -1
  43. package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +1 -1
  44. package/scripts/README.md +59 -0
  45. package/scripts/audit-ops-hygiene.cjs +350 -0
  46. package/scripts/hooks/guard-destructive-git.cjs +10 -34
  47. package/scripts/hooks/guard-inline-implementation.cjs +14 -8
  48. package/scripts/hooks/guard-prd-writes.cjs +7 -36
  49. package/scripts/hooks/guard-self-schedule.cjs +175 -0
  50. package/scripts/ops-sweep.cjs +355 -0
  51. package/scripts/scheduler-mcp-server.cjs +35 -71
  52. package/src/main/__tests__/agentModelResolve.test.cjs +100 -9
  53. package/src/main/__tests__/bilkoHost-integration.test.cjs +3 -3
  54. package/src/main/__tests__/broadcastCoalescer.test.cjs +18 -0
  55. package/src/main/__tests__/chat-cancel-terminal.test.cjs +6 -9
  56. package/src/main/__tests__/chat-exit-close-race.test.cjs +3 -3
  57. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +4 -5
  58. package/src/main/__tests__/chat-queue.test.cjs +2 -2
  59. package/src/main/__tests__/chat-stop-signal.test.cjs +2 -2
  60. package/src/main/__tests__/dep-orphan-archive-health.test.cjs +77 -0
  61. package/src/main/__tests__/dod-batchkey.test.cjs +2 -2
  62. package/src/main/__tests__/dod-drain-hook.test.cjs +2 -2
  63. package/src/main/__tests__/dod-report.test.cjs +2 -2
  64. package/src/main/__tests__/dod-reverify.test.cjs +2 -2
  65. package/src/main/__tests__/epicMint.test.cjs +4 -4
  66. package/src/main/__tests__/exchanges.test.cjs +2 -2
  67. package/src/main/__tests__/extractJson.test.cjs +2 -2
  68. package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
  69. package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +556 -0
  70. package/src/main/__tests__/health-build-freshness.test.cjs +39 -0
  71. package/src/main/__tests__/health-delegation-chain.test.cjs +17 -2
  72. package/src/main/__tests__/health-queue-dispatch.test.cjs +58 -7
  73. package/src/main/__tests__/health-tick-liveness.test.cjs +11 -3
  74. package/src/main/__tests__/health-usage-poller.test.cjs +70 -23
  75. package/src/main/__tests__/historyRollup.test.cjs +2 -2
  76. package/src/main/__tests__/kg-augment.test.cjs +2 -2
  77. package/src/main/__tests__/mcpStatus.test.cjs +2 -2
  78. package/src/main/__tests__/memoryAggregate.test.cjs +1 -1
  79. package/src/main/__tests__/memoryStale.test.cjs +1 -1
  80. package/src/main/__tests__/needsReviewLedger.test.cjs +162 -0
  81. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +28 -4
  82. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +48 -4
  83. package/src/main/__tests__/prd-group-allocator.test.cjs +2 -2
  84. package/src/main/__tests__/prdAdminRouteParity.test.cjs +2 -0
  85. package/src/main/__tests__/prdAdminRoutes.test.cjs +14 -2
  86. package/src/main/__tests__/prdAuthoringSeed.test.cjs +39 -0
  87. package/src/main/__tests__/prdCreateDisposition.test.cjs +201 -0
  88. package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +125 -0
  89. package/src/main/__tests__/prdLocations.test.cjs +100 -2
  90. package/src/main/__tests__/prdLocationsArchived.test.cjs +50 -1
  91. package/src/main/__tests__/prdSetDisposition.test.cjs +222 -0
  92. package/src/main/__tests__/proc-role-env.test.cjs +125 -0
  93. package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +304 -0
  94. package/src/main/__tests__/procname-sm-processes.test.cjs +127 -0
  95. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +81 -401
  96. package/src/main/__tests__/projectPages.test.cjs +63 -149
  97. package/src/main/__tests__/queue-health-verdict.test.cjs +170 -0
  98. package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +117 -20
  99. package/src/main/__tests__/queue-starvation-per-project.test.cjs +14 -2
  100. package/src/main/__tests__/queueHistory.test.cjs +65 -2
  101. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +38 -3
  102. package/src/main/__tests__/reconcileTiming.test.cjs +135 -0
  103. package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +181 -0
  104. package/src/main/__tests__/runVerify.test.cjs +5 -5
  105. package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +3 -3
  106. package/src/main/__tests__/scheduleJobTransitions.test.cjs +103 -3
  107. package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +143 -0
  108. package/src/main/__tests__/scheduler-autofix-select.test.cjs +2 -2
  109. package/src/main/__tests__/scheduler-autopromote.test.cjs +2 -2
  110. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +3 -5
  111. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +79 -98
  112. package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +121 -0
  113. package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +43 -0
  114. package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +161 -0
  115. package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +58 -0
  116. package/src/main/__tests__/scheduler-epic-digest.test.cjs +3 -5
  117. package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +2 -2
  118. package/src/main/__tests__/scheduler-gate-shadow.test.cjs +119 -0
  119. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +390 -0
  120. package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +80 -0
  121. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +25 -18
  122. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +4 -4
  123. package/src/main/__tests__/scheduler-job-budget.test.cjs +172 -0
  124. package/src/main/__tests__/scheduler-launch-failure.test.cjs +3 -5
  125. package/src/main/__tests__/scheduler-looks-done.test.cjs +119 -10
  126. package/src/main/__tests__/scheduler-manual-pause.test.cjs +118 -0
  127. package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +26 -3
  128. package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +164 -0
  129. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +17 -2
  130. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +3 -5
  131. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +41 -6
  132. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +62 -6
  133. package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +3 -5
  134. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +182 -3
  135. package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +87 -0
  136. package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +100 -0
  137. package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +3 -3
  138. package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +4 -4
  139. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +110 -0
  140. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +168 -0
  141. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +18 -8
  142. package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +245 -0
  143. package/src/main/__tests__/scheduler-supervisor-record.test.cjs +81 -0
  144. package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +2 -2
  145. package/src/main/__tests__/scheduler-tick-wedge.test.cjs +172 -0
  146. package/src/main/__tests__/scheduler-transient-failure.test.cjs +2 -2
  147. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +51 -11
  148. package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +3 -5
  149. package/src/main/__tests__/usageSingleFlight.test.cjs +157 -0
  150. package/src/main/__tests__/workTypeLibrary.test.cjs +1 -1
  151. package/src/main/bilkoHost.cjs +16 -7
  152. package/src/main/build-info.json +8 -0
  153. package/src/main/chatRunner.cjs +12 -7
  154. package/src/main/config.cjs +2 -3
  155. package/src/main/docEdit.cjs +4 -2
  156. package/src/main/health.cjs +268 -52
  157. package/src/main/heapSnapshot.cjs +2 -2
  158. package/src/main/historyAggregator.cjs +6 -1
  159. package/src/main/index.cjs +152 -50
  160. package/src/main/ipcSchemas.cjs +53 -17
  161. package/src/main/lib/__tests__/active-sessions.test.cjs +251 -0
  162. package/src/main/lib/__tests__/auditLog.test.cjs +38 -0
  163. package/src/main/lib/__tests__/bootSelfHeal.test.cjs +107 -0
  164. package/src/main/lib/__tests__/buildIdentity.test.cjs +121 -0
  165. package/src/main/lib/__tests__/cwdClassify.test.cjs +111 -0
  166. package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +95 -0
  167. package/src/main/lib/__tests__/delegationReadiness.test.cjs +323 -44
  168. package/src/main/lib/__tests__/dispatchLoop.test.cjs +63 -0
  169. package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +239 -0
  170. package/src/main/lib/__tests__/gateFixtures.json +20 -0
  171. package/src/main/lib/__tests__/gitWorktree.test.cjs +97 -1
  172. package/src/main/lib/__tests__/guardShims.test.cjs +151 -0
  173. package/src/main/lib/__tests__/instanceLock.test.cjs +93 -7
  174. package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +78 -0
  175. package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +11 -0
  176. package/src/main/lib/__tests__/localAdminHttp.test.cjs +1 -1
  177. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +6 -1
  178. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +5 -5
  179. package/src/main/lib/__tests__/prdDisposition.test.cjs +224 -0
  180. package/src/main/lib/__tests__/procIdentity.test.cjs +119 -0
  181. package/src/main/lib/__tests__/procName.test.cjs +92 -0
  182. package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +67 -0
  183. package/src/main/lib/__tests__/reaperHelpers.test.cjs +179 -1
  184. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +6 -7
  185. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +37 -204
  186. package/src/main/lib/__tests__/schedulerPaths.test.cjs +226 -0
  187. package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +94 -0
  188. package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +56 -0
  189. package/src/main/lib/__tests__/sessionSlots.test.cjs +45 -0
  190. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +1 -1
  191. package/src/main/lib/__tests__/upgradeDrain.test.cjs +130 -0
  192. package/src/main/lib/__tests__/usageCircuit.test.cjs +285 -0
  193. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +375 -0
  194. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +266 -0
  195. package/src/main/lib/activeIndexRebuild.cjs +1 -3
  196. package/{scripts → src/main}/lib/activeSessions.cjs +67 -89
  197. package/src/main/lib/adoptedRunSupervisor.cjs +136 -0
  198. package/src/main/lib/agentModelResolve.cjs +94 -24
  199. package/src/main/lib/agentPersonaSchema.cjs +2 -2
  200. package/src/main/lib/auditLog.cjs +30 -5
  201. package/src/main/lib/bootSelfHeal.cjs +88 -0
  202. package/src/main/lib/buildIdentity.cjs +113 -0
  203. package/src/main/lib/classifyPromptTicket.cjs +3 -2
  204. package/src/main/lib/claudeBin.cjs +37 -2
  205. package/src/main/lib/cleanEnv.cjs +27 -1
  206. package/src/main/lib/credentials.cjs +4 -2
  207. package/src/main/lib/cwdClassify.cjs +185 -0
  208. package/src/main/lib/definitionOfDone.cjs +296 -52
  209. package/src/main/lib/delegationReadiness.cjs +290 -225
  210. package/src/main/lib/dispatchLoop.cjs +40 -0
  211. package/src/main/lib/effectiveModelInfo.cjs +332 -0
  212. package/src/main/lib/ephemeralCwd.cjs +7 -28
  213. package/src/main/lib/epicMint.cjs +3 -3
  214. package/src/main/lib/gitWorktree.cjs +136 -19
  215. package/src/main/lib/guardShims.cjs +156 -0
  216. package/src/main/lib/historyRollup.cjs +6 -7
  217. package/src/main/lib/instanceLock.cjs +32 -5
  218. package/src/main/lib/jobDirtFilter.cjs +7 -2
  219. package/src/main/lib/jobSupervisorRecord.cjs +147 -0
  220. package/src/main/lib/jobWorktreeBootLive.cjs +9 -5
  221. package/src/main/lib/launchFailure.cjs +2 -1
  222. package/src/main/lib/localAdminHttp.cjs +9 -27
  223. package/src/main/lib/mcpToolCatalog.cjs +33 -78
  224. package/src/main/lib/needsReviewLedger.cjs +205 -0
  225. package/src/main/lib/opsErrorLog.cjs +1 -1
  226. package/src/main/lib/opsOwnership.cjs +27 -19
  227. package/src/main/lib/prdAuthoringSeed.cjs +36 -0
  228. package/src/main/lib/prdCreate.cjs +56 -1
  229. package/src/main/lib/prdDisposition.cjs +199 -0
  230. package/src/main/lib/prdFrontmatter.cjs +8 -2
  231. package/src/main/lib/prdLocations.cjs +214 -45
  232. package/src/main/lib/procIdentity.cjs +126 -0
  233. package/src/main/lib/procName.cjs +98 -0
  234. package/src/main/lib/projectHomeAdminRoutes.cjs +56 -327
  235. package/src/main/lib/projectRootResolve.cjs +1 -1
  236. package/src/main/lib/queueHistory.cjs +27 -8
  237. package/src/main/lib/queueStore.cjs +63 -38
  238. package/src/main/lib/quietMachineLease.cjs +19 -2
  239. package/src/main/lib/reaperHelpers.cjs +181 -15
  240. package/src/main/lib/reservationExpiry.cjs +30 -0
  241. package/src/main/lib/runClaudeP.cjs +4 -2
  242. package/src/main/lib/runLogRetention.cjs +3 -2
  243. package/src/main/lib/scheduleJobSchema.cjs +10 -2
  244. package/src/main/lib/scheduleJobTransitions.cjs +60 -0
  245. package/src/main/lib/schedulerBatch.cjs +27 -2
  246. package/src/main/lib/schedulerConfig.cjs +24 -0
  247. package/src/main/lib/schedulerPaths.cjs +175 -0
  248. package/src/main/lib/schedulerRuntimeState.cjs +59 -0
  249. package/src/main/lib/sessionSlots.cjs +47 -9
  250. package/src/main/lib/smProcNames.cjs +51 -0
  251. package/src/main/lib/upgradeDrain.cjs +188 -0
  252. package/src/main/lib/usageCircuit.cjs +206 -0
  253. package/{scripts → src/main}/lib/watchdogHelpers.cjs +71 -38
  254. package/src/main/lib/withTimeout.cjs +33 -0
  255. package/src/main/mcpStatus.cjs +4 -2
  256. package/src/main/pluginInstall.cjs +6 -2
  257. package/src/main/projectPages.cjs +41 -145
  258. package/src/main/pty.cjs +8 -1
  259. package/src/main/queueOps.cjs +10 -10
  260. package/src/main/runVerify.cjs +69 -3
  261. package/src/main/scheduler/prdParser.cjs +13 -0
  262. package/src/main/scheduler.cjs +2841 -482
  263. package/src/main/seedAgentPersonas.cjs +1 -1
  264. package/src/main/seedDevPlugin.cjs +1 -1
  265. package/src/main/seedSchedulerMcp.cjs +7 -6
  266. package/src/main/seedStatus.cjs +1 -1
  267. package/src/main/supervisor.cjs +5 -3
  268. package/src/main/templates/PRD_AUTHORING.md +50 -0
  269. package/src/main/usage.cjs +140 -58
  270. package/src/preload/api.d.ts +110 -65
  271. package/src/preload/index.cjs +12 -4
  272. package/src/seed/agents/project-home-builder.md +31 -48
  273. package/dist/assets/AgentLibrary-DTFL7y8G.js +0 -3
  274. package/dist/assets/MarkdownEditor-DCIubYWf.js +0 -1
  275. package/dist/assets/Permissions-BiYZNGYW.js +0 -3
  276. package/dist/assets/Scheduler-DcLBiJBq.js +0 -14
  277. package/dist/assets/Settings-Cv-pRyms.js +0 -3
  278. package/dist/assets/settingsSchema-BJVciriw.js +0 -3
  279. package/screenshots/.gitkeep +0 -0
  280. package/screenshots/README-screenshots.md +0 -13
  281. package/scripts/project-pages-logic/dist/logic.cjs +0 -4709
  282. package/scripts/render-project-pages/dist/renderer.cjs +0 -18900
  283. package/scripts/render-project-pages.cjs +0 -70
  284. package/scripts/validate-project-pages-summary.cjs +0 -62
  285. package/src/main/lib/projectPageSummarySchema.cjs +0 -181
  286. package/src/main/teams.cjs +0 -95
  287. package/src/main/templates/project-pages-catalog.json +0 -741
  288. package/src/main/templates/project-pages-default-home.html +0 -123
  289. package/src/main/templates/project-pages-pipeline.md +0 -417
  290. package/src/seed/prompts/code-review/ac-coverage-check.md +0 -8
  291. package/src/seed/prompts/code-review/correctness-only.md +0 -8
  292. package/src/seed/prompts/code-review/full-spectrum-high.md +0 -8
  293. package/src/seed/prompts/code-review/hallucination-check.md +0 -8
  294. package/src/seed/prompts/code-review/public-api-compat.md +0 -8
  295. package/src/seed/prompts/code-review/readability-naming.md +0 -8
  296. package/src/seed/prompts/debugging/bug-as-failing-test.md +0 -8
  297. package/src/seed/prompts/debugging/git-bisect-regression.md +0 -8
  298. package/src/seed/prompts/debugging/instrument-intermittent-bug.md +0 -8
  299. package/src/seed/prompts/debugging/localize-pipeline-failure.md +0 -8
  300. package/src/seed/prompts/debugging/reproduce-then-diagnose.md +0 -8
  301. package/src/seed/prompts/documentation/adr-from-change.md +0 -8
  302. package/src/seed/prompts/documentation/module-readme.md +0 -8
  303. package/src/seed/prompts/documentation/onboarding-plan.md +0 -8
  304. package/src/seed/prompts/documentation/refresh-claude-md.md +0 -8
  305. package/src/seed/prompts/documentation/tsdoc-public-exports.md +0 -8
  306. package/src/seed/prompts/git-pr/conventional-commit.md +0 -8
  307. package/src/seed/prompts/git-pr/draft-pr-title-body.md +0 -8
  308. package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +0 -8
  309. package/src/seed/prompts/git-pr/release-notes-block.md +0 -8
  310. package/src/seed/prompts/git-pr/split-large-pr.md +0 -8
  311. package/src/seed/prompts/performance/bundle-startup-audit.md +0 -8
  312. package/src/seed/prompts/performance/complexity-audit.md +0 -8
  313. package/src/seed/prompts/performance/cpu-profile-hot-path.md +0 -8
  314. package/src/seed/prompts/performance/db-query-plan-review.md +0 -8
  315. package/src/seed/prompts/performance/memory-leak-hunt.md +0 -8
  316. package/src/seed/prompts/qa/api-contract-tests.md +0 -8
  317. package/src/seed/prompts/qa/e2e-critical-path.md +0 -8
  318. package/src/seed/prompts/qa/failing-test-for-bug.md +0 -8
  319. package/src/seed/prompts/qa/find-missing-test-coverage.md +0 -8
  320. package/src/seed/prompts/qa/stabilize-flaky-test.md +0 -8
  321. package/src/seed/prompts/qa/tdd-red-first.md +0 -8
  322. package/src/seed/prompts/qa/visual-regression-review.md +0 -8
  323. package/src/seed/prompts/qa/wcag-axe-scan.md +0 -8
  324. package/src/seed/prompts/refactoring/dead-code-sweep.md +0 -8
  325. package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +0 -8
  326. package/src/seed/prompts/refactoring/modernize-legacy-file.md +0 -8
  327. package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +0 -8
  328. package/src/seed/prompts/refactoring/tighten-module-boundaries.md +0 -8
  329. package/src/seed/prompts/security/authz-audit.md +0 -8
  330. package/src/seed/prompts/security/crypto-correctness.md +0 -8
  331. package/src/seed/prompts/security/cwe-top-25-hunt.md +0 -8
  332. package/src/seed/prompts/security/dependency-audit.md +0 -8
  333. package/src/seed/prompts/security/ipc-boundary-hardening.md +0 -8
  334. package/src/seed/prompts/security/owasp-top-10-staged-diff.md +0 -8
  335. package/src/seed/prompts/security/secret-credential-scan.md +0 -8
@@ -0,0 +1,172 @@
1
+ /**
2
+ * scheduler-job-budget.test.cjs — per-job wall-clock budget derived from the
3
+ * PRD's own `estimateMinutes`, and the classifier that parks an over-budget
4
+ * job `needs_review` (never 'failed', never silently 'completed').
5
+ *
6
+ * Closes the gap `findOverrunningJobs` deliberately leaves open (escalation
7
+ * only, never kills — see its own header): p50/p90/max across 606 run-meta
8
+ * records since 2026-08-25 was 13m/60m/240m against a concurrencyCap of 4,
9
+ * so a chatty-but-runaway executor that keeps emitting output (never
10
+ * tripping IDLE_OUTPUT_KILL_MS) could hold a dispatch slot for hours with
11
+ * nothing to notice or act on it.
12
+ *
13
+ * Run: timeout 120 npx vitest run src/main/__tests__/scheduler-job-budget.test.cjs
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ // vitest, NOT node:test — this repo's suite is vitest-only (CLAUDE.md).
19
+ import { test } from 'vitest';
20
+ const assert = require('node:assert/strict');
21
+ const {
22
+ computeJobBudgetMs,
23
+ classifyBudgetKill,
24
+ isJobBudgetExempt,
25
+ shouldKillForBudget,
26
+ resolveBudgetKillOutcome,
27
+ selectAutoFixTargets,
28
+ JOB_BUDGET_FACTOR,
29
+ JOB_BUDGET_FLOOR_MS,
30
+ JOB_BUDGET_CEILING_MS,
31
+ } = require('../scheduler.cjs');
32
+
33
+ // ---------- computeJobBudgetMs: both clamps ----------
34
+
35
+ test('computeJobBudgetMs: a mid-size estimate is factor*estimate, no clamp hit', () => {
36
+ // 30m * 3 = 90m — comfortably between the 45m floor and the 180m ceiling.
37
+ const budgetMs = computeJobBudgetMs(30);
38
+ assert.strictEqual(budgetMs, 90 * 60_000);
39
+ });
40
+
41
+ test('computeJobBudgetMs: the floor clamp — a tiny/missing/zero estimate never goes below 45m', () => {
42
+ // 5m * 3 = 15m, well under the 45m floor.
43
+ assert.strictEqual(computeJobBudgetMs(5), JOB_BUDGET_FLOOR_MS);
44
+ // Missing/zero estimate is treated as 0 — the floor dominates, no special-casing.
45
+ assert.strictEqual(computeJobBudgetMs(0), JOB_BUDGET_FLOOR_MS);
46
+ assert.strictEqual(computeJobBudgetMs(null), JOB_BUDGET_FLOOR_MS);
47
+ assert.strictEqual(computeJobBudgetMs(undefined), JOB_BUDGET_FLOOR_MS);
48
+ });
49
+
50
+ test('computeJobBudgetMs: the ceiling clamp — a huge estimate never exceeds 180m', () => {
51
+ // 200m * 3 = 600m, way past the 180m ceiling.
52
+ assert.strictEqual(computeJobBudgetMs(200), JOB_BUDGET_CEILING_MS);
53
+ });
54
+
55
+ test('computeJobBudgetMs: honors caller overrides when neither clamp is hit', () => {
56
+ const budgetMs = computeJobBudgetMs(5, { factor: 2, floorMs: 1000, ceilingMs: 3_600_000 });
57
+ assert.strictEqual(budgetMs, 10 * 60_000);
58
+ });
59
+
60
+ test('JOB_BUDGET_FACTOR/FLOOR/CEILING are the named constants the AC requires (3x, 45m, 180m)', () => {
61
+ assert.strictEqual(JOB_BUDGET_FACTOR, 3);
62
+ assert.strictEqual(JOB_BUDGET_FLOOR_MS, 45 * 60_000);
63
+ assert.strictEqual(JOB_BUDGET_CEILING_MS, 180 * 60_000);
64
+ });
65
+
66
+ // ---------- shouldKillForBudget: under vs over budget ----------
67
+
68
+ test('shouldKillForBudget: a job under budget is untouched', () => {
69
+ assert.strictEqual(shouldKillForBudget(44 * 60_000, 45 * 60_000), false);
70
+ });
71
+
72
+ test('shouldKillForBudget: a job at or past its budget fires', () => {
73
+ assert.strictEqual(shouldKillForBudget(45 * 60_000, 45 * 60_000), true);
74
+ assert.strictEqual(shouldKillForBudget(241 * 60_000, 90 * 60_000), true);
75
+ });
76
+
77
+ // ---------- classifyBudgetKill: the needs_review park + evidence threading ----------
78
+
79
+ test('classifyBudgetKill: a job not killed by the budget watchdog is untouched (returns null)', () => {
80
+ assert.strictEqual(classifyBudgetKill({ killedByWatchdog: 'idle-tail', exitCode: 143 }, null), null);
81
+ assert.strictEqual(classifyBudgetKill({ killedByWatchdog: null, exitCode: 0 }, null), null);
82
+ assert.strictEqual(classifyBudgetKill(null, null), null);
83
+ });
84
+
85
+ test('classifyBudgetKill: an over-budget job parks needs_review with the exact reason string, no evidence', () => {
86
+ const res = {
87
+ killedByWatchdog: 'budget',
88
+ exitCode: 143,
89
+ budgetKillReason: 'wall-clock budget exceeded: ran 241m against a 90m budget (estimateMinutes=30)',
90
+ };
91
+ const decision = classifyBudgetKill(res, null);
92
+ assert.strictEqual(decision.status, 'needs_review');
93
+ assert.strictEqual(decision.reason, 'wall-clock budget exceeded: ran 241m against a 90m budget (estimateMinutes=30)');
94
+ assert.strictEqual(decision.landedCommit, null);
95
+ });
96
+
97
+ test('classifyBudgetKill: an over-budget job with a verified landed commit is still adjudicated on git evidence, not discarded', () => {
98
+ const res = {
99
+ killedByWatchdog: 'budget',
100
+ exitCode: 143,
101
+ budgetKillReason: 'wall-clock budget exceeded: ran 91m against a 90m budget (estimateMinutes=30)',
102
+ };
103
+ const decision = classifyBudgetKill(res, 'deadbeef1234567890');
104
+ // Never 'failed' or 'completed' just because a commit landed — still parked
105
+ // for review, but the evidence is threaded onto the row rather than dropped.
106
+ assert.strictEqual(decision.status, 'needs_review');
107
+ assert.strictEqual(decision.landedCommit, 'deadbeef1234567890');
108
+ });
109
+
110
+ // ---------- isJobBudgetExempt: quietMachine + explicit opt-out ----------
111
+
112
+ test('isJobBudgetExempt: a quietMachine job is exempt', () => {
113
+ assert.strictEqual(isJobBudgetExempt({ quietMachine: true }), true);
114
+ });
115
+
116
+ test('isJobBudgetExempt: a PRD with an explicit budgetExempt opt-out is exempt', () => {
117
+ assert.strictEqual(isJobBudgetExempt({ budgetExempt: true }), true);
118
+ });
119
+
120
+ test('isJobBudgetExempt: an ordinary job is not exempt', () => {
121
+ assert.strictEqual(isJobBudgetExempt({}), false);
122
+ assert.strictEqual(isJobBudgetExempt({ quietMachine: false, budgetExempt: false }), false);
123
+ });
124
+
125
+ // ---------- resolveBudgetKillOutcome: the watchdog-tick vs natural-exit race ----------
126
+
127
+ test('resolveBudgetKillOutcome: a genuine signal kill (exit 143) is trusted', () => {
128
+ const outcome = resolveBudgetKillOutcome({
129
+ killedByWatchdog: 'budget', killedBySignal: true, durationMs: 91 * 60_000, jobBudgetMs: 90 * 60_000, estimateMinutes: 30,
130
+ });
131
+ assert.strictEqual(outcome.killedByWatchdog, 'budget');
132
+ assert.strictEqual(outcome.budgetKillReason, 'wall-clock budget exceeded: ran 91m against a 90m budget (estimateMinutes=30)');
133
+ });
134
+
135
+ test('resolveBudgetKillOutcome: a race where the child exits cleanly (exit 0) in the same tick is NOT trusted as a budget kill', () => {
136
+ // The watchdog's action() fired (stamping ctx.killedByWatchdog='budget')
137
+ // but the agent had already exited on its own with exit 0 — killTree()
138
+ // was a no-op against an already-dead pid. Must never misclassify a
139
+ // genuinely successful run as needs_review.
140
+ const outcome = resolveBudgetKillOutcome({
141
+ killedByWatchdog: 'budget', killedBySignal: false, durationMs: 90 * 60_000, jobBudgetMs: 90 * 60_000, estimateMinutes: 30,
142
+ });
143
+ assert.strictEqual(outcome.killedByWatchdog, null);
144
+ assert.strictEqual(outcome.budgetKillReason, null);
145
+ });
146
+
147
+ test('resolveBudgetKillOutcome: a race where the child fails on its own (exit 1, unrelated) is NOT trusted as a budget kill', () => {
148
+ const outcome = resolveBudgetKillOutcome({
149
+ killedByWatchdog: 'budget', killedBySignal: false, durationMs: 90 * 60_000, jobBudgetMs: 90 * 60_000, estimateMinutes: 30,
150
+ });
151
+ assert.strictEqual(outcome.killedByWatchdog, null);
152
+ });
153
+
154
+ test('resolveBudgetKillOutcome: no watchdog fired at all — untouched', () => {
155
+ const outcome = resolveBudgetKillOutcome({
156
+ killedByWatchdog: 'idle-tail', killedBySignal: true, durationMs: 25 * 60_000, jobBudgetMs: 90 * 60_000, estimateMinutes: 30,
157
+ });
158
+ assert.strictEqual(outcome.killedByWatchdog, 'idle-tail');
159
+ assert.strictEqual(outcome.budgetKillReason, null);
160
+ });
161
+
162
+ // ---------- selectAutoFixTargets: a budget-killed job never gets auto-retried ----------
163
+
164
+ test('selectAutoFixTargets: a budget_exceeded needs_review job is excluded — it parks for a human/ladder decision, never a fix-plan', () => {
165
+ const jobs = [
166
+ { slug: '10-budget-victim', status: 'needs_review', verifierVerdict: 'budget_exceeded', runId: 'r1' },
167
+ { slug: '11-ordinary', status: 'needs_review', verifierVerdict: 'transcript_errors', runId: 'r2' },
168
+ ];
169
+ const targets = selectAutoFixTargets(jobs, { fixSlugExists: () => false });
170
+ assert.ok(!targets.some((t) => t.slug === '10-budget-victim'));
171
+ assert.ok(targets.some((t) => t.slug === '11-ordinary'));
172
+ });
@@ -20,6 +20,7 @@ import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
20
20
  const fs = require('node:fs');
21
21
  const os = require('node:os');
22
22
  const path = require('node:path');
23
+ const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
23
24
 
24
25
  let tmpHome;
25
26
  let originalHome;
@@ -50,8 +51,7 @@ afterEach(() => {
50
51
  const THINKING_400 = 'API Error: 400 {"detail":{"error":"{\\"message\\":\\"\\\\\\"thinking.type.enabled\\\\\\" is not supported for this model. Use \\\\\\"thinking.type.adaptive\\\\\\" and \\\\\\"output_config.effort\\\\\\" to control thinking behavior.\\"}"}}';
51
52
 
52
53
  function writeStub({ mode }) {
53
- const stubPath = path.join(os.tmpdir(), `sm-claude-stub-launch-${process.pid}-${Math.floor(Math.random() * 1e9)}.cjs`);
54
- const body = `
54
+ return claudeStub.writeClaudeStub({ body: `
55
55
  const fs = require('fs');
56
56
  const path = require('path');
57
57
  fs.writeFileSync(path.join(process.cwd(), 'env.marker'), JSON.stringify({
@@ -67,9 +67,7 @@ function writeStub({ mode }) {
67
67
  }
68
68
  process.stdout.write(JSON.stringify({ type: 'result', subtype: 'error', is_error: true, num_turns: 9, usage: { input_tokens: 100, output_tokens: 250 }, result: 'Error: tests failed' }) + '\\n');
69
69
  process.exit(1);
70
- `;
71
- fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
72
- return stubPath;
70
+ ` });
73
71
  }
74
72
 
75
73
  function mkProject() {
@@ -33,8 +33,8 @@ const { execFileSync } = require('node:child_process');
33
33
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'looks-done-test-'));
34
34
  process.env.HOME = tmpHome;
35
35
 
36
- const { reverifyNeedsReview, computeLooksDone, findSatisfyingCommitOnMain } = require('../scheduler.cjs');
37
- const { resolvePrdWriteDir } = require('../lib/prdLocations.cjs');
36
+ const { reverifyNeedsReview, computeLooksDone, applyNeedsReviewAutoResolve, findSatisfyingCommitOnMain } = require('../scheduler.cjs');
37
+ const { resolveEpicPrdWriteDir } = require('../lib/prdLocations.cjs');
38
38
  const { bustCwdCache } = require('../lib/queueStore.cjs');
39
39
 
40
40
  function git(args, cwd) {
@@ -92,13 +92,32 @@ function writeRunLog(runId, slug, lines) {
92
92
  fs.writeFileSync(path.join(runDir, `${slug}.log`), lines.join('\n') + '\n');
93
93
  }
94
94
 
95
+ // Epic-scoped, not the legacy flat prds/ dir: the flat dir is retired and
96
+ // swept into prds-archived/ by consolidateFlatPrds the moment a job is no
97
+ // longer live (pending/running/needs_review/investigating), which would
98
+ // strand a row that starts 'failed' before reverifyNeedsReview ever gets a
99
+ // chance to decide anything about it.
95
100
  function writePrd(cwd, slug, body) {
96
- const dir = resolvePrdWriteDir(cwd);
101
+ const dir = resolveEpicPrdWriteDir(cwd, 'test-fixture-epic');
97
102
  fs.mkdirSync(dir, { recursive: true });
98
103
  const text = `---\ntitle: Test PRD\ncwd: ${cwd}\nestimateMinutes: 30\n---\n${body}\n`;
99
104
  fs.writeFileSync(path.join(dir, `${slug}.md`), text);
100
105
  }
101
106
 
107
+ // A job reverifyNeedsReview() heals to 'completed' has its PRD archived
108
+ // immediately (archiveCompletedPrd) and its row backfilled to history.jsonl
109
+ // + dropped from queue.json on the very same reconcile pass — see
110
+ // scheduler-reconcile-history-backfill.test.cjs. Look in both places.
111
+ function readTerminalRow(queuePath, projectCwd, slug) {
112
+ const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
113
+ const row = jobs.find((j) => j.slug === slug);
114
+ if (row) return row;
115
+ const historyPath = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'state', 'history.jsonl');
116
+ if (!fs.existsSync(historyPath)) return undefined;
117
+ const lines = fs.readFileSync(historyPath, 'utf8').trim().split('\n').filter(Boolean);
118
+ return lines.map((l) => JSON.parse(l)).reverse().find((j) => j.slug === slug);
119
+ }
120
+
102
121
  async function wait(ms) {
103
122
  await new Promise((r) => setTimeout(r, ms));
104
123
  }
@@ -127,7 +146,10 @@ test('failed + no result event (unverified-shaped) + later commit touching decla
127
146
  writeRunLog('run-171', '171-example', ['[scheduler] starting 171-example']); // no result event
128
147
 
129
148
  await wait(1100); // git --since has 1s resolution
130
- commitFile(projectCwd, 'src/foo.js', 'hello', 'fix 171');
149
+ // Must name this job's own slug to be attributable (see
150
+ // attributeLandedCommits's 'slug trailer' rule) — path overlap alone is no
151
+ // longer evidence.
152
+ commitFile(projectCwd, 'src/foo.js', 'hello', 'fix 171-example');
131
153
 
132
154
  await reverifyNeedsReview();
133
155
 
@@ -136,8 +158,9 @@ test('failed + no result event (unverified-shaped) + later commit touching decla
136
158
  assert.equal(jobs[0].status, 'needs_review', 'looksDone never auto-completes — only surfaces for a human');
137
159
  assert.ok(jobs[0].looksDone, 'expected a looksDone annotation');
138
160
  assert.equal(jobs[0].looksDone.commits.length, 1);
161
+ assert.equal(jobs[0].looksDone.rule, 'slug trailer');
139
162
  assert.deepEqual(jobs[0].looksDone.paths, ['src/foo.js']);
140
- assert.match(jobs[0].error, /looks done — 1 commit/);
163
+ assert.match(jobs[0].error, /looks done \(slug trailer\) — 1 commit/);
141
164
  });
142
165
 
143
166
  test('failed + a real result event (genuine gate failure) → not a candidate, stays failed untouched', async () => {
@@ -216,11 +239,11 @@ test('needs_review with an in-window commit heals exactly as today (regression
216
239
 
217
240
  await reverifyNeedsReview();
218
241
 
219
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
220
- assert.equal(jobs.length, 1);
221
- assert.equal(jobs[0].status, 'completed', 'a clean re-verify on a RESCANNABLE verdict must still heal');
222
- assert.equal(jobs[0].verifierVerdict, undefined);
223
- assert.equal(jobs[0].error, null);
242
+ const row = readTerminalRow(queuePath, projectCwd, '20-clean-feature');
243
+ assert.ok(row, 'healed job must be found in queue.json or (once archived+backfilled) history.jsonl');
244
+ assert.equal(row.status, 'completed', 'a clean re-verify on a RESCANNABLE verdict must still heal');
245
+ assert.equal(row.verifierVerdict, undefined);
246
+ assert.equal(row.error, null);
224
247
  });
225
248
 
226
249
  test('computeLooksDone: no declared paths on the PRD → null, never fabricates evidence', async () => {
@@ -379,3 +402,89 @@ test('computeLooksDone: non-git cwd → null without throwing (git-unavailable)'
379
402
  const looksDone = await computeLooksDone(job);
380
403
  assert.equal(looksDone, null);
381
404
  });
405
+
406
+ // --- Attribution regression (PRD 1204/1205 incident, 2026-09-13) ---
407
+ //
408
+ // Two PRDs in the SAME Epic declaring the same hot file: sibling job B lands
409
+ // a commit on that path while job A is parked in needs_review. Path overlap
410
+ // alone must never read as job A's own evidence — that's exactly how 1204's
411
+ // row cited 1205's commit 5dadf3c as "looks done" while 1204's real
412
+ // implementing commit (164184e) sat unnoticed.
413
+
414
+ test('sibling PRD commit on the same declared path is not attributable — computeLooksDone returns null, applyNeedsReviewAutoResolve never promotes to completed', async () => {
415
+ const projectCwd = path.join(tmpHome, 'proj-1204-sibling-incident');
416
+ initRepo(projectCwd);
417
+ registerActiveProject(projectCwd, 'proj-1204-sibling-incident-slug');
418
+ writePrd(projectCwd, '1204-job-a', [
419
+ '# Implementation notes',
420
+ 'Edit `src/main/scheduler.cjs`.',
421
+ ].join('\n'));
422
+
423
+ const startedAt = new Date().toISOString();
424
+ await wait(1100);
425
+ // Sibling job B's commit: touches job A's declared path, but names neither
426
+ // job A's slug nor any `sm-job/1204-job-a` branch, and job A's row carries
427
+ // no landedCommit of its own.
428
+ commitFile(projectCwd, 'src/main/scheduler.cjs', 'sibling change', 'feat(scheduler): restructure the backlog into Epic groups (1205-job-b)');
429
+
430
+ const jobA = {
431
+ slug: '1204-job-a',
432
+ cwd: projectCwd,
433
+ status: 'needs_review',
434
+ startedAt,
435
+ autoFixAttempted: true,
436
+ autoFixOutcome: 'no-plan',
437
+ autoFixRetries: 1,
438
+ exhaustedResolveAttempts: 0,
439
+ statusHistory: [{ to: 'needs_review', at: new Date(Date.now() - 45 * 60_000).toISOString() }],
440
+ };
441
+
442
+ const looksDone = await computeLooksDone(jobA);
443
+ assert.equal(looksDone, null, 'a sibling\'s path-overlapping commit must never be credited as job A\'s own evidence');
444
+
445
+ const outcome = applyNeedsReviewAutoResolve(jobA);
446
+ assert.equal(outcome, 'requeued', 'with no attributable evidence, the row must fall through to the existing requeue path, never complete');
447
+ assert.equal(jobA.status, 'pending');
448
+ });
449
+
450
+ test('this job\'s OWN attributable commit (landedCommit) still produces looksDone and still promotes to completed', async () => {
451
+ const projectCwd = path.join(tmpHome, 'proj-1204-own-commit');
452
+ initRepo(projectCwd);
453
+ registerActiveProject(projectCwd, 'proj-1204-own-commit-slug');
454
+ writePrd(projectCwd, '1204-job-a2', [
455
+ '# Implementation notes',
456
+ 'Edit `src/main/scheduler.cjs`.',
457
+ ].join('\n'));
458
+
459
+ const startedAt = new Date().toISOString();
460
+ await wait(1100);
461
+ // A sibling commit lands on the same path first...
462
+ commitFile(projectCwd, 'src/main/scheduler.cjs', 'sibling change', 'feat(scheduler): unrelated sibling work');
463
+ // ...then this job's OWN commit lands, exactly as spawnJob's finalize step
464
+ // would have recorded it on job.landedCommit.
465
+ commitFile(projectCwd, 'src/main/scheduler.cjs', 'this job\'s own change', 'fix(scheduler): attribution fix');
466
+ const ownSha = git(['rev-parse', 'HEAD'], projectCwd).trim();
467
+
468
+ const jobA = {
469
+ slug: '1204-job-a2',
470
+ cwd: projectCwd,
471
+ status: 'needs_review',
472
+ startedAt,
473
+ landedCommit: ownSha,
474
+ autoFixAttempted: true,
475
+ autoFixOutcome: 'no-plan',
476
+ autoFixRetries: 1,
477
+ exhaustedResolveAttempts: 0,
478
+ statusHistory: [{ to: 'needs_review', at: new Date(Date.now() - 45 * 60_000).toISOString() }],
479
+ };
480
+
481
+ const looksDone = await computeLooksDone(jobA);
482
+ assert.ok(looksDone, 'this job\'s own landedCommit must be recognized as attributable evidence');
483
+ assert.equal(looksDone.rule, 'landedCommit');
484
+ assert.deepEqual(looksDone.commits, [ownSha], 'only this job\'s own commit is evidence — never the sibling\'s');
485
+
486
+ jobA.looksDone = looksDone;
487
+ const outcome = applyNeedsReviewAutoResolve(jobA);
488
+ assert.equal(outcome, 'completed', 'strong, attributable evidence must still promote the row');
489
+ assert.equal(jobA.status, 'completed');
490
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * scheduler-manual-pause.test.cjs — user-initiated pause (schedule:pause /
3
+ * POST /admin/scheduler/pause). A 'manual' pause stops NEW dispatch, never
4
+ * kills running jobs, survives restart, and is cleared only by an explicit
5
+ * Resume / Run now — never by the rate-limit reset timer or network recovery.
6
+ *
7
+ * Run: timeout 120 npx vitest run src/main/__tests__/scheduler-manual-pause.test.cjs
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ import { test, expect, beforeAll, afterAll, beforeEach } from 'vitest';
13
+ const fs = require('node:fs');
14
+ const os = require('node:os');
15
+ const path = require('node:path');
16
+
17
+ let tmpHome;
18
+ let originalHome;
19
+ let scheduler;
20
+ let queueStore;
21
+
22
+ beforeAll(() => {
23
+ originalHome = process.env.HOME;
24
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-manual-pause-'));
25
+ process.env.HOME = tmpHome;
26
+ process.env.SM_JOB_WORKTREE_DISABLE = '1';
27
+ scheduler = require('../scheduler.cjs');
28
+ queueStore = require('../lib/queueStore.cjs');
29
+ });
30
+
31
+ afterAll(() => {
32
+ process.env.HOME = originalHome;
33
+ delete process.env.SM_JOB_WORKTREE_DISABLE;
34
+ fs.rmSync(tmpHome, { recursive: true, force: true });
35
+ });
36
+
37
+ beforeEach(async () => {
38
+ await scheduler.clearPause('manual');
39
+ });
40
+
41
+ test('pause engages reason "manual" with no resumeAt, even inside the manual-override cooldown', async () => {
42
+ await scheduler.clearPause('manual'); // starts the 5-minute cooldown
43
+ await scheduler.setPaused('manual', null);
44
+ const state = await queueStore.readMerged();
45
+ expect(state.paused).toBeTruthy();
46
+ expect(state.paused.reason).toBe('manual');
47
+ expect(state.paused.resumeAt).toBeNull();
48
+ });
49
+
50
+ test('a manual pause never arms a resume time, even if a resumeAt is supplied', async () => {
51
+ await scheduler.setPaused('manual', new Date(Date.now() + 3_600_000).toISOString());
52
+ const state = await queueStore.readMerged();
53
+ expect(state.paused.resumeAt).toBeNull();
54
+ });
55
+
56
+ test('pause blocks tickQueue dispatch', async () => {
57
+ await scheduler.setPaused('manual', null);
58
+ const result = await scheduler.tickQueue();
59
+ expect(result.fired).toBe(false);
60
+ expect(result.reason).toBe('paused');
61
+ });
62
+
63
+ test('an auto rate_limit / network / auth pause cannot displace a manual pause', async () => {
64
+ await scheduler.setPaused('manual', null);
65
+ await scheduler.setPaused('rate_limit', new Date(Date.now() + 3_600_000).toISOString());
66
+ await scheduler.setPaused('network', null);
67
+ await scheduler.setPaused('auth', null);
68
+ const state = await queueStore.readMerged();
69
+ expect(state.paused.reason).toBe('manual');
70
+ expect(state.paused.resumeAt).toBeNull();
71
+ });
72
+
73
+ test('poll recovery and the resume-time computation never clear/arm a manual pause', () => {
74
+ expect(scheduler.pollRecoveryClearSource('manual', true)).toBeNull();
75
+ expect(scheduler.pollRecoveryClearSource('manual', false)).toBeNull();
76
+ expect(scheduler.computeEffectiveResumeAt('manual', null)).toBeNull();
77
+ });
78
+
79
+ test('a running job survives a pause untouched', async () => {
80
+ const projectCwd = fs.mkdtempSync(path.join(tmpHome, 'pause-survivor-'));
81
+ const slugDir = path.join(tmpHome, '.claude', 'projects', 'pause-survivor-slug');
82
+ fs.mkdirSync(slugDir, { recursive: true });
83
+ fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd: projectCwd }) + '\n');
84
+ queueStore.bustCwdCache();
85
+ const stateDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'state');
86
+ fs.mkdirSync(stateDir, { recursive: true });
87
+ fs.writeFileSync(path.join(stateDir, 'queue.json'), JSON.stringify({
88
+ jobs: [{ slug: 'pause-survivor', status: 'running', cwd: projectCwd, pid: process.pid, runId: 'r1', startedAt: new Date().toISOString() }],
89
+ }));
90
+ await scheduler.setPaused('manual', null);
91
+ const state = await queueStore.readMerged();
92
+ const row = state.jobs.find((j) => j.slug === 'pause-survivor');
93
+ expect(row.status).toBe('running');
94
+ expect(row.runId).toBe('r1');
95
+ });
96
+
97
+ test('a manual pause survives restart (re-read from disk; boot never clears it)', async () => {
98
+ await scheduler.setPaused('manual', null);
99
+ const onDisk = await queueStore.readMerged(); // what a fresh process reads at boot
100
+ expect(onDisk.paused.reason).toBe('manual');
101
+ // Boot only clears a pause whose resumeAt has elapsed / re-arms one with a
102
+ // resumeAt; a manual pause has none, so neither branch applies.
103
+ expect(onDisk.paused.resumeAt).toBeNull();
104
+ });
105
+
106
+ test('resume clears a manual pause', async () => {
107
+ await scheduler.setPaused('manual', null);
108
+ await scheduler.clearPause('manual');
109
+ const state = await queueStore.readMerged();
110
+ expect(state.paused).toBeNull();
111
+ });
112
+
113
+ test('remote.pause / remote.resume are the admin-route twins of the IPC handlers', async () => {
114
+ expect(await scheduler.remote.pause()).toEqual({ ok: true });
115
+ expect((await queueStore.readMerged()).paused.reason).toBe('manual');
116
+ expect(await scheduler.remote.resume()).toEqual({ ok: true });
117
+ expect((await queueStore.readMerged()).paused).toBeNull();
118
+ });
@@ -5,19 +5,42 @@
5
5
  * scheduler process is otherwise invisible in the run record, which cost a
6
6
  * full investigation to diagnose a false pass_no_commit.
7
7
  *
8
+ * SCHEDULER_CODE_SHA now resolves through src/main/lib/buildIdentity.cjs
9
+ * (build-identity-stamp PRD): a production npx install ships no .git, so a
10
+ * runtime `git rev-parse` was structurally always null there — this file
11
+ * used to accept that null unconditionally, which made the dead stamp green
12
+ * forever. With src/main/build-info.json present (ensured below — the exact
13
+ * artifact scripts/write-build-info.cjs bakes at publish time), the sha must
14
+ * be real.
15
+ *
8
16
  * Run: timeout 120 npx vitest run src/main/__tests__/scheduler-meta-code-sha.test.cjs
9
17
  */
10
18
 
11
19
  'use strict';
12
20
 
13
- import { test, expect } from 'vitest';
21
+ import { test, expect, afterAll } from 'vitest';
22
+ const fs = require('node:fs');
23
+ const path = require('node:path');
24
+
25
+ const BUILD_INFO_PATH = path.join(__dirname, '..', 'build-info.json');
26
+ const preexistingBuildInfo = fs.existsSync(BUILD_INFO_PATH) ? fs.readFileSync(BUILD_INFO_PATH, 'utf8') : null;
27
+ if (preexistingBuildInfo === null) {
28
+ // eslint-disable-next-line global-require
29
+ require('../../../scripts/write-build-info.cjs').writeBuildInfo();
30
+ }
31
+
14
32
  const { SCHEDULER_BOOTED_AT, SCHEDULER_CODE_SHA } = require('../scheduler.cjs');
15
33
 
34
+ afterAll(() => {
35
+ if (preexistingBuildInfo === null) fs.rmSync(BUILD_INFO_PATH, { force: true });
36
+ else fs.writeFileSync(BUILD_INFO_PATH, preexistingBuildInfo);
37
+ });
38
+
16
39
  test('SCHEDULER_BOOTED_AT is a valid ISO date string', () => {
17
40
  expect(typeof SCHEDULER_BOOTED_AT).toBe('string');
18
41
  expect(Number.isNaN(new Date(SCHEDULER_BOOTED_AT).getTime())).toBe(false);
19
42
  });
20
43
 
21
- test('SCHEDULER_CODE_SHA is null or a short git hex SHA', () => {
22
- expect(SCHEDULER_CODE_SHA === null || /^[0-9a-f]{7,40}$/.test(SCHEDULER_CODE_SHA)).toBe(true);
44
+ test('SCHEDULER_CODE_SHA is a real short git hex SHA when build-info.json is present — never null', () => {
45
+ expect(SCHEDULER_CODE_SHA).toMatch(/^[0-9a-f]{7,40}$/);
23
46
  });