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,88 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * bootSelfHeal.cjs — the epic-index self-heal scan, split out of
5
+ * app.whenReady() so it can run off the boot critical path.
6
+ *
7
+ * A project whose active-index.json is MISSING or fails to parse has lost
8
+ * every open Epic's status unless the per-Epic status mirror
9
+ * (epicStatusMirror.cjs) can rebuild it. `allProjectCwds()` is a synchronous
10
+ * ~270ms directory scan; looping it straight through every project inside
11
+ * app.whenReady()'s synchronous body blocked the event loop long enough that
12
+ * the renderer's first IPC round trips (schedule.state, billing.fetch,
13
+ * teams.list) hit their 5s deadlines before any handler could dispatch.
14
+ *
15
+ * `scheduleBootSelfHeal` defers `runEpicIndexSelfHeal` until the main window
16
+ * has painted (`did-finish-load`), with an unconditional `setImmediate`
17
+ * fallback for the window-destroyed-before-load and no-window (headless)
18
+ * cases, so the scan still runs exactly once per boot either way. It yields
19
+ * to the event loop between projects so a machine with thousands of
20
+ * projects can't stall it for a second straight.
21
+ */
22
+
23
+ async function runEpicIndexSelfHeal(deps) {
24
+ const { fs, allProjectCwds, promptSessionsActiveIndexPath, rebuildActiveIndex, logs } = deps;
25
+ try {
26
+ for (const projectCwd of allProjectCwds()) {
27
+ const indexPath = promptSessionsActiveIndexPath(projectCwd);
28
+ let unclean = false;
29
+ let parseError = null;
30
+ if (!fs.existsSync(indexPath)) {
31
+ unclean = true;
32
+ } else {
33
+ try {
34
+ JSON.parse(fs.readFileSync(indexPath, 'utf8'));
35
+ } catch (e) {
36
+ unclean = true;
37
+ parseError = e.message;
38
+ }
39
+ }
40
+ if (unclean) {
41
+ try {
42
+ const { rows, skipped } = rebuildActiveIndex(projectCwd);
43
+ logs.writeLine({
44
+ scope: 'active-index-rebuild',
45
+ level: 'info',
46
+ message: 'rebuilt active-index.json from status mirrors',
47
+ meta: { cwd: projectCwd, parseError, restored: rows.length, skipped: skipped.length },
48
+ });
49
+ } catch (e) {
50
+ logs.writeLine({ scope: 'active-index-rebuild', level: 'error', message: 'rebuild failed', meta: { cwd: projectCwd, error: e?.message } });
51
+ }
52
+ }
53
+ // Yield between projects so a multi-thousand-project machine can't
54
+ // stall the event loop for a second at a time.
55
+ await new Promise((resolve) => { setImmediate(resolve); });
56
+ }
57
+ } catch (e) {
58
+ logs.writeLine({ scope: 'active-index-rebuild', level: 'error', message: 'boot sweep failed', meta: { error: e?.message } });
59
+ }
60
+ }
61
+
62
+ function scheduleBootSelfHeal(mainWindow, deps) {
63
+ let ran = false;
64
+ const runOnce = () => {
65
+ if (ran) return;
66
+ ran = true;
67
+ runEpicIndexSelfHeal(deps);
68
+ };
69
+
70
+ const webContents = mainWindow && typeof mainWindow.isDestroyed === 'function' && !mainWindow.isDestroyed()
71
+ ? mainWindow.webContents
72
+ : null;
73
+
74
+ if (webContents && !webContents.isDestroyed()) {
75
+ webContents.once('did-finish-load', runOnce);
76
+ // Edge case: the window can be destroyed before it ever finishes
77
+ // loading (closed during boot, crashed renderer). did-finish-load would
78
+ // then never fire and the self-heal would silently never run — fall
79
+ // back to an unconditional setImmediate so it still runs exactly once.
80
+ webContents.once('destroyed', () => setImmediate(runOnce));
81
+ } else {
82
+ // Headless / no-visible-window boot — nothing will ever emit
83
+ // did-finish-load, so run on the very next event-loop turn instead.
84
+ setImmediate(runOnce);
85
+ }
86
+ }
87
+
88
+ module.exports = { scheduleBootSelfHeal, runEpicIndexSelfHeal };
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * buildIdentity.cjs — answers "which code produced this run" for both a
5
+ * published npx install (no .git shipped) and a dev checkout / job worktree
6
+ * (a real .git, but never the source of truth once a build has been baked).
7
+ *
8
+ * Resolution order, memoized after first call:
9
+ * 1. src/main/build-info.json (written by scripts/write-build-info.cjs at
10
+ * `prepack`/`prepublishOnly` time, from the PUBLISHING checkout) — the
11
+ * only artifact that is honest in production, since the npx cache the
12
+ * app actually runs from ships no .git directory at all.
13
+ * 2. `git rev-parse` against the package root, but ONLY when a .git
14
+ * directory exists AT that exact root — never git's own upward walk.
15
+ * Without this guard, a relocated/nested install whose npm cache
16
+ * happens to sit under an ancestor .git would silently stamp a FOREIGN
17
+ * sha (git -C only sets the starting point; it still searches upward
18
+ * for the nearest .git by default). Checking existence at the package
19
+ * root ourselves, and only then invoking git, means the first (and
20
+ * only) .git git can find IS the one we already verified.
21
+ * 3. null.
22
+ */
23
+
24
+ const fs = require('node:fs');
25
+ const path = require('node:path');
26
+ const { execFileSync } = require('node:child_process');
27
+ const { resolveInstallChannel } = require('./machineProfile.cjs');
28
+
29
+ // src/main/lib -> src/main -> src -> package root (repo root in dev, or the
30
+ // npx-cached package dir in production).
31
+ const PACKAGE_ROOT = path.resolve(__dirname, '..', '..', '..');
32
+
33
+ function loadBuildInfoJson(packageRoot) {
34
+ try {
35
+ const parsed = JSON.parse(fs.readFileSync(path.join(packageRoot, 'src', 'main', 'build-info.json'), 'utf8'));
36
+ return parsed && typeof parsed === 'object' ? parsed : null;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function resolvePackageVersion(packageRoot) {
43
+ try {
44
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
45
+ return typeof pkg.version === 'string' ? pkg.version : null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function resolveGitCodeSha(packageRoot) {
52
+ if (!fs.existsSync(path.join(packageRoot, '.git'))) return null;
53
+ try {
54
+ return execFileSync('git', ['-C', packageRoot, 'rev-parse', '--short', 'HEAD'], {
55
+ timeout: 5000,
56
+ encoding: 'utf8',
57
+ stdio: ['ignore', 'pipe', 'ignore'],
58
+ }).trim();
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ function computeBuildIdentity(packageRoot) {
65
+ const buildInfo = loadBuildInfoJson(packageRoot);
66
+ const version = (buildInfo && typeof buildInfo.version === 'string') ? buildInfo.version : resolvePackageVersion(packageRoot);
67
+ const codeSha = buildInfo
68
+ ? ((typeof buildInfo.gitShortSha === 'string') ? buildInfo.gitShortSha : null)
69
+ : resolveGitCodeSha(packageRoot);
70
+ const builtAt = (buildInfo && typeof buildInfo.builtAt === 'string') ? buildInfo.builtAt : null;
71
+ return {
72
+ version,
73
+ codeSha,
74
+ builtAt,
75
+ appPath: packageRoot,
76
+ installChannel: resolveInstallChannel({ appPath: packageRoot, devFlag: !!process.env.SM_DEV }),
77
+ };
78
+ }
79
+
80
+ // Keyed by resolved root: a real process only ever resolves ONE root (this
81
+ // module's own package root), so in production/dev this behaves as a plain
82
+ // singleton memo — the Map only grows past size 1 under test, where each
83
+ // case injects a distinct fixture packageRoot.
84
+ const identityCache = new Map();
85
+
86
+ /**
87
+ * @param {object} [opts]
88
+ * @param {string|null} [opts.bootedAt] - the caller's OWN already-captured
89
+ * process-boot timestamp (e.g. scheduler.cjs's SCHEDULER_BOOTED_AT). Never
90
+ * re-captured here — a memoized resolver that stamped its own "now" would
91
+ * drift from what SCHEDULER_BOOTED_AT was introduced to expose (PRD 812:
92
+ * process staleness relative to on-disk source).
93
+ * @param {string} [opts.packageRoot] - test-only override; production/dev
94
+ * callers never pass this, so it always resolves to this module's own
95
+ * package root.
96
+ */
97
+ function resolveBuildIdentity(opts = {}) {
98
+ const packageRoot = opts.packageRoot || PACKAGE_ROOT;
99
+ if (!identityCache.has(packageRoot)) identityCache.set(packageRoot, computeBuildIdentity(packageRoot));
100
+ return { ...identityCache.get(packageRoot), bootedAt: opts.bootedAt ?? null };
101
+ }
102
+
103
+ /**
104
+ * readInstalledBuildInfo() → the build-info.json currently ON DISK at this
105
+ * package root (fresh read, never the boot-time memo), or null. Compared with
106
+ * the running identity's codeSha to detect that an install/update already
107
+ * replaced the files under a live process.
108
+ */
109
+ function readInstalledBuildInfo(packageRoot = PACKAGE_ROOT) {
110
+ return loadBuildInfoJson(packageRoot);
111
+ }
112
+
113
+ module.exports = { resolveBuildIdentity, readInstalledBuildInfo };
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  const { spawn } = require('node:child_process');
19
- const { resolveClaudeBin } = require('./claudeBin.cjs');
19
+ const { resolveClaudeBin, claudeSpawnTarget } = require('./claudeBin.cjs');
20
20
  const { cleanChildEnv, pathWithUserBins } = require('./cleanEnv.cjs');
21
21
 
22
22
  const CLASSIFY_TIMEOUT_MS = 30_000;
@@ -78,7 +78,8 @@ function classifyPromptTicket(text) {
78
78
 
79
79
  try {
80
80
  // stdin closed — `claude -p` otherwise blocks waiting for piped stdin.
81
- child = spawnImpl(claudeBin, args, { env: childEnv, stdio: ['ignore', 'pipe', 'pipe'] });
81
+ const target = claudeSpawnTarget('aux', 'classify', claudeBin);
82
+ child = spawnImpl(target.command, args, { env: childEnv, stdio: ['ignore', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
82
83
  } catch {
83
84
  finish('inline');
84
85
  return;
@@ -39,6 +39,40 @@ function resolveClaudeBin() {
39
39
  return cached;
40
40
  }
41
41
 
42
+ // ---------- process naming ----------
43
+ //
44
+ // Every `claude` spawn goes through claudeSpawnTarget so GNOME System Monitor
45
+ // shows a distinct comm per role (sm-claude-job / -chat / -aux) and a labelled
46
+ // argv0. See lib/procName.cjs for the mechanics. Invariants:
47
+ // - SM_CLAUDE_BIN set → aliasing SKIPPED, raw value used verbatim (tests point
48
+ // it at self-locating stubs; it must stay ONE binary, no path splitting).
49
+ // - aliasBinFor fails open (returns realBin) → no argv0 is set either, so the
50
+ // fallback is byte-identical to the pre-naming spawn.
51
+ // - Every alias and argv0 label contains the word `claude`, which the four
52
+ // /\bclaude\b/ cmdline gates (killOrphanClaudePid, claudePidAlive,
53
+ // findLiveProcessForJob x2) depend on.
54
+ const ROLE_ALIASES = { job: 'sm-claude-job', chat: 'sm-claude-chat', aux: 'sm-claude-aux' };
55
+
56
+ /**
57
+ * @param {'job'|'chat'|'aux'} role
58
+ * @param {string} [detail] argv0 label detail (job slug / epic session id)
59
+ * @param {string} [realBin] defaults to resolveClaudeBin()
60
+ * @returns {{ command: string, argv0?: string }} spread `argv0` into spawn options
61
+ */
62
+ function claudeSpawnTarget(role, detail, realBin = resolveClaudeBin()) {
63
+ if (process.env.SM_CLAUDE_BIN) return { command: realBin };
64
+ const alias = ROLE_ALIASES[role];
65
+ if (!alias) return { command: realBin };
66
+ try {
67
+ const { aliasBinFor, smArgv0 } = require('./procName.cjs');
68
+ const command = aliasBinFor(realBin, alias);
69
+ if (command === realBin) return { command: realBin };
70
+ return { command, argv0: smArgv0(role, detail) };
71
+ } catch {
72
+ return { command: realBin };
73
+ }
74
+ }
75
+
42
76
  // ---------- version probe ----------
43
77
  //
44
78
  // `claude --version`, cached. The scheduler's launch circuit breaker
@@ -75,7 +109,8 @@ function probeClaudeVersion({ now = Date.now(), execFileImpl } = {}) {
75
109
  // it, and the binary must never be handed the caller's working tree —
76
110
  // a stub `claude` (tests point SM_CLAUDE_BIN at one) that writes
77
111
  // markers or commits into process.cwd() once did so in the real repo.
78
- execFile(bin, ['--version'], { timeout: VERSION_PROBE_TIMEOUT_MS, windowsHide: true, cwd: os.tmpdir() }, (err, stdout) => {
112
+ const target = claudeSpawnTarget('aux', 'version', bin);
113
+ execFile(target.command, ['--version'], { timeout: VERSION_PROBE_TIMEOUT_MS, windowsHide: true, cwd: os.tmpdir(), ...(target.argv0 ? { argv0: target.argv0 } : {}) }, (err, stdout) => {
79
114
  if (err) return done(null);
80
115
  const m = /(\d+\.\d+\.\d+[^\s]*)/.exec(String(stdout || ''));
81
116
  done(m ? m[1] : (String(stdout || '').trim() || null));
@@ -92,4 +127,4 @@ function resetClaudeVersionCache() {
92
127
  versionCache = { at: 0, value: null, inflight: null };
93
128
  }
94
129
 
95
- module.exports = { resolveClaudeBin, probeClaudeVersion, resetClaudeVersionCache };
130
+ module.exports = { resolveClaudeBin, claudeSpawnTarget, probeClaudeVersion, resetClaudeVersionCache };
@@ -35,8 +35,34 @@ function pathWithUserBins() {
35
35
  return base ? `${base}:${userBinDirs().join(':')}` : userBinDirs().join(':');
36
36
  }
37
37
 
38
+ /**
39
+ * SM_PROC_ROLE attributes a child (and, by inheritance, every MCP server the
40
+ * `claude` child spawns — we cannot rename those) via /proc/<pid>/environ.
41
+ * Read by scripts/sm-ps.cjs. NOT SM_PROC_ROOT (procIdentity.cjs's /proc-root
42
+ * test override) — unrelated variable.
43
+ */
44
+ const PROC_ROLE_ENV = 'SM_PROC_ROLE';
45
+
46
+ /** Explicit `SM_PROC_ROLE` in `extra` wins; else inferred from the attribution
47
+ * vars each spawn site already sets (job slug → job, tab id → shell, chat
48
+ * session → chat); else `aux`. Inference keeps role stamping in ONE place. */
49
+ function inferProcRole(extra) {
50
+ if (extra[PROC_ROLE_ENV]) return String(extra[PROC_ROLE_ENV]);
51
+ if (extra.SM_SCHEDULER_JOB_SLUG) return 'job';
52
+ if (extra.SESSION_MANAGER_TAB_ID) return 'shell';
53
+ if (extra.SM_CHAT_SESSION_ID) return 'chat';
54
+ return 'aux';
55
+ }
56
+
57
+ /** Stamp SM_PROC_ROLE onto a raw env object (for sites that deliberately do not
58
+ * use cleanChildEnv's secret stripping, e.g. runClaudeP/docEdit). Returns a copy. */
59
+ function withProcRole(env, role) {
60
+ return { ...env, [PROC_ROLE_ENV]: role };
61
+ }
62
+
38
63
  function cleanChildEnv(extra = {}) {
39
64
  const env = { ...process.env, ...extra };
65
+ env[PROC_ROLE_ENV] = inferProcRole(extra);
40
66
  for (const k of Object.keys(env)) {
41
67
  if (
42
68
  k === 'CLAUDE_EFFORT' ||
@@ -52,4 +78,4 @@ function cleanChildEnv(extra = {}) {
52
78
  return env;
53
79
  }
54
80
 
55
- module.exports = { cleanChildEnv, userBinDirs, pathWithUserBins };
81
+ module.exports = { cleanChildEnv, withProcRole, inferProcRole, PROC_ROLE_ENV, userBinDirs, pathWithUserBins };
@@ -6,7 +6,7 @@ const path = require('node:path');
6
6
  const os = require('node:os');
7
7
  const { spawn, execFileSync } = require('node:child_process');
8
8
  const { cleanChildEnv, pathWithUserBins } = require('./cleanEnv.cjs');
9
- const { resolveClaudeBin } = require('./claudeBin.cjs');
9
+ const { resolveClaudeBin, claudeSpawnTarget } = require('./claudeBin.cjs');
10
10
 
11
11
  const CREDS_PATH = path.join(os.homedir(), '.claude', '.credentials.json');
12
12
 
@@ -188,7 +188,9 @@ function tryCliFallback() {
188
188
  try {
189
189
  // Resolve the absolute binary + inject Homebrew/user bins so this works
190
190
  // even when Electron launched from Finder/Dock with a stripped PATH (mac).
191
- child = spawn(resolveClaudeBin(), ['--version'], {
191
+ const target = claudeSpawnTarget('aux', 'credcheck');
192
+ child = spawn(target.command, ['--version'], {
193
+ ...(target.argv0 ? { argv0: target.argv0 } : {}),
192
194
  stdio: 'ignore',
193
195
  env: cleanChildEnv({ PATH: pathWithUserBins() }),
194
196
  });
@@ -0,0 +1,185 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * cwdClassify.cjs — THE one answer to "what project is this path?".
5
+ *
6
+ * activeSessions.projectRootOf, ephemeralCwd.isEphemeralCwd and
7
+ * opsOwnership.resolveProjectRoot each used to re-derive that answer and each
8
+ * failed OPEN: when a worktree's `.git` FILE survives but its admin-dir
9
+ * back-reference is gone, worktreeMainRootOf returned null, projectRootOf
10
+ * handed the worktree path back unchanged, isLinkedWorktreeRoot said false, and
11
+ * resolveOpsRoot built `<worktree>/session-manager-operations` — the shape
12
+ * behind the lost-Epics incidents. classifyCwd reports BOTH facts (which
13
+ * project, and how sure we are) so a caller can refuse WRITES for the
14
+ * unprovable case (`unknown`) while dispatch and reads carry on.
15
+ *
16
+ * Kinds:
17
+ * worktree — `.git` is a FILE whose gitdir sits under `<main>/.git/worktrees/`
18
+ * AND the main tree's admin dir back-references it (round trip).
19
+ * projectRoot = <main>.
20
+ * project — `.git` is a directory, OR a FILE whose gitdir is NOT under a
21
+ * worktrees/ dir (separate-git-dir / submodule), OR no `.git` at
22
+ * all. projectRoot = the dir holding `.git` (else the cwd).
23
+ * ephemeral — os.tmpdir() itself, or inside a managed job/epic worktree root,
24
+ * and not provably a live worktree.
25
+ * unknown — a `.git` FILE we cannot resolve (garbled, unreadable, worktrees
26
+ * gitdir with a missing/mismatched back-reference). projectRoot is
27
+ * the (ops-truncated) cwd unchanged; ops WRITES must be refused.
28
+ *
29
+ * Two ops-root facts are returned and deliberately NOT unified:
30
+ * outermostOpsRoot — first `session-manager-operations` segment (indexOf).
31
+ * activeSessions truncates here: a stray nested ops root is itself the bug.
32
+ * innermostOpsRoot — last segment (lastIndexOf). opsOwnership.assertOpsWrite
33
+ * derives the project a write targets from here.
34
+ * Both are null when the path is not inside an ops root.
35
+ *
36
+ * Pure fs, synchronous, never throws. Must stay light — required by
37
+ * scripts/hooks/guard-inline-implementation.cjs (via nearestGitEntry) and
38
+ * activeSessions.cjs; no gitWorktree/config requires.
39
+ */
40
+
41
+ const fs = require('node:fs');
42
+ const os = require('node:os');
43
+ const path = require('node:path');
44
+ const schedulerPaths = require('./schedulerPaths.cjs');
45
+
46
+ const OPS_DIRNAME = 'session-manager-operations';
47
+ const WORKTREES_MARKER = `${path.sep}.git${path.sep}worktrees${path.sep}`;
48
+ // Bound on the ancestor walk — past any real fs depth, guarantees termination.
49
+ const MAX_WALK = 40;
50
+
51
+ /**
52
+ * nearestGitEntry(cwd) → { dir, isFile } for the nearest ancestor-or-self that
53
+ * has a `.git` entry (`isFile` true = linked worktree / submodule pointer file),
54
+ * or null. The tiny pure helper for callers that must not load git tooling.
55
+ */
56
+ function nearestGitEntry(cwd) {
57
+ let dir = cwd;
58
+ for (let i = 0; i < MAX_WALK; i++) {
59
+ let stat = null;
60
+ try { stat = fs.statSync(path.join(dir, '.git')); } catch { /* none here */ }
61
+ if (stat) return { dir, isFile: stat.isFile() };
62
+ const parent = path.dirname(dir);
63
+ if (parent === dir) return null;
64
+ dir = parent;
65
+ }
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * Classify the git shape at/above `start`. O(depth) stats + ≤2 small reads.
71
+ * → { shape: 'none' | 'gitdir' | 'worktree' | 'separate' | 'unknown', dir, mainRoot, reason }
72
+ */
73
+ function inspectGit(start) {
74
+ const entry = nearestGitEntry(start);
75
+ if (!entry) return { shape: 'none', dir: null, mainRoot: null, reason: 'no .git found' };
76
+ if (!entry.isFile) return { shape: 'gitdir', dir: entry.dir, mainRoot: null, reason: '.git is a directory' };
77
+
78
+ const gitPath = path.join(entry.dir, '.git');
79
+ let body;
80
+ try {
81
+ body = fs.readFileSync(gitPath, 'utf8');
82
+ } catch (e) {
83
+ return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: `unreadable .git file: ${e?.code || e?.message}` };
84
+ }
85
+ const firstLine = body.split('\n', 1)[0].trim();
86
+ if (!firstLine.startsWith('gitdir:')) {
87
+ return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: '.git file has no "gitdir:" line' };
88
+ }
89
+ const gitdirRaw = firstLine.slice('gitdir:'.length).trim();
90
+ if (!gitdirRaw) {
91
+ return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: '.git file has an empty gitdir' };
92
+ }
93
+ // Submodules point at a RELATIVE gitdir; resolve against the file's own dir.
94
+ const gitdir = path.resolve(entry.dir, gitdirRaw);
95
+ const markerIdx = gitdir.indexOf(WORKTREES_MARKER);
96
+ if (markerIdx === -1) {
97
+ // Not a linked worktree: separate-git-dir / submodule. Its own project.
98
+ return { shape: 'separate', dir: entry.dir, mainRoot: null, reason: 'gitdir is not under .git/worktrees/ (separate-git-dir or submodule)' };
99
+ }
100
+ if (markerIdx <= 0) {
101
+ return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: 'gitdir worktrees marker has no main root above it' };
102
+ }
103
+ const candidateMain = gitdir.slice(0, markerIdx);
104
+ const worktreeName = gitdir.slice(markerIdx + WORKTREES_MARKER.length).split(path.sep)[0];
105
+ if (!worktreeName) {
106
+ return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: 'gitdir names no worktree' };
107
+ }
108
+ // Round-trip: the .git file is plain text anything could have written, so it
109
+ // must not redirect callers to an arbitrary path. The main tree's own admin
110
+ // dir must point back at THIS .git file.
111
+ const adminGitdirFile = path.join(candidateMain, '.git', 'worktrees', worktreeName, 'gitdir');
112
+ let backRef;
113
+ try {
114
+ backRef = fs.readFileSync(adminGitdirFile, 'utf8').trim();
115
+ } catch {
116
+ return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: `worktree admin back-reference missing (${adminGitdirFile})` };
117
+ }
118
+ if (path.resolve(backRef) !== path.resolve(gitPath)) {
119
+ return { shape: 'unknown', dir: entry.dir, mainRoot: null, reason: `worktree admin back-reference points elsewhere (${adminGitdirFile})` };
120
+ }
121
+ return { shape: 'worktree', dir: entry.dir, mainRoot: candidateMain, reason: 'linked worktree, back-reference verified' };
122
+ }
123
+
124
+ /**
125
+ * worktreeMainRootOf(cwd) → the main tree's root when cwd sits inside a
126
+ * verified linked worktree, the ancestor itself when `.git` is a directory,
127
+ * else null (no git, separate-git-dir, or anything unprovable).
128
+ */
129
+ function worktreeMainRootOf(cwd) {
130
+ const g = inspectGit(cwd);
131
+ if (g.shape === 'worktree') return g.mainRoot;
132
+ if (g.shape === 'gitdir') return g.dir;
133
+ return null;
134
+ }
135
+
136
+ function isExactlyTmpdir(absCwd) {
137
+ return absCwd === path.resolve(os.tmpdir());
138
+ }
139
+
140
+ function isUnderManagedWorktreeRoot(absCwd) {
141
+ return ['job', 'epic'].map((k) => path.resolve(schedulerPaths.worktreeRoot(k))).some((root) => {
142
+ const rel = path.relative(root, absCwd);
143
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
144
+ });
145
+ }
146
+
147
+ /**
148
+ * classifyCwd(cwd) → { kind, projectRoot, outermostOpsRoot, innermostOpsRoot, reason }
149
+ * See the file header. A non-string/empty cwd is `unknown` with a null root.
150
+ */
151
+ function classifyCwd(cwd) {
152
+ if (!cwd || typeof cwd !== 'string') {
153
+ return { kind: 'unknown', projectRoot: null, outermostOpsRoot: null, innermostOpsRoot: null, reason: 'cwd is not a non-empty string' };
154
+ }
155
+ const parts = cwd.split(path.sep);
156
+ const first = parts.indexOf(OPS_DIRNAME);
157
+ const last = parts.lastIndexOf(OPS_DIRNAME);
158
+ const opsAt = (i) => (i >= 0 ? (parts.slice(0, i + 1).join(path.sep) || path.sep) : null);
159
+ const outermostOpsRoot = opsAt(first);
160
+ const innermostOpsRoot = opsAt(last);
161
+ // Outermost truncation (the activeSessions law). i === 0 is a relative
162
+ // fragment, never truncated.
163
+ const truncated = first > 0 ? (parts.slice(0, first).join(path.sep) || path.sep) : cwd;
164
+
165
+ const g = inspectGit(truncated);
166
+ const facts = { outermostOpsRoot, innermostOpsRoot };
167
+ if (g.shape === 'worktree') {
168
+ return { kind: 'worktree', projectRoot: g.mainRoot, ...facts, reason: g.reason };
169
+ }
170
+ if (path.isAbsolute(cwd)) {
171
+ const abs = path.resolve(cwd);
172
+ if (isExactlyTmpdir(abs) || isUnderManagedWorktreeRoot(abs)) {
173
+ return { kind: 'ephemeral', projectRoot: truncated, ...facts, reason: 'tmpdir or managed job/epic worktree root' };
174
+ }
175
+ }
176
+ if (g.shape === 'unknown') {
177
+ return { kind: 'unknown', projectRoot: truncated, ...facts, reason: g.reason };
178
+ }
179
+ if (g.shape === 'none') {
180
+ return { kind: 'project', projectRoot: truncated, ...facts, reason: g.reason };
181
+ }
182
+ return { kind: 'project', projectRoot: g.dir, ...facts, reason: g.reason };
183
+ }
184
+
185
+ module.exports = { classifyCwd, nearestGitEntry, worktreeMainRootOf, OPS_DIRNAME };