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
@@ -11,7 +11,9 @@
11
11
  const fs = require('node:fs');
12
12
  const os = require('node:os');
13
13
  const path = require('node:path');
14
- const { KIND_CONFIG: WORKTREE_KIND_CONFIG } = require('../../src/main/lib/gitWorktree.cjs');
14
+ const { KIND_CONFIG: WORKTREE_KIND_CONFIG } = require('./gitWorktree.cjs');
15
+ const { OPS_ROOT_DIR } = require('./opsOwnership.cjs');
16
+ const { classifyCwd, worktreeMainRootOf } = require('./cwdClassify.cjs');
15
17
 
16
18
  const HOME = os.homedir();
17
19
  const TMPDIR = os.tmpdir();
@@ -31,7 +33,7 @@ const TMPDIR = os.tmpdir();
31
33
  // could not — e.g. a bare, .git-less worktree-root directory.
32
34
  const TMP_DROP_ROOTS = {
33
35
  exactDropCwds: [TMPDIR],
34
- prefixDropRoots: [WORKTREE_KIND_CONFIG.job.root, WORKTREE_KIND_CONFIG.epic.root],
36
+ get prefixDropRoots() { return [WORKTREE_KIND_CONFIG.job.root, WORKTREE_KIND_CONFIG.epic.root]; },
35
37
  };
36
38
 
37
39
  // Transcript reads only need the last line with a `cwd` field — a few dozen
@@ -41,6 +43,30 @@ const TAIL_BYTES = 64 * 1024;
41
43
  // Safety cap on distinct cwds to bound result set size. O(1) extra space.
42
44
  const MAX_CWDS = 50;
43
45
 
46
+ // activeProjectCwds' scan is a readdirSync of every dir under ~/.claude
47
+ // /projects (2209 dirs / 8.8 GB observed) plus a statSync per transcript —
48
+ // ~270 ms warm. reconcile() calls into it (directly or via allProjectCwds)
49
+ // several times per pass, so cache the resolved cwd list keyed on the
50
+ // resolved argument TUPLE (projectsDir, maxAgeMin, maxCwds) — never on
51
+ // `!opts`, since allProjectCwds always constructs an options object and that
52
+ // guard would cache nothing on the hottest path.
53
+ //
54
+ // Invalidation: projectsDir's own mtime (bumps when a project DIRECTORY is
55
+ // added/removed — NOT when an existing transcript gains a line, which is
56
+ // fine: a project already in the list stays in the list) plus a short TTL
57
+ // as the backstop for everything else. Same mtime-keyed idiom as
58
+ // scheduler/prdParser.cjs's dirCache and queueHistory.cjs's historyCacheKey.
59
+ // TTL sits above the 60 s dispatch-loop interval (scheduler POLL_INTERVAL_MS) so
60
+ // one pass never re-scans cold. Cached arrays are frozen — shared, never mutated.
61
+ const CACHE_TTL_MS = 120_000;
62
+ const EMPTY_CWDS = Object.freeze([]);
63
+ const cwdScanCache = new Map(); // key -> { dirMtimeMs, cachedAt, result }
64
+
65
+ /** Forces the next activeProjectCwds/allProjectCwds call to rescan. */
66
+ function bustProjectCwdCache() {
67
+ cwdScanCache.clear();
68
+ }
69
+
44
70
  // The ops-root folder name. A transcript's `cwd` can point INSIDE it whenever
45
71
  // an agent `cd`s into an artifact directory (a PRD folder, prompt-sessions,
46
72
  // scheduler/state) — Claude Code records the new absolute cwd on every
@@ -55,95 +81,18 @@ const MAX_CWDS = 50;
55
81
  // Truncating at the segment is strictly better than dropping the row: the
56
82
  // ancestor IS the project the agent was working in, so the active-project
57
83
  // signal survives instead of being silently lost.
58
- const OPS_DIRNAME = 'session-manager-operations';
59
-
60
- // Bound on the ancestor walk in worktreeMainRootOf — well past any real
61
- // filesystem depth, purely to guarantee termination without relying on
62
- // reaching '/' (e.g. a symlink loop or an unusually deep path).
63
- const MAX_WORKTREE_WALK = 40;
64
-
65
- const WORKTREES_MARKER = `${path.sep}.git${path.sep}worktrees${path.sep}`;
66
-
67
- /**
68
- * worktreeMainRootOf(cwd) → the main tree's root when cwd sits inside a
69
- * linked `git worktree` (job/epic worktrees under os.tmpdir(), or a
70
- * user-made one anywhere). Walks UP from cwd looking for a `.git` entry:
71
- * - `.git` is a FILE (linked worktree) whose first line is
72
- * `gitdir: <main>/.git/worktrees/<name>` → returns <main>.
73
- * - `.git` is a DIRECTORY (already the main tree) → returns that ancestor.
74
- * - nothing found within MAX_WORKTREE_WALK levels → returns null.
75
- * Pure fs, synchronous, never throws — any unexpected shape (garbled file,
76
- * unreadable entry, gitdir path without the worktrees marker) yields null so
77
- * the caller leaves the cwd unchanged rather than guessing.
78
- */
79
- function worktreeMainRootOf(cwd) {
80
- let dir = cwd;
81
- for (let i = 0; i < MAX_WORKTREE_WALK; i++) {
82
- const gitPath = path.join(dir, '.git');
83
- let stat;
84
- try {
85
- stat = fs.statSync(gitPath);
86
- } catch {
87
- stat = null;
88
- }
89
- if (stat) {
90
- if (stat.isDirectory()) return dir;
91
- if (stat.isFile()) {
92
- let body;
93
- try {
94
- body = fs.readFileSync(gitPath, 'utf8');
95
- } catch {
96
- return null;
97
- }
98
- const firstLine = body.split('\n', 1)[0].trim();
99
- const prefix = 'gitdir:';
100
- if (!firstLine.startsWith(prefix)) return null;
101
- const gitdir = firstLine.slice(prefix.length).trim();
102
- const markerIdx = gitdir.indexOf(WORKTREES_MARKER);
103
- if (markerIdx <= 0) return null;
104
- const candidateMain = gitdir.slice(0, markerIdx);
105
- const worktreeName = gitdir.slice(markerIdx + WORKTREES_MARKER.length).split(path.sep)[0];
106
- // Round-trip verification: gitdir's content is a plain file that
107
- // anything on disk could have written (e.g. a nested `.git` file
108
- // tracked inside an untrusted repo), so it must not be trusted to
109
- // redirect callers to an arbitrary attacker-chosen absolute path.
110
- // A genuine linked worktree's admin dir back-references the exact
111
- // `.git` FILE we are resolving via its own `gitdir` pointer file —
112
- // require that round trip before accepting candidateMain.
113
- if (!worktreeName) return null;
114
- const adminGitdirFile = path.join(candidateMain, '.git', 'worktrees', worktreeName, 'gitdir');
115
- let backRef;
116
- try {
117
- backRef = fs.readFileSync(adminGitdirFile, 'utf8').trim();
118
- } catch {
119
- return null;
120
- }
121
- if (path.resolve(backRef) !== path.resolve(gitPath)) return null;
122
- return candidateMain;
123
- }
124
- return null;
125
- }
126
- const parent = path.dirname(dir);
127
- if (parent === dir) return null;
128
- dir = parent;
129
- }
130
- return null;
131
- }
84
+ const OPS_DIRNAME = OPS_ROOT_DIR;
132
85
 
133
86
  /**
134
87
  * projectRootOf(cwd) → the project root for a cwd that may sit inside an ops
135
88
  * tree and/or a linked git worktree. Returns cwd unchanged when neither
136
- * applies. Only the FIRST ops-dir occurrence matters — a nested stray ops
137
- * root is itself the bug, never a project. The ops-dir truncation runs
138
- * first, then the worktree resolution, so a cwd deep inside a worktree's OWN
139
- * ops tree still lands on the real project's main root.
89
+ * applies, or when the worktree cannot be proven (kind `unknown` — callers that
90
+ * WRITE must consult classifyCwd; this stays total so dispatch never breaks).
91
+ * The classification itself (outermost ops truncation, then worktree
92
+ * resolution) lives in cwdClassify.cjs.
140
93
  */
141
94
  function projectRootOf(cwd) {
142
- const parts = cwd.split(path.sep);
143
- const i = parts.indexOf(OPS_DIRNAME);
144
- const truncated = i > 0 ? (parts.slice(0, i).join(path.sep) || path.sep) : cwd;
145
- const mainRoot = worktreeMainRootOf(truncated);
146
- return mainRoot || truncated;
95
+ return classifyCwd(cwd).projectRoot ?? cwd;
147
96
  }
148
97
 
149
98
  /**
@@ -190,6 +139,23 @@ function activeProjectCwds(maxAgeMin = 90, {
190
139
  maxCwds = MAX_CWDS,
191
140
  tmpDropRoots = TMP_DROP_ROOTS,
192
141
  } = {}) {
142
+ const cacheKey = [projectsDir, maxAgeMin, maxCwds].join('|');
143
+ let dirMtimeMs;
144
+ try {
145
+ dirMtimeMs = fs.statSync(projectsDir).mtimeMs;
146
+ } catch {
147
+ // Missing/unreadable dir: nothing to scan, and any prior cache entry for
148
+ // this key must not survive to be handed back once the dir later
149
+ // appears — drop it rather than caching this empty result.
150
+ cwdScanCache.delete(cacheKey);
151
+ return EMPTY_CWDS;
152
+ }
153
+ const cached = cwdScanCache.get(cacheKey);
154
+ const now0 = Date.now();
155
+ if (cached && cached.dirMtimeMs === dirMtimeMs && now0 - cached.cachedAt < CACHE_TTL_MS) {
156
+ return cached.result;
157
+ }
158
+
193
159
  // maxAgeMin === Infinity means "every project ever seen, no recency filter"
194
160
  // (allProjectCwds below). Date.now() - Infinity is -Infinity, which every
195
161
  // mtime clears — spelled out here because it reads like an accident.
@@ -202,14 +168,18 @@ function activeProjectCwds(maxAgeMin = 90, {
202
168
  // Normalize an ops-internal cwd up to its project root BEFORE any of the
203
169
  // checks below — the stray-ops-root incident (see OPS_DIRNAME above) got
204
170
  // through precisely because such a path is absolute and does exist.
205
- const cwd = projectRootOf(rawCwd);
171
+ const classified = classifyCwd(rawCwd);
172
+ // `unknown` (a .git file we cannot resolve) is non-registrable: registering
173
+ // it would hand every consumer a project whose ops writes are refused.
174
+ if (classified.kind === 'unknown') return;
175
+ const cwd = classified.projectRoot;
206
176
  // Must be ABSOLUTE. A relative fragment would pass the statSync below
207
177
  // whenever it happens to resolve against THIS process's own cwd, and
208
178
  // every consumer (queueStore.projectStateDir, prdLocations) then joins
209
179
  // it into an ops-root path that lands somewhere arbitrary. Callers key
210
180
  // whole per-project state off these strings — a project is a cwd, and a
211
181
  // cwd is an absolute path.
212
- if (!path.isAbsolute(cwd)) return;
182
+ if (!cwd || !path.isAbsolute(cwd)) return;
213
183
  // Drop-guard of last resort: a cwd that is (or sits inside) a known
214
184
  // worktree-scratch root after projectRootOf's normalization is one
215
185
  // worktreeMainRootOf could not resolve to a main tree — never a real
@@ -231,7 +201,7 @@ function activeProjectCwds(maxAgeMin = 90, {
231
201
 
232
202
  // Scan ~/.claude/projects/*/ transcript *.jsonl files.
233
203
  let slugs;
234
- try { slugs = fs.readdirSync(projectsDir); } catch { return result; }
204
+ try { slugs = fs.readdirSync(projectsDir); } catch { cwdScanCache.delete(cacheKey); return Object.freeze(result); }
235
205
 
236
206
  for (const slug of slugs) {
237
207
  if (result.length >= maxCwds) break;
@@ -266,6 +236,8 @@ function activeProjectCwds(maxAgeMin = 90, {
266
236
  }
267
237
  }
268
238
 
239
+ Object.freeze(result);
240
+ cwdScanCache.set(cacheKey, { dirMtimeMs, cachedAt: now0, result });
269
241
  return result;
270
242
  }
271
243
 
@@ -288,4 +260,10 @@ function allProjectCwds(opts = {}) {
288
260
  return activeProjectCwds(Infinity, { maxCwds: 500, ...opts });
289
261
  }
290
262
 
291
- module.exports = { activeProjectCwds, allProjectCwds, projectRootOf, worktreeMainRootOf };
263
+ module.exports = {
264
+ activeProjectCwds,
265
+ allProjectCwds,
266
+ projectRootOf,
267
+ worktreeMainRootOf,
268
+ bustProjectCwdCache,
269
+ };
@@ -0,0 +1,136 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * adoptedRunSupervisor.cjs — re-arms the budget watchdog, idle-tail kill and
5
+ * deadman for an executor the boot SPARED (scheduler.cjs stamps
6
+ * `adoptedAtBoot`), from the durable `<slug>.supervisor.json` record.
7
+ *
8
+ * executeJob attaches those three timers to the child it spawned; an adopted
9
+ * executor belongs to a dead app process, so nothing supervised it (and a
10
+ * quietMachine row held no lease). Here the same three conditions are polled
11
+ * against the record's own numbers (budgetMs / startedAt, idleKillMs vs the
12
+ * log mtime, maxDurationMs) by the app process that owns the row. The
13
+ * signal goes to the recorded process group only after the pid is proven to
14
+ * still be the recorded process (procIdentity, fail-closed). It never mints
15
+ * a slot token: the adopted row stays `untrackedRunning` in pickNextBatch.
16
+ *
17
+ * Plain Node, no electron — every effect is injected so it is unit-testable
18
+ * with fake timers and without scheduler.cjs.
19
+ */
20
+
21
+ /**
22
+ * evaluateAdoptedRun(record, { now, logMtimeMs, defaultMaxDurationMs })
23
+ * → { kind: 'deadman'|'budget'|'idle-tail', reason } | null
24
+ * Pure. Precedence deadman > budget > idle-tail (hardest kill first).
25
+ */
26
+ function evaluateAdoptedRun(record, { now, logMtimeMs, defaultMaxDurationMs = 0 }) {
27
+ const started = Number.isFinite(record.startedAt) ? record.startedAt : null;
28
+ const elapsed = started === null ? null : now - started;
29
+ const deadmanMs = record.maxDurationMs > 0 ? record.maxDurationMs : defaultMaxDurationMs;
30
+ if (elapsed !== null && deadmanMs > 0 && elapsed >= deadmanMs) {
31
+ return { kind: 'deadman', reason: `deadman: ran ${Math.round(elapsed / 60_000)}m (>= ${Math.round(deadmanMs / 60_000)}m)` };
32
+ }
33
+ if (elapsed !== null && record.budgetMs > 0 && elapsed >= record.budgetMs) {
34
+ return {
35
+ kind: 'budget',
36
+ reason: `wall-clock budget exceeded: ran ${Math.round(elapsed / 60_000)}m against a ${Math.round(record.budgetMs / 60_000)}m budget (adopted after app restart)`,
37
+ };
38
+ }
39
+ if (record.idleKillMs > 0 && Number.isFinite(logMtimeMs) && now - logMtimeMs > record.idleKillMs) {
40
+ return { kind: 'idle-tail', reason: `idle-output: log mtime stalled ${Math.round((now - logMtimeMs) / 1000)}s (> ${Math.round(record.idleKillMs / 1000)}s)` };
41
+ }
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * superviseAdoptedRun(record, deps) → { cancel(), check() }
47
+ * One poll timer per adopted run; `check()` runs once immediately so a run
48
+ * already past its budget at boot is killed without waiting a full interval.
49
+ * deps: { logPath, now, statLogMtimeMs, pidAlive, identityOf, isDifferentProcess,
50
+ * killGroup(pid, signal), stampKill(kind, reason) → Promise, log(msg),
51
+ * checkIntervalMs, sigkillAfterMs, defaultMaxDurationMs }
52
+ */
53
+ function superviseAdoptedRun(record, deps) {
54
+ const {
55
+ logPath, now = Date.now, statLogMtimeMs, pidAlive, identityOf, isDifferentProcess, killGroup,
56
+ stampKill, log = () => {}, checkIntervalMs, sigkillAfterMs, defaultMaxDurationMs = 0,
57
+ } = deps;
58
+ let done = false;
59
+ let timer = null;
60
+ let killTimer = null;
61
+
62
+ const isOurs = () => pidAlive(record.pid) && !isDifferentProcess(record.identity, identityOf(record.pid));
63
+ const cancel = () => {
64
+ done = true;
65
+ if (timer) clearInterval(timer);
66
+ };
67
+
68
+ async function check() {
69
+ if (done) return null;
70
+ if (!isOurs()) { cancel(); return 'gone'; } // exited (reaper finalizes) or pid recycled — never signal
71
+ const verdict = evaluateAdoptedRun(record, {
72
+ now: now(), logMtimeMs: statLogMtimeMs(logPath), defaultMaxDurationMs,
73
+ });
74
+ if (!verdict) return null;
75
+ cancel();
76
+ try { await stampKill(verdict.kind, verdict.reason); } catch (e) { log(`stampKill failed: ${e?.message ?? e}`); }
77
+ if (!isOurs()) return 'gone'; // exited while the stamp was in flight
78
+ const first = verdict.kind === 'deadman' ? 'SIGKILL' : 'SIGTERM';
79
+ log(`adopted-run ${verdict.kind} watchdog: ${verdict.reason} — ${first} process group`);
80
+ killGroup(record.pgid || record.pid, first, record.pid);
81
+ if (first === 'SIGTERM') {
82
+ killTimer = setTimeout(() => {
83
+ if (isOurs()) killGroup(record.pgid || record.pid, 'SIGKILL', record.pid);
84
+ }, sigkillAfterMs);
85
+ if (killTimer.unref) killTimer.unref();
86
+ }
87
+ return verdict.kind;
88
+ }
89
+
90
+ timer = setInterval(() => { check().catch((e) => log(`check failed: ${e?.message ?? e}`)); }, checkIntervalMs);
91
+ if (timer.unref) timer.unref();
92
+ return {
93
+ cancel() { cancel(); if (killTimer) clearTimeout(killTimer); },
94
+ check,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * superviseAdoptedRuns(jobs, ctx) → Promise<string[]> (slugs newly armed)
100
+ * Arms a supervisor for every `running` row stamped adoptedAtBoot that has
101
+ * none in `ctx.registry` (Map key `slug|runId`), drops registry entries whose
102
+ * row is no longer that run, re-acquires the quietMachine lease and stamps
103
+ * supervisedAt. ctx: { registry, runDir(row), readRecord(runDir, slug),
104
+ * lease: { acquire, holder }, markSupervised(row), makeDeps(row, record) }
105
+ * Complexity: O(jobs).
106
+ */
107
+ async function superviseAdoptedRuns(jobs, ctx) {
108
+ const { registry, runDir, readRecord, lease, markSupervised, makeDeps } = ctx;
109
+ const live = new Set();
110
+ const armed = [];
111
+ for (const j of jobs) {
112
+ if (j.status !== 'running' || !j.adoptedAtBoot) continue;
113
+ const key = `${j.slug}|${j.runId ?? ''}`;
114
+ live.add(key);
115
+ if (registry.has(key)) continue;
116
+ const dir = runDir(j);
117
+ const record = dir ? readRecord(dir, j.slug) : null;
118
+ if (!record || !record.pid) continue; // no durable record → nothing to re-arm from
119
+ if (j.quietMachine === true && !lease.acquire(j.slug) && lease.holder() !== j.slug) {
120
+ console.warn(`[scheduler] adopted quietMachine run ${j.slug}: lease held by ${lease.holder()} — not re-acquired`);
121
+ }
122
+ const handle = superviseAdoptedRun(record, makeDeps(j, record));
123
+ registry.set(key, handle);
124
+ armed.push(j.slug);
125
+ await markSupervised(j);
126
+ handle.check().catch(() => {});
127
+ }
128
+ for (const [key, handle] of registry) {
129
+ if (live.has(key)) continue;
130
+ handle.cancel();
131
+ registry.delete(key);
132
+ }
133
+ return armed;
134
+ }
135
+
136
+ module.exports = { evaluateAdoptedRun, superviseAdoptedRun, superviseAdoptedRuns };
@@ -3,14 +3,19 @@
3
3
  /**
4
4
  * agentModelResolve.cjs — resolves the `--model` an Epic's session should
5
5
  * launch a headless `claude -p` (or Terminal `claude`) process with: its
6
- * agentType persona's own `model` field (Agent Library) when the Epic has
7
- * one set and it isn't 'inherit', else `fallbackModel`.
6
+ * agentType persona's own `model` field, project-overlay-then-global
7
+ * (`epicMint.cjs`'s `resolvePersonaPaths`), when the Epic has one set and
8
+ * it isn't 'inherit', else `fallbackModel`.
8
9
  *
9
10
  * Extracted from PRD agent-model-default-terminal (which wired this for the
10
11
  * Terminal-view launch in EpicTerminalPane.tsx) so chatRunner.cjs's headless
11
12
  * Chat-view launch resolves the SAME persona-derived model — Chat and
12
13
  * Terminal are two VIEWS over one Epic session (CLAUDE.md) and must agree on
13
- * which model that session launches with.
14
+ * which model that session launches with. Terminal reaches `resolveEpicModel`
15
+ * through the `agents:resolve-epic-model` IPC (main-process only; the
16
+ * renderer has no fs access) rather than a second implementation, so the two
17
+ * views literally call the same function instead of two functions that are
18
+ * merely supposed to agree.
14
19
  *
15
20
  * Plain Node module (sync fs) — no Electron deps, mirrors epicMint.cjs's
16
21
  * readActiveIndex so this can be called from chatRunner.cjs's synchronous
@@ -21,7 +26,8 @@ const fs = require('node:fs');
21
26
  const path = require('node:path');
22
27
  const os = require('node:os');
23
28
  const { splitFrontmatter } = require('./prdFrontmatter.cjs');
24
- const { readActiveIndex } = require('./epicMint.cjs');
29
+ const { readActiveIndex, resolvePersonaPaths } = require('./epicMint.cjs');
30
+ const { PERSONA_NAME_RE } = require('../agentLibrary.cjs');
25
31
  const configMgr = require('../config.cjs');
26
32
 
27
33
  /** The hardcoded floor every branch of resolveEpicModel falls back to — --model must never be omitted (CLAUDE.md "Automation model pinning"). */
@@ -73,30 +79,87 @@ function findAgentTypeByClaudeSessionId(cwd, claudeSessionId, deps = {}) {
73
79
  }
74
80
 
75
81
  /**
76
- * Reads a global agent persona's `model` frontmatter field by name
77
- * (`~/.claude/agents/<agentType>.md`), sync. Returns null on any miss
78
- * (no such file, unreadable, no `model` key) — never throws.
82
+ * Shared miss-tolerant reader `readOverlayAwarePersonaModel` below is a thin
83
+ * wrapper over: tries each candidate path in order (`validatePath` then sync
84
+ * `readFileSync`), and returns the first readable file's `model` frontmatter
85
+ * field — even if that field is absent, which is why an empty-`model`
86
+ * overlay file does NOT fall through to a later candidate (matches
87
+ * `getPersonaBody`'s own semantics). Logs the dangling-persona warning at
88
+ * most once per `(deps.cwd, agentType)` only when EVERY candidate misses.
89
+ * Never throws.
79
90
  */
80
- function readPersonaModel(agentType, deps = {}) {
81
- if (!agentType) return null;
82
- const globalDir = deps.globalDir || path.join(os.homedir(), '.claude', 'agents');
91
+ function readModelFromCandidatePaths(candidates, agentType, deps) {
83
92
  const validatePath = deps.validatePath || configMgr.validatePath;
84
- let real;
85
- try {
86
- real = validatePath(path.join(globalDir, `${agentType}.md`));
87
- } catch {
88
- logDanglingPersonaOnce(deps.cwd, agentType);
89
- return null;
93
+ for (const candidate of candidates) {
94
+ let real;
95
+ try {
96
+ real = validatePath(candidate);
97
+ } catch {
98
+ continue;
99
+ }
100
+ try {
101
+ const text = fs.readFileSync(real, 'utf8');
102
+ const { fm } = splitFrontmatter(text);
103
+ return fm.model || null;
104
+ } catch {
105
+ continue;
106
+ }
90
107
  }
91
- let text;
108
+ logDanglingPersonaOnce(deps.cwd, agentType);
109
+ return null;
110
+ }
111
+
112
+ /**
113
+ * Reads an Epic's agentType persona's `model` frontmatter field with the SAME
114
+ * project-overlay-then-global precedence `agentLibrary.cjs`'s `getPersonaBody`
115
+ * (and, through it, `resolvePrdPersonaForSpawn` below) already applies for a
116
+ * scheduled PRD — `epicMint.cjs`'s `resolvePersonaPaths` is the shared path
117
+ * resolver both readers go through, so there is exactly one place that decides
118
+ * which of the two files wins.
119
+ *
120
+ * Sync (not `getPersonaBody`'s async `fsp.readFile`) because `resolveEpicModel`
121
+ * must stay synchronous: `chatRunner.cjs`'s `executeRun()` registers its
122
+ * cancel handle into `inFlight` synchronously, and its caller (`pump()`) reads
123
+ * that entry back immediately after invoking the executor — an `await`
124
+ * inserted ahead of that registration would run it a tick late and silently
125
+ * break `cancel()`. Returns null on a total miss (neither location resolves)
126
+ * — never throws.
127
+ */
128
+ function readOverlayAwarePersonaModel(agentType, deps = {}) {
129
+ // Same guard agentLibrary.cjs's getPersonaBody applies before it joins
130
+ // `name` into a path: without it, a caller-supplied agentType like
131
+ // "../../other-project/CLAUDE" survives resolvePersonaPaths' plain
132
+ // path.join and validatePath's allowed-roots check (which only confirms
133
+ // the resolved path stays under the home dir / an opened project), letting
134
+ // any readable .md under those roots be read on this --model spawn path.
135
+ if (!agentType || !PERSONA_NAME_RE.test(agentType)) return null;
136
+ const resolvePaths = deps.resolvePersonaPaths || resolvePersonaPaths;
137
+ const { projectPath, globalPath } = resolvePaths(deps.cwd, agentType, deps);
138
+ return readModelFromCandidatePaths([projectPath, globalPath], agentType, deps);
139
+ }
140
+
141
+ /**
142
+ * True when `personaPath` (an already-resolved persona file path, e.g. from
143
+ * agentLibrary.cjs's getPersonaBody) is the PROJECT overlay candidate rather
144
+ * than the global one — the same "which of resolvePersonaPaths' two
145
+ * candidates won" question readModelFromCandidatePaths above answers
146
+ * structurally by trying the project path first. Exported so a caller that
147
+ * already has a resolved persona path from a DIFFERENT reader (one that
148
+ * needs async I/O or richer provenance than this module's sync
149
+ * readOverlayAwarePersonaModel returns — e.g. effectiveModelInfo.cjs) can
150
+ * ask this module the overlay question instead of re-deriving its own
151
+ * resolvePersonaPaths-based comparison. Never throws; a resolution failure
152
+ * conservatively answers "not overlay" (assume global).
153
+ */
154
+ function isProjectOverlayPersonaPath(cwd, agentType, personaPath, deps = {}) {
155
+ if (!personaPath) return false;
92
156
  try {
93
- text = fs.readFileSync(real, 'utf8');
157
+ const resolvePaths = deps.resolvePersonaPaths || resolvePersonaPaths;
158
+ const { projectPath } = resolvePaths(cwd, agentType, deps);
159
+ return Boolean(projectPath) && path.resolve(projectPath) === path.resolve(personaPath);
94
160
  } catch {
95
- logDanglingPersonaOnce(deps.cwd, agentType);
96
- return null;
161
+ return false;
97
162
  }
98
- const { fm } = splitFrontmatter(text);
99
- return fm.model || null;
100
163
  }
101
164
 
102
165
  /**
@@ -105,6 +168,12 @@ function readPersonaModel(agentType, deps = {}) {
105
168
  * file) falls back to `fallbackModel` (default: 'sonnet', the same literal
106
169
  * every claude -p call site pins per CLAUDE.md's model-pinning rule).
107
170
  *
171
+ * This is the SINGLE authority both Chat (`chatRunner.cjs`, in-process) and
172
+ * Terminal (`EpicTerminalPane.tsx`, via the `agents:resolve-epic-model` IPC
173
+ * added in the same change) call for an Epic-backed launch, so the two views
174
+ * of one Epic agree on which model that session launches with — see this
175
+ * module's header.
176
+ *
108
177
  * @param {{ cwd: string, claudeSessionId: string, fallbackModel?: string, deps?: object }} opts
109
178
  * @returns {string}
110
179
  */
@@ -112,7 +181,7 @@ function resolveEpicModel({ cwd, claudeSessionId, fallbackModel = FALLBACK_MODEL
112
181
  try {
113
182
  const agentType = findAgentTypeByClaudeSessionId(cwd, claudeSessionId, deps);
114
183
  if (!agentType) return fallbackModel;
115
- const model = readPersonaModel(agentType, { ...deps, cwd });
184
+ const model = readOverlayAwarePersonaModel(agentType, { ...deps, cwd });
116
185
  if (!model || model === 'inherit') return fallbackModel;
117
186
  return model;
118
187
  } catch {
@@ -181,6 +250,7 @@ module.exports = {
181
250
  FALLBACK_MODEL,
182
251
  resolveEpicModel,
183
252
  findAgentTypeByClaudeSessionId,
184
- readPersonaModel,
253
+ readOverlayAwarePersonaModel,
254
+ isProjectOverlayPersonaPath,
185
255
  resolvePrdPersonaForSpawn,
186
256
  };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Canonical runtime schema for an Agent persona WRITE payload, mirroring the
3
- * TS `AgentPersonaSaveInput` interface at src/preload/api.d.ts:229-246
3
+ * TS `AgentPersonaSaveInput` interface in src/preload/api.d.ts
4
4
  * field-for-field.
5
5
  *
6
6
  * Modelled on promptSessionSchema.cjs / scheduleJobSchema.cjs's "zod schema
@@ -39,7 +39,7 @@ const ProjectEntrySchema = z.string().refine(
39
39
  `must be "${ALL_PROJECTS}" or an absolute path`,
40
40
  );
41
41
 
42
- // Mirrors AgentPersonaSaveInput (src/preload/api.d.ts:229-246). `tags` is the
42
+ // Mirrors AgentPersonaSaveInput (src/preload/api.d.ts). `tags` is the
43
43
  // Epic-mission WorkType union, NOT a free-form string list — the same
44
44
  // concept as PromptSessionSchema's `tag` / scheduleRetagPrd's `tag`, just
45
45
  // plural here since a persona can be associated with more than one mission.
@@ -16,10 +16,9 @@
16
16
  'use strict';
17
17
 
18
18
  const fs = require('node:fs');
19
- const os = require('node:os');
20
19
  const path = require('node:path');
21
20
 
22
- const AUDIT_LOG_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'audit-log.jsonl');
21
+ const { auditLogPath } = require('./schedulerPaths.cjs');
23
22
 
24
23
  /**
25
24
  * Best-effort identity of the process making this call — there is no
@@ -50,11 +49,37 @@ function appendAuditEvent(kind, fields = {}) {
50
49
  ...fields,
51
50
  caller: callerContext(),
52
51
  };
53
- fs.mkdirSync(path.dirname(AUDIT_LOG_PATH), { recursive: true });
54
- fs.appendFileSync(AUDIT_LOG_PATH, `${JSON.stringify(record)}\n`);
52
+ const target = auditLogPath();
53
+ fs.mkdirSync(path.dirname(target), { recursive: true });
54
+ fs.appendFileSync(target, `${JSON.stringify(record)}\n`);
55
55
  } catch (e) {
56
56
  console.error('[auditLog] failed to append audit event', e?.message ?? String(e));
57
57
  }
58
58
  }
59
59
 
60
- module.exports = { appendAuditEvent, AUDIT_LOG_PATH };
60
+ /**
61
+ * readTail(maxBytes, target?) → string[] — the complete JSONL lines within the
62
+ * last `maxBytes` of the log, oldest first. O(maxBytes), never O(file): the
63
+ * audit log is a never-rotated provenance record and grows without bound. A
64
+ * line cut by the window's start is dropped. Missing/unreadable file → [].
65
+ */
66
+ function readTail(maxBytes, target = auditLogPath()) {
67
+ let fd;
68
+ try {
69
+ fd = fs.openSync(target, 'r');
70
+ const { size } = fs.fstatSync(fd);
71
+ const readSize = Math.min(Math.max(0, maxBytes), size);
72
+ if (readSize === 0) return [];
73
+ const buf = Buffer.alloc(readSize);
74
+ fs.readSync(fd, buf, 0, readSize, size - readSize);
75
+ const lines = buf.toString('utf8').split('\n');
76
+ if (readSize < size) lines.shift(); // first element is a partial line
77
+ return lines.filter(Boolean);
78
+ } catch {
79
+ return [];
80
+ } finally {
81
+ if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* */ } }
82
+ }
83
+ }
84
+
85
+ module.exports = { appendAuditEvent, auditLogPath, readTail };