claude-code-session-manager 0.87.0 → 0.88.1

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 (291) hide show
  1. package/README.md +26 -53
  2. package/dist/assets/{AgentLibrary-DyLWzZDf.js → AgentLibrary-kQF8_wAw.js} +1 -1
  3. package/dist/assets/{DataModel--mISIJ6h.js → DataModel-CY_mMnsr.js} +1 -1
  4. package/dist/assets/{History-C2ahUXTg.js → History-Sji2brb0.js} +1 -1
  5. package/dist/assets/{Hooks-BiC6oyR2.js → Hooks-CtqpO-p5.js} +1 -1
  6. package/dist/assets/{HostBilko-BPleEOld.js → HostBilko-DN0ydOCr.js} +1 -1
  7. package/dist/assets/{Library-Dc8Qst1R.js → Library-CoiLZTKh.js} +1 -1
  8. package/dist/assets/{ListDetail-DIXh-OLX.js → ListDetail-CmqiQ_io.js} +1 -1
  9. package/dist/assets/{MarkdownEditor-C90bkLXK.js → MarkdownEditor-HPoNeKhT.js} +1 -1
  10. package/dist/assets/{McpServers-DqcbLOLZ.js → McpServers-Bj1X3_NT.js} +1 -1
  11. package/dist/assets/{Memory-CW62MXlh.js → Memory-1PKjsM3b.js} +1 -1
  12. package/dist/assets/{Panel-Bw1FhRuF.js → Panel-I1s6ghPo.js} +1 -1
  13. package/dist/assets/{Permissions-BcUC-5y8.js → Permissions-BhiiZjHQ.js} +1 -1
  14. package/dist/assets/{Plugins-BnKx9flD.js → Plugins-gv3_fkPX.js} +2 -2
  15. package/dist/assets/{ProvenanceBadge-Bw5vNVPT.js → ProvenanceBadge-BGcgBd8V.js} +1 -1
  16. package/dist/assets/{SaveBar-CWr0O_w-.js → SaveBar-BGr9eZ4g.js} +1 -1
  17. package/dist/assets/{Scheduler-DYdLuUqq.js → Scheduler-Bdo_awPq.js} +7 -7
  18. package/dist/assets/{ScopeSwitcher-CrBLbg8s.js → ScopeSwitcher-ukl0qWaA.js} +1 -1
  19. package/dist/assets/{Settings-DluB-vN1.js → Settings-DJkFj22G.js} +1 -1
  20. package/dist/assets/{SkillReferenceGraph-CHLSseay.js → SkillReferenceGraph-0t1Qblp6.js} +1 -1
  21. package/dist/assets/{Skills-gNdo_HNK.js → Skills-CeVdBGE-.js} +1 -1
  22. package/dist/assets/{SystemPrompt-Cru05-Ia.js → SystemPrompt-CfKvNHi3.js} +1 -1
  23. package/dist/assets/{TagLibrary-DNHY0xou.js → TagLibrary-DAzNaUPd.js} +1 -1
  24. package/dist/assets/{TiptapBody-I4lmbCgP.js → TiptapBody--S_rGXoA.js} +1 -1
  25. package/dist/assets/{Toggle-bWMHjmRh.js → Toggle-Dszm0xnp.js} +1 -1
  26. package/dist/assets/{index-DV3PorRY.css → index-BHTX4OTc.css} +1 -1
  27. package/dist/assets/{index-fc_JjdxL.js → index-Dp4Rc--q.js} +356 -356
  28. package/dist/assets/{settingsSchema-BfhtZnGD.js → settingsSchema-BLbssMYo.js} +1 -1
  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 +10 -13
  40. package/scripts/README.md +59 -0
  41. package/scripts/audit-ops-hygiene.cjs +350 -0
  42. package/scripts/hooks/guard-destructive-git.cjs +10 -34
  43. package/scripts/hooks/guard-inline-implementation.cjs +14 -8
  44. package/scripts/hooks/guard-prd-writes.cjs +7 -36
  45. package/scripts/hooks/guard-self-schedule.cjs +175 -0
  46. package/scripts/ops-sweep.cjs +355 -0
  47. package/scripts/scheduler-mcp-server.cjs +28 -71
  48. package/src/main/__tests__/bilkoHost-integration.test.cjs +3 -3
  49. package/src/main/__tests__/chat-cancel-terminal.test.cjs +6 -9
  50. package/src/main/__tests__/chat-exit-close-race.test.cjs +3 -3
  51. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +4 -5
  52. package/src/main/__tests__/chat-queue.test.cjs +2 -2
  53. package/src/main/__tests__/chat-stop-signal.test.cjs +2 -2
  54. package/src/main/__tests__/dep-orphan-archive-health.test.cjs +77 -0
  55. package/src/main/__tests__/dod-batchkey.test.cjs +2 -2
  56. package/src/main/__tests__/dod-drain-hook.test.cjs +2 -2
  57. package/src/main/__tests__/dod-report.test.cjs +2 -2
  58. package/src/main/__tests__/dod-reverify.test.cjs +2 -2
  59. package/src/main/__tests__/epicMint.test.cjs +2 -2
  60. package/src/main/__tests__/exchanges.test.cjs +2 -2
  61. package/src/main/__tests__/extractJson.test.cjs +2 -2
  62. package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
  63. package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +556 -0
  64. package/src/main/__tests__/health-build-freshness.test.cjs +39 -0
  65. package/src/main/__tests__/health-delegation-chain.test.cjs +15 -1
  66. package/src/main/__tests__/health-queue-dispatch.test.cjs +58 -7
  67. package/src/main/__tests__/health-tick-liveness.test.cjs +27 -3
  68. package/src/main/__tests__/health-usage-poller.test.cjs +70 -23
  69. package/src/main/__tests__/historyRollup.test.cjs +2 -2
  70. package/src/main/__tests__/kg-augment.test.cjs +2 -2
  71. package/src/main/__tests__/mcpStatus.test.cjs +2 -2
  72. package/src/main/__tests__/memoryAggregate.test.cjs +1 -1
  73. package/src/main/__tests__/memoryStale.test.cjs +1 -1
  74. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +25 -1
  75. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +33 -3
  76. package/src/main/__tests__/prd-group-allocator.test.cjs +2 -2
  77. package/src/main/__tests__/prdAdminRouteParity.test.cjs +2 -0
  78. package/src/main/__tests__/prdAdminRoutes.test.cjs +14 -2
  79. package/src/main/__tests__/prdAuthoringSeed.test.cjs +39 -0
  80. package/src/main/__tests__/prdLocationsArchived.test.cjs +20 -13
  81. package/src/main/__tests__/proc-role-env.test.cjs +125 -0
  82. package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +304 -0
  83. package/src/main/__tests__/procname-sm-processes.test.cjs +127 -0
  84. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +81 -401
  85. package/src/main/__tests__/projectPages.test.cjs +63 -149
  86. package/src/main/__tests__/queue-health-verdict.test.cjs +9 -9
  87. package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +117 -20
  88. package/src/main/__tests__/queueHistory.test.cjs +2 -2
  89. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +38 -3
  90. package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +181 -0
  91. package/src/main/__tests__/runVerify.test.cjs +5 -5
  92. package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +3 -3
  93. package/src/main/__tests__/scheduleJobTransitions.test.cjs +2 -2
  94. package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +143 -0
  95. package/src/main/__tests__/scheduler-autofix-select.test.cjs +2 -2
  96. package/src/main/__tests__/scheduler-autopromote.test.cjs +2 -2
  97. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +3 -5
  98. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +78 -97
  99. package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +161 -0
  100. package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +58 -0
  101. package/src/main/__tests__/scheduler-epic-digest.test.cjs +3 -5
  102. package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +2 -2
  103. package/src/main/__tests__/scheduler-gate-shadow.test.cjs +119 -0
  104. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +46 -0
  105. package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +80 -0
  106. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +25 -18
  107. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +4 -4
  108. package/src/main/__tests__/scheduler-launch-failure.test.cjs +3 -5
  109. package/src/main/__tests__/scheduler-looks-done.test.cjs +26 -7
  110. package/src/main/__tests__/scheduler-manual-pause.test.cjs +118 -0
  111. package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +26 -3
  112. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +17 -2
  113. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +3 -5
  114. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +41 -6
  115. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +62 -6
  116. package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +3 -5
  117. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +23 -4
  118. package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +100 -0
  119. package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +3 -3
  120. package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +4 -4
  121. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +110 -0
  122. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +81 -1
  123. package/src/main/__tests__/scheduler-stall-per-project.test.cjs +14 -0
  124. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +4 -4
  125. package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +245 -0
  126. package/src/main/__tests__/scheduler-supervisor-record.test.cjs +81 -0
  127. package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +2 -2
  128. package/src/main/__tests__/scheduler-tick-wedge.test.cjs +172 -0
  129. package/src/main/__tests__/scheduler-transient-failure.test.cjs +2 -2
  130. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +37 -11
  131. package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +3 -5
  132. package/src/main/__tests__/usageSingleFlight.test.cjs +157 -0
  133. package/src/main/__tests__/workTypeLibrary.test.cjs +1 -1
  134. package/src/main/bilkoHost.cjs +16 -7
  135. package/src/main/build-info.json +8 -0
  136. package/src/main/chatRunner.cjs +4 -2
  137. package/src/main/config.cjs +2 -3
  138. package/src/main/docEdit.cjs +4 -2
  139. package/src/main/health.cjs +276 -52
  140. package/src/main/heapSnapshot.cjs +2 -2
  141. package/src/main/historyAggregator.cjs +1 -1
  142. package/src/main/index.cjs +61 -10
  143. package/src/main/ipcSchemas.cjs +6 -17
  144. package/src/main/lib/__tests__/auditLog.test.cjs +38 -0
  145. package/src/main/lib/__tests__/buildIdentity.test.cjs +121 -0
  146. package/src/main/lib/__tests__/cwdClassify.test.cjs +111 -0
  147. package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +95 -0
  148. package/src/main/lib/__tests__/delegationReadiness.test.cjs +1 -1
  149. package/src/main/lib/__tests__/dispatchLoop.test.cjs +63 -0
  150. package/src/main/lib/__tests__/gateFixtures.json +20 -0
  151. package/src/main/lib/__tests__/gitWorktree.test.cjs +8 -1
  152. package/src/main/lib/__tests__/instanceLock.test.cjs +93 -7
  153. package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +78 -0
  154. package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +11 -0
  155. package/src/main/lib/__tests__/localAdminHttp.test.cjs +1 -1
  156. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +6 -1
  157. package/src/main/lib/__tests__/procIdentity.test.cjs +119 -0
  158. package/src/main/lib/__tests__/procName.test.cjs +92 -0
  159. package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +67 -0
  160. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +6 -7
  161. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +37 -204
  162. package/src/main/lib/__tests__/schedulerPaths.test.cjs +226 -0
  163. package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +94 -0
  164. package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +56 -0
  165. package/src/main/lib/__tests__/sessionSlots.test.cjs +45 -0
  166. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +1 -1
  167. package/src/main/lib/__tests__/upgradeDrain.test.cjs +130 -0
  168. package/src/main/lib/__tests__/usageCircuit.test.cjs +61 -0
  169. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +63 -0
  170. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +73 -0
  171. package/src/main/lib/activeIndexRebuild.cjs +1 -3
  172. package/src/main/lib/activeSessions.cjs +20 -88
  173. package/src/main/lib/adoptedRunSupervisor.cjs +136 -0
  174. package/src/main/lib/agentModelResolve.cjs +33 -1
  175. package/src/main/lib/agentPersonaSchema.cjs +2 -2
  176. package/src/main/lib/auditLog.cjs +30 -5
  177. package/src/main/lib/buildIdentity.cjs +113 -0
  178. package/src/main/lib/classifyPromptTicket.cjs +3 -2
  179. package/src/main/lib/claudeBin.cjs +37 -2
  180. package/src/main/lib/cleanEnv.cjs +27 -1
  181. package/src/main/lib/credentials.cjs +4 -2
  182. package/src/main/lib/cwdClassify.cjs +185 -0
  183. package/src/main/lib/definitionOfDone.cjs +296 -52
  184. package/src/main/lib/dispatchLoop.cjs +40 -0
  185. package/src/main/lib/effectiveModelInfo.cjs +9 -10
  186. package/src/main/lib/ephemeralCwd.cjs +7 -28
  187. package/src/main/lib/gitWorktree.cjs +95 -8
  188. package/src/main/lib/guardShims.cjs +3 -3
  189. package/src/main/lib/historyRollup.cjs +6 -7
  190. package/src/main/lib/instanceLock.cjs +32 -5
  191. package/src/main/lib/jobSupervisorRecord.cjs +147 -0
  192. package/src/main/lib/jobWorktreeBootLive.cjs +9 -5
  193. package/src/main/lib/localAdminHttp.cjs +9 -27
  194. package/src/main/lib/mcpToolCatalog.cjs +29 -77
  195. package/src/main/lib/opsOwnership.cjs +27 -19
  196. package/src/main/lib/prdAuthoringSeed.cjs +36 -0
  197. package/src/main/lib/prdLocations.cjs +48 -1
  198. package/src/main/lib/procIdentity.cjs +126 -0
  199. package/src/main/lib/procName.cjs +98 -0
  200. package/src/main/lib/projectHomeAdminRoutes.cjs +56 -327
  201. package/src/main/lib/queueHistory.cjs +8 -7
  202. package/src/main/lib/queueStore.cjs +57 -37
  203. package/src/main/lib/quietMachineLease.cjs +19 -2
  204. package/src/main/lib/reservationExpiry.cjs +30 -0
  205. package/src/main/lib/runClaudeP.cjs +4 -2
  206. package/src/main/lib/runLogRetention.cjs +3 -2
  207. package/src/main/lib/scheduleJobSchema.cjs +2 -2
  208. package/src/main/lib/scheduleJobTransitions.cjs +29 -2
  209. package/src/main/lib/schedulerBatch.cjs +27 -2
  210. package/src/main/lib/schedulerPaths.cjs +175 -0
  211. package/src/main/lib/schedulerRuntimeState.cjs +59 -0
  212. package/src/main/lib/sessionSlots.cjs +47 -9
  213. package/src/main/lib/smProcNames.cjs +51 -0
  214. package/src/main/lib/upgradeDrain.cjs +188 -0
  215. package/src/main/lib/usageCircuit.cjs +53 -6
  216. package/src/main/lib/watchdogHelpers.cjs +70 -37
  217. package/src/main/lib/withTimeout.cjs +33 -0
  218. package/src/main/mcpStatus.cjs +4 -2
  219. package/src/main/pluginInstall.cjs +6 -2
  220. package/src/main/projectPages.cjs +41 -145
  221. package/src/main/pty.cjs +8 -1
  222. package/src/main/queueOps.cjs +10 -10
  223. package/src/main/runVerify.cjs +69 -3
  224. package/src/main/scheduler.cjs +1607 -383
  225. package/src/main/seedAgentPersonas.cjs +1 -1
  226. package/src/main/seedDevPlugin.cjs +1 -1
  227. package/src/main/seedSchedulerMcp.cjs +7 -6
  228. package/src/main/seedStatus.cjs +1 -1
  229. package/src/main/supervisor.cjs +5 -3
  230. package/src/main/usage.cjs +126 -62
  231. package/src/preload/api.d.ts +18 -64
  232. package/src/preload/index.cjs +2 -4
  233. package/src/seed/agents/project-home-builder.md +31 -48
  234. package/screenshots/.gitkeep +0 -0
  235. package/screenshots/README-screenshots.md +0 -13
  236. package/src/main/lib/projectPageSummarySchema.cjs +0 -181
  237. package/src/main/teams.cjs +0 -95
  238. package/src/main/templates/project-pages-catalog.json +0 -741
  239. package/src/main/templates/project-pages-default-home.html +0 -123
  240. package/src/main/templates/project-pages-pipeline.md +0 -417
  241. package/src/seed/prompts/code-review/ac-coverage-check.md +0 -8
  242. package/src/seed/prompts/code-review/correctness-only.md +0 -8
  243. package/src/seed/prompts/code-review/full-spectrum-high.md +0 -8
  244. package/src/seed/prompts/code-review/hallucination-check.md +0 -8
  245. package/src/seed/prompts/code-review/public-api-compat.md +0 -8
  246. package/src/seed/prompts/code-review/readability-naming.md +0 -8
  247. package/src/seed/prompts/debugging/bug-as-failing-test.md +0 -8
  248. package/src/seed/prompts/debugging/git-bisect-regression.md +0 -8
  249. package/src/seed/prompts/debugging/instrument-intermittent-bug.md +0 -8
  250. package/src/seed/prompts/debugging/localize-pipeline-failure.md +0 -8
  251. package/src/seed/prompts/debugging/reproduce-then-diagnose.md +0 -8
  252. package/src/seed/prompts/documentation/adr-from-change.md +0 -8
  253. package/src/seed/prompts/documentation/module-readme.md +0 -8
  254. package/src/seed/prompts/documentation/onboarding-plan.md +0 -8
  255. package/src/seed/prompts/documentation/refresh-claude-md.md +0 -8
  256. package/src/seed/prompts/documentation/tsdoc-public-exports.md +0 -8
  257. package/src/seed/prompts/git-pr/conventional-commit.md +0 -8
  258. package/src/seed/prompts/git-pr/draft-pr-title-body.md +0 -8
  259. package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +0 -8
  260. package/src/seed/prompts/git-pr/release-notes-block.md +0 -8
  261. package/src/seed/prompts/git-pr/split-large-pr.md +0 -8
  262. package/src/seed/prompts/performance/bundle-startup-audit.md +0 -8
  263. package/src/seed/prompts/performance/complexity-audit.md +0 -8
  264. package/src/seed/prompts/performance/cpu-profile-hot-path.md +0 -8
  265. package/src/seed/prompts/performance/db-query-plan-review.md +0 -8
  266. package/src/seed/prompts/performance/memory-leak-hunt.md +0 -8
  267. package/src/seed/prompts/qa/api-contract-tests.md +0 -8
  268. package/src/seed/prompts/qa/e2e-critical-path.md +0 -8
  269. package/src/seed/prompts/qa/failing-test-for-bug.md +0 -8
  270. package/src/seed/prompts/qa/find-missing-test-coverage.md +0 -8
  271. package/src/seed/prompts/qa/stabilize-flaky-test.md +0 -8
  272. package/src/seed/prompts/qa/tdd-red-first.md +0 -8
  273. package/src/seed/prompts/qa/visual-regression-review.md +0 -8
  274. package/src/seed/prompts/qa/wcag-axe-scan.md +0 -8
  275. package/src/seed/prompts/refactoring/dead-code-sweep.md +0 -8
  276. package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +0 -8
  277. package/src/seed/prompts/refactoring/modernize-legacy-file.md +0 -8
  278. package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +0 -8
  279. package/src/seed/prompts/refactoring/tighten-module-boundaries.md +0 -8
  280. package/src/seed/prompts/security/authz-audit.md +0 -8
  281. package/src/seed/prompts/security/crypto-correctness.md +0 -8
  282. package/src/seed/prompts/security/cwe-top-25-hunt.md +0 -8
  283. package/src/seed/prompts/security/dependency-audit.md +0 -8
  284. package/src/seed/prompts/security/ipc-boundary-hardening.md +0 -8
  285. package/src/seed/prompts/security/owasp-top-10-staged-diff.md +0 -8
  286. package/src/seed/prompts/security/secret-credential-scan.md +0 -8
  287. package/web/README.md +0 -41
  288. package/web/project-pages/logic/dist/logic.cjs +0 -4709
  289. package/web/project-pages/render.cjs +0 -70
  290. package/web/project-pages/renderer/dist/renderer.cjs +0 -18900
  291. package/web/project-pages/validate-summary.cjs +0 -62
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * guard-self-schedule.cjs — PreToolUse hook that denies `ScheduleWakeup`,
4
+ * `CronCreate`, and any backgrounded `Task`/`Agent` invocation
5
+ * (`run_in_background` truthy) from INSIDE a scheduler-spawned headless
6
+ * `claude -p` run.
7
+ *
8
+ * Why this exists: standards.md's Execution discipline section already says,
9
+ * in prose, "You ARE the executor — never re-queue or self-schedule" — and
10
+ * spells out the ScheduleWakeup/background-agent case explicitly. Prose has
11
+ * now failed three times: PRD 460 invoked `/develop` and exited 0 having done
12
+ * nothing; PRD 479 landed its commit, then backgrounded `/code-review --fix`
13
+ * + `/security-review` and called ScheduleWakeup to "wait" for them; PRD 1204
14
+ * (2026-09-13) did the same with the `code-review` skill's 8-agent fan-out —
15
+ * correct, committed-ready work sat in a `ListAgents` wait loop until the
16
+ * reaper terminalized the row, work still uncommitted. A headless run has no
17
+ * later turn: nothing ever delivers the wakeup or collects the background
18
+ * agent's result, so the process just exits and the job parks in
19
+ * `needs_review` even when the underlying work was right. A PreToolUse hook
20
+ * is the one lever that sits OUTSIDE the model and can refuse the call before
21
+ * it schedules anything — see guard-prd-writes.cjs's header for the fuller
22
+ * argument for why a hook, not a rule, is the fix.
23
+ *
24
+ * ── Scope: headless scheduler runs ONLY ──────────────────────────────────
25
+ * Detection is purely env-based: `scheduler.cjs`'s `executeJob` stamps every
26
+ * spawned job's child process (and everything it inherits its env to,
27
+ * including PreToolUse hook subprocesses) with `SM_SCHEDULER_JOB_SLUG` — the
28
+ * same marker `scheduler-mcp-server.cjs` already reads to refuse a self-queue
29
+ * `scheduler_create_prd` call, and `prdCreate.cjs` reads to pick the
30
+ * non-interactive default for a disposition prompt. When that env var is
31
+ * absent (an interactive Epic's Terminal/Chat session), this hook allows
32
+ * everything — an interactive Epic must keep full, unrestricted use of
33
+ * ScheduleWakeup and background agents; that is a legitimate, common pattern
34
+ * this guard must never touch.
35
+ *
36
+ * Unlike `SM_SCHEDULER_JOB_MAY_QUEUE` (which exempts an `architect` persona's
37
+ * job so it may still call `scheduler_create_prd`), this guard does NOT carve
38
+ * out any persona. Every headless job — whatever its `agentType` — runs as a
39
+ * single, un-resumable process; scheduling a wakeup or backgrounding a review
40
+ * agent is equally structurally wrong no matter who the job is running as.
41
+ *
42
+ * ── Fail-open everywhere else ─────────────────────────────────────────────
43
+ * Any internal error (malformed stdin JSON, an unexpected payload shape) is
44
+ * logged to stderr and allowed through — a broken guard must never brick the
45
+ * whole tool. This mirrors guard-prd-writes.cjs, not guard-destructive-git.cjs's
46
+ * fail-closed-on-ambiguity stance: unlike an irreversible git operation, a
47
+ * parse failure here just means the run keeps whatever behavior it already
48
+ * had, which is the safer default for a guard scoped this narrowly.
49
+ *
50
+ * ── Install ───────────────────────────────────────────────────────────────
51
+ * Registered in `guardShims.cjs`'s `GUARD_NAMES` and auto-installed by
52
+ * `delegationReadiness.cjs`'s `ensureGuardsInstalled` (the New Epic readiness
53
+ * banner's silent self-heal path) exactly like the other three guards, via
54
+ * the same stable shim (`~/.claude/session-manager/hooks/guard-self-schedule.cjs`)
55
+ * — never a hard-coded ephemeral `npx` app path. Add to a project's
56
+ * `.claude/settings.json` (project scope only):
57
+ *
58
+ * {
59
+ * "hooks": {
60
+ * "PreToolUse": [
61
+ * {
62
+ * "matcher": "ScheduleWakeup|CronCreate|Task|Agent",
63
+ * "hooks": [
64
+ * { "type": "command", "command": "node scripts/hooks/guard-self-schedule.cjs" }
65
+ * ]
66
+ * }
67
+ * ]
68
+ * }
69
+ * }
70
+ *
71
+ * ── Uninstall ────────────────────────────────────────────────────────────
72
+ * Remove this hook's entry from `.claude/settings.json`. The script is inert
73
+ * with no settings.json entry pointing at it.
74
+ *
75
+ * ── Contract ─────────────────────────────────────────────────────────────
76
+ * - Reads a PreToolUse payload on stdin: { tool_name, tool_input, ... }.
77
+ * - Allowed by construction whenever `process.env.SM_SCHEDULER_JOB_SLUG` is
78
+ * unset — the whole point of this guard is to be provably inert outside a
79
+ * headless scheduler run.
80
+ * - Denies `ScheduleWakeup` and `CronCreate` unconditionally when the marker
81
+ * is present.
82
+ * - Denies `Task`/`Agent` (the harness's own tool name for the Agent tool is
83
+ * `Task` — see classifyTranscriptLine.cjs, which checks both names for the
84
+ * same reason) only when `tool_input.run_in_background` is truthy. An
85
+ * inline/synchronous call (absent or explicitly `false`) is always allowed
86
+ * — standards.md sanctions running a review pass synchronously, inline,
87
+ * before the finish protocol.
88
+ * - Every other tool (including the `Skill` tool generally) is allowed by
89
+ * construction — this guard only ever inspects the four names above.
90
+ */
91
+ 'use strict';
92
+
93
+ const ALWAYS_DENY_TOOLS = new Set(['ScheduleWakeup', 'CronCreate']);
94
+ const BACKGROUNDABLE_AGENT_TOOLS = new Set(['Task', 'Agent']);
95
+
96
+ const STANDARDS_QUOTE = 'You ARE the executor — never re-queue or self-schedule.';
97
+ const STANDARDS_CITE = 'plugins/session-manager-dev/skills/develop/standards.md:85 (Execution discipline)';
98
+
99
+ function readStdin() {
100
+ return new Promise((resolve) => {
101
+ let data = '';
102
+ process.stdin.setEncoding('utf8');
103
+ process.stdin.on('data', (chunk) => { data += chunk; });
104
+ process.stdin.on('end', () => resolve(data));
105
+ // Fail open if stdin never closes/errors for some reason.
106
+ process.stdin.on('error', () => resolve(data));
107
+ });
108
+ }
109
+
110
+ function allow() {
111
+ process.stdout.write(JSON.stringify({ continue: true }));
112
+ process.exit(0);
113
+ }
114
+
115
+ function deny(reason) {
116
+ process.stdout.write(JSON.stringify({
117
+ continue: true,
118
+ decision: 'block',
119
+ reason,
120
+ hookSpecificOutput: {
121
+ hookEventName: 'PreToolUse',
122
+ permissionDecision: 'deny',
123
+ permissionDecisionReason: reason,
124
+ },
125
+ }));
126
+ process.exit(0);
127
+ }
128
+
129
+ async function main() {
130
+ const raw = await readStdin();
131
+ let payload;
132
+ try {
133
+ payload = raw ? JSON.parse(raw) : {};
134
+ } catch (e) {
135
+ console.error(`[guard-self-schedule] malformed stdin JSON, failing open: ${e?.message}`);
136
+ return allow();
137
+ }
138
+
139
+ try {
140
+ const jobSlug = process.env.SM_SCHEDULER_JOB_SLUG;
141
+ if (!jobSlug) return allow(); // interactive session — this guard is inert here
142
+
143
+ const toolName = payload.tool_name;
144
+
145
+ if (ALWAYS_DENY_TOOLS.has(toolName)) {
146
+ const reason = [
147
+ `Blocked: \`${toolName}\` from inside a headless scheduler-spawned run (job '${jobSlug}').`,
148
+ `${STANDARDS_CITE}: "${STANDARDS_QUOTE}"`,
149
+ 'A headless run has no next turn — nothing will ever deliver this wakeup, so the job exits with its work stranded and parks in needs_review even if it was already correct.',
150
+ 'Do it now instead: commit what you have, then run any required review synchronously inline (call the reviewer and read its result in this same turn) before the finish protocol — never schedule it for later.',
151
+ ].join(' ');
152
+ return deny(reason);
153
+ }
154
+
155
+ if (BACKGROUNDABLE_AGENT_TOOLS.has(toolName) && payload.tool_input && payload.tool_input.run_in_background) {
156
+ const reason = [
157
+ `Blocked: a backgrounded ${toolName} call (run_in_background: true) from inside a headless scheduler-spawned run (job '${jobSlug}').`,
158
+ `${STANDARDS_CITE}: "${STANDARDS_QUOTE}"`,
159
+ 'A headless run has no later turn to collect a background agent\'s result, so waiting on it (ListAgents, Monitor, ScheduleWakeup) strands the job in needs_review with its work already done but uncommitted.',
160
+ 'Call it synchronously instead — omit run_in_background (or set it to false) and read its result in this same turn, before the finish protocol.',
161
+ ].join(' ');
162
+ return deny(reason);
163
+ }
164
+
165
+ return allow();
166
+ } catch (e) {
167
+ console.error(`[guard-self-schedule] internal error, failing open: ${e?.message}`);
168
+ return allow();
169
+ }
170
+ }
171
+
172
+ main().catch((e) => {
173
+ console.error(`[guard-self-schedule] unhandled error, failing open: ${e?.message}`);
174
+ allow();
175
+ });
@@ -0,0 +1,355 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ops-sweep.cjs — generalized version of
4
+ * session-manager-operations/architecture/ops-maintenance-protocol.md, runnable
5
+ * against ANY project's `session-manager-operations/` folder (Pattern H in that
6
+ * doc). Read-only: never moves, archives, or deletes anything — it only reports.
7
+ *
8
+ * Unlike this project's own audit-ops-hygiene.cjs (which hardcodes
9
+ * session-manager's own OWNERS vocabulary), this script does not assume any
10
+ * particular namespace list. It re-derives the source-of-truth hierarchy for
11
+ * the TARGET project from what's actually on disk there:
12
+ * 1. That project's own CLAUDE.md (if present) — the declared architecture.
13
+ * 2. Each namespace's own README.md under its ops root.
14
+ * 3. On-disk content — folder names, and archived/processed subfolders.
15
+ *
16
+ * It flags, per namespace:
17
+ * - MISSING_README — namespace dir has no README.md
18
+ * - UNDOCUMENTED — namespace name never mentioned anywhere in the
19
+ * project's ownership doc (see below)
20
+ * - CONTRADICTION — the ownership doc contains both "retired/gone"
21
+ * language AND "owner/owns" language for the same
22
+ * namespace, OR the namespace's own README and the
23
+ * ownership doc disagree on retired-vs-active status
24
+ * - NO_RETENTION_POLICY — namespace has an archived/processed subfolder with
25
+ * files but no retention/prune/expire language in its
26
+ * README (or no README at all)
27
+ * - NESTED_CLAUDE_MD_OVER_BUDGET — any CLAUDE.md strictly below the repo root
28
+ * (excluding node_modules) exceeds 4000 bytes. Nested
29
+ * CLAUDE.md files are scoped-context notes, not the
30
+ * root law file, so they get a fixed 4000-byte cap
31
+ * rather than the root's self-declared SIZE BUDGET.
32
+ *
33
+ * Ownership-doc resolution (UNDOCUMENTED + the retired/owner-signal checks that
34
+ * feed CONTRADICTION): the single-writer OWNERS enumeration used to live only in
35
+ * the target project's root CLAUDE.md. It can now instead live in a scoped
36
+ * `session-manager-operations/CLAUDE.md`, so these checks read
37
+ * `<targetCwd>/session-manager-operations/CLAUDE.md` when that file exists and
38
+ * fall back to `<targetCwd>/CLAUDE.md` otherwise — so a project that hasn't
39
+ * split its ops docs out yet keeps working unchanged. The root CLAUDE.md's own
40
+ * CLAUDE_MD_OVER_BUDGET check (against its self-declared SIZE BUDGET header) is
41
+ * unaffected by this fallback and always reads the root file.
42
+ *
43
+ * Never deletes/migrates/archives anything and never files Epics itself — it
44
+ * only prints a JSON report. Routing findings into proposed Epics is the
45
+ * calling skill's job (session-manager-dev:ops-sweep), reported to the human
46
+ * pointed at the target project's own cwd.
47
+ *
48
+ * Usage: node scripts/ops-sweep.cjs <target-project-cwd>
49
+ */
50
+ 'use strict';
51
+
52
+ const fs = require('node:fs');
53
+ const path = require('node:path');
54
+ const { parseClaudeMdBudget, evaluateClaudeMdBudget, computeEpicIndexDrift } = require('../src/main/health.cjs');
55
+
56
+ const targetCwd = process.argv[2];
57
+ if (!targetCwd) {
58
+ process.stderr.write('usage: ops-sweep.cjs <target-project-cwd>\n');
59
+ process.exit(1);
60
+ }
61
+
62
+ const OPS_ROOT = path.join(targetCwd, 'session-manager-operations');
63
+ const CLAUDE_MD_PATH = path.join(targetCwd, 'CLAUDE.md');
64
+ const OPS_CLAUDE_MD_PATH = path.join(OPS_ROOT, 'CLAUDE.md');
65
+ const NESTED_CLAUDE_MD_BUDGET_BYTES = 4000;
66
+
67
+ // computeClaudeMdSize(claudeMdText) -> { chars, budget, withinBudget } | null
68
+ //
69
+ // Reuses health.cjs's parseClaudeMdBudget/evaluateClaudeMdBudget (same
70
+ // SIZE BUDGET header convention, same byte-length-not-String.length rule)
71
+ // rather than re-implementing the parse here. Returns null when there's no
72
+ // CLAUDE.md at all, matching hasClaudeMd's existing null-safe shape.
73
+ function computeClaudeMdSize(claudeMdText) {
74
+ if (claudeMdText == null) return null;
75
+ const chars = Buffer.byteLength(claudeMdText, 'utf8');
76
+ const budget = parseClaudeMdBudget(claudeMdText);
77
+ const evaluated = evaluateClaudeMdBudget(chars, budget);
78
+ return { chars, budget, withinBudget: evaluated.ok };
79
+ }
80
+
81
+ function readText(p) {
82
+ try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
83
+ }
84
+
85
+ function listDirEntries(p) {
86
+ try { return fs.readdirSync(p, { withFileTypes: true }); } catch { return []; }
87
+ }
88
+
89
+ function countFilesRecursive(dir, depth = 0) {
90
+ if (depth > 4) return 0;
91
+ let count = 0;
92
+ for (const entry of listDirEntries(dir)) {
93
+ if (entry.name.startsWith('.')) continue;
94
+ const full = path.join(dir, entry.name);
95
+ if (entry.isDirectory()) count += countFilesRecursive(full, depth + 1);
96
+ else count += 1;
97
+ }
98
+ return count;
99
+ }
100
+
101
+ function namespaceMentionRegex(name) {
102
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
103
+ // The two negative lookaheads right after `name` reject a match that's
104
+ // actually a SUBFOLDER reference ("scheduler/prds/") rather than a mention
105
+ // of the namespace itself: `(?![\w-])` rejects a longer identifier
106
+ // ("scheduler2"), `(?!/[\w-])` rejects a continuing path segment
107
+ // ("scheduler/prds"). A bare mention or a trailing-slash-then-boundary
108
+ // mention ("`scheduler`", "scheduler/") still matches. Without this, a
109
+ // namespace's own README describing a retired SUBFOLDER (e.g.
110
+ // "scheduler/prds/ is RETIRED") reads as the whole namespace being
111
+ // retired.
112
+ return new RegExp(`[\`/]?${escaped}(?![\\w-])(?!/[\\w-])/?[\`]?`, 'i');
113
+ }
114
+
115
+ // Extracts just the immediate clause around a mention (up to the nearest
116
+ // comma/semicolon/period/parenthesis/em-dash boundary, capped at `radius`
117
+ // chars each way) — CLAUDE.md-style docs often list many namespace names in
118
+ // one long sentence, so scoping to "same sentence" is still too coarse and
119
+ // makes every co-listed namespace look contradictory. Scoping to "same
120
+ // clause" keeps a retired/owner signal tied to the namespace it actually
121
+ // describes.
122
+ const CLAUSE_BOUNDARY_RE = /[,;.()]|—|--/;
123
+ function clauseAround(text, matchIndex, matchLen, radius) {
124
+ const start = Math.max(0, matchIndex - radius);
125
+ const end = Math.min(text.length, matchIndex + matchLen + radius);
126
+ let left = text.slice(start, matchIndex);
127
+ let right = text.slice(matchIndex + matchLen, end);
128
+ const leftBoundary = [...left.matchAll(new RegExp(CLAUSE_BOUNDARY_RE, 'g'))].pop();
129
+ if (leftBoundary) left = left.slice(leftBoundary.index + leftBoundary[0].length);
130
+ const rightBoundary = right.match(CLAUSE_BOUNDARY_RE);
131
+ if (rightBoundary) right = right.slice(0, rightBoundary.index);
132
+ return left + text.slice(matchIndex, matchIndex + matchLen) + right;
133
+ }
134
+
135
+ const RETIRED_RE = /\bretir(ed|ement)?\b|\bis gone\b|\bno longer\b|\bfully retired\b/i;
136
+ const OWNER_RE = /\bOWNERS?\b|\bowns?\b|\bowning\b|\bsole writer\b/i;
137
+ const RETENTION_RE = /\bretention\b|\bprune|\bexpire|\bage out|\bkept for\b|\bkeep(s)? .{0,20}(day|week|month|year)/i;
138
+ const ARCHIVE_DIR_RE = /archiv|process/i;
139
+
140
+ // Scans every namespace-level mention in `text` (per `mentionRe`) and reports
141
+ // whether the immediately surrounding clause carries retired/owner language.
142
+ // Shared by the ownership-doc scan and the namespace's own README scan so
143
+ // both sides of the CONTRADICTION check are scoped identically.
144
+ function clauseMentionSignals(text, mentionRe) {
145
+ let retired = false;
146
+ let owner = false;
147
+ if (!text) return { retired, owner };
148
+ const re = new RegExp(mentionRe.source, 'gi');
149
+ let match;
150
+ while ((match = re.exec(text)) !== null) {
151
+ const clause = clauseAround(text, match.index, match[0].length, 120);
152
+ if (RETIRED_RE.test(clause)) retired = true;
153
+ if (OWNER_RE.test(clause)) owner = true;
154
+ if (re.lastIndex === match.index) re.lastIndex++;
155
+ }
156
+ return { retired, owner };
157
+ }
158
+
159
+ if (!fs.existsSync(OPS_ROOT) || !fs.statSync(OPS_ROOT).isDirectory()) {
160
+ process.stdout.write(JSON.stringify({
161
+ targetCwd,
162
+ opsRoot: OPS_ROOT,
163
+ error: 'no session-manager-operations/ folder found at target cwd',
164
+ hasClaudeMd: fs.existsSync(CLAUDE_MD_PATH),
165
+ claudeMdSize: computeClaudeMdSize(readText(CLAUDE_MD_PATH)),
166
+ namespaces: [],
167
+ findings: [],
168
+ }, null, 2) + '\n');
169
+ process.exit(0);
170
+ }
171
+
172
+ const claudeMd = readText(CLAUDE_MD_PATH);
173
+ const claudeMdSize = computeClaudeMdSize(claudeMd);
174
+ // The OWNERS enumeration may now live in a scoped session-manager-operations/
175
+ // CLAUDE.md instead of the root one — see the header comment. Namespace-level
176
+ // UNDOCUMENTED/retired/owner-signal checks read that file when it exists.
177
+ const ownershipDocPath = fs.existsSync(OPS_CLAUDE_MD_PATH) ? OPS_CLAUDE_MD_PATH : CLAUDE_MD_PATH;
178
+ const ownershipDoc = ownershipDocPath === CLAUDE_MD_PATH ? claudeMd : readText(ownershipDocPath);
179
+ const namespaceDirs = listDirEntries(OPS_ROOT).filter((e) => e.isDirectory());
180
+
181
+ const namespaces = [];
182
+ const findings = [];
183
+
184
+ // Epic index drift (orphan_rows/orphan_files/unmirrored) — same three counts
185
+ // health.cjs's epic_index component reports, reused here (not
186
+ // re-implemented) so the two never disagree. See computeEpicIndexDrift's
187
+ // header in src/main/health.cjs.
188
+ const epicIndexDrift = computeEpicIndexDrift(targetCwd);
189
+ if (epicIndexDrift.orphan_rows > 0) {
190
+ findings.push({
191
+ type: 'EPIC_INDEX_ORPHAN_ROWS',
192
+ detail: `prompt-sessions/active-index.json has ${epicIndexDrift.orphan_rows} row(s) with no matching `
193
+ + `prompt-sessions/<id>.json file (${epicIndexDrift.orphanRowIds.join(', ')})`,
194
+ });
195
+ }
196
+ if (claudeMdSize && !claudeMdSize.withinBudget) {
197
+ findings.push({
198
+ type: 'CLAUDE_MD_OVER_BUDGET',
199
+ detail: `CLAUDE.md is ${claudeMdSize.chars} chars, over its ${claudeMdSize.budget}-char budget by ${claudeMdSize.chars - claudeMdSize.budget}`,
200
+ });
201
+ }
202
+
203
+ for (const entry of namespaceDirs) {
204
+ const name = entry.name;
205
+ const nsPath = path.join(OPS_ROOT, name);
206
+ const readmePath = path.join(nsPath, 'README.md');
207
+ const readme = readText(readmePath);
208
+
209
+ const mentionRe = namespaceMentionRegex(name);
210
+ const mentionedInClaudeMd = ownershipDoc ? mentionRe.test(ownershipDoc) : false;
211
+
212
+ // Clause-scoped, not whole-document: a namespace's own README describing a
213
+ // retired SUBFOLDER of itself ("scheduler/prds/ is RETIRED") must not read
214
+ // as the namespace itself being retired. Scoping both sides (ownership doc
215
+ // AND the namespace's own README) to the clause immediately around an
216
+ // actual namespace-level mention (see namespaceMentionRegex) keeps the two
217
+ // signals comparable apples-to-apples.
218
+ const { retired: claudeMdRetiredSignal, owner: claudeMdOwnerSignal } =
219
+ clauseMentionSignals(ownershipDoc, mentionRe);
220
+ const { retired: readmeRetiredSignal } = clauseMentionSignals(readme, mentionRe);
221
+
222
+ const archiveSubdirs = listDirEntries(nsPath)
223
+ .filter((e) => e.isDirectory() && ARCHIVE_DIR_RE.test(e.name));
224
+ const archivedFileCount = archiveSubdirs.reduce(
225
+ (sum, d) => sum + countFilesRecursive(path.join(nsPath, d.name)),
226
+ 0
227
+ );
228
+ const readmeHasRetentionPolicy = readme ? RETENTION_RE.test(readme) : false;
229
+
230
+ const nsFindings = [];
231
+
232
+ if (!readme) {
233
+ nsFindings.push({ type: 'MISSING_README', detail: `${name}/ has no README.md` });
234
+ }
235
+
236
+ const ownershipDocRelPath = path.relative(targetCwd, ownershipDocPath);
237
+
238
+ if (ownershipDoc && !mentionedInClaudeMd) {
239
+ nsFindings.push({
240
+ type: 'UNDOCUMENTED',
241
+ detail: `${name}/ is never mentioned in ${ownershipDocRelPath} — no declared ownership or explicit non-owned status`,
242
+ });
243
+ }
244
+
245
+ if (ownershipDoc && claudeMdRetiredSignal && claudeMdOwnerSignal) {
246
+ nsFindings.push({
247
+ type: 'CONTRADICTION',
248
+ detail: `${name}/ has both "retired/gone" language and "owner/owns" language in ${ownershipDocRelPath} — the doc contradicts itself on this namespace's status`,
249
+ });
250
+ }
251
+
252
+ if (ownershipDoc && readme && claudeMdRetiredSignal !== readmeRetiredSignal && mentionedInClaudeMd) {
253
+ nsFindings.push({
254
+ type: 'CONTRADICTION',
255
+ detail: `${name}/README.md and ${ownershipDocRelPath} disagree on whether this namespace is retired (README retired=${readmeRetiredSignal}, CLAUDE.md retired=${claudeMdRetiredSignal})`,
256
+ });
257
+ }
258
+
259
+ if (archivedFileCount > 0 && !readmeHasRetentionPolicy) {
260
+ nsFindings.push({
261
+ type: 'NO_RETENTION_POLICY',
262
+ detail: `${name}/ has ${archivedFileCount} file(s) under archived/processed subfolder(s) (${archiveSubdirs.map((d) => d.name).join(', ')}) but no stated retention policy in its README`,
263
+ });
264
+ }
265
+
266
+ namespaces.push({
267
+ name,
268
+ hasReadme: !!readme,
269
+ mentionedInClaudeMd,
270
+ claudeMdRetiredSignal,
271
+ claudeMdOwnerSignal,
272
+ readmeRetiredSignal,
273
+ archivedFileCount,
274
+ readmeHasRetentionPolicy,
275
+ findings: nsFindings,
276
+ });
277
+ findings.push(...nsFindings.map((f) => ({ namespace: name, ...f })));
278
+ }
279
+
280
+ // Source lint (PRD 1082): every path inside the ops root must be built by
281
+ // opsOwnership.cjs's opsPath/resolveOpsRoot — the one place that normalizes
282
+ // a worktree or ops-internal cwd and refuses an ephemeral one. Any other file
283
+ // under src/main/ spelling the ops-root literal is a regression of that choke
284
+ // point and is reported as OPS_PATH_LITERAL. Only meaningful when the target
285
+ // IS the session-manager repo (the module has to exist to be bypassed).
286
+ const opsPathLint = (() => {
287
+ const resolverPath = path.join(targetCwd, 'src', 'main', 'lib', 'opsOwnership.cjs');
288
+ if (!fs.existsSync(resolverPath)) return { applicable: false, violations: [] };
289
+ // The shared scanner is dev-only (not in package.json files); an npm-installed copy of this
290
+ // script has no scanner and simply skips the rule.
291
+ let scan;
292
+ try { ({ scan } = require('./check-ops-path-literals.cjs')); } catch { return { applicable: false, violations: [] }; }
293
+ const violations = scan(targetCwd);
294
+ return { applicable: true, violations };
295
+ })();
296
+ for (const v of opsPathLint.violations) {
297
+ findings.push({
298
+ type: 'OPS_PATH_LITERAL',
299
+ file: v.file,
300
+ line: v.line,
301
+ detail: `ops-root literal outside lib/opsOwnership.cjs — build the path with opsPath()/resolveOpsRoot() instead: ${v.text}`,
302
+ });
303
+ }
304
+
305
+ // Nested-CLAUDE.md budget lint: every CLAUDE.md strictly below the repo root
306
+ // (excluding node_modules and .git) gets a fixed 4000-byte cap — these are
307
+ // scoped-context notes (house style: H1 + "What's here"/"Who consumes
308
+ // this"/"What must NOT be assumed"), not the root law file, so they don't
309
+ // carry a self-declared SIZE BUDGET header the way root CLAUDE.md does.
310
+ const NESTED_CLAUDE_MD_EXCLUDE_DIRS = new Set(['node_modules', '.git']);
311
+ function findNestedClaudeMdFiles(root) {
312
+ const rootClaudeMdPath = path.join(root, 'CLAUDE.md');
313
+ const found = [];
314
+ const walk = (dir) => {
315
+ let entries;
316
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
317
+ for (const e of entries) {
318
+ if (NESTED_CLAUDE_MD_EXCLUDE_DIRS.has(e.name)) continue;
319
+ const full = path.join(dir, e.name);
320
+ if (e.isDirectory()) { walk(full); continue; }
321
+ if (e.name === 'CLAUDE.md' && full !== rootClaudeMdPath) found.push(full);
322
+ }
323
+ };
324
+ walk(root);
325
+ return found;
326
+ }
327
+ const nestedClaudeMdFiles = findNestedClaudeMdFiles(targetCwd);
328
+ for (const p of nestedClaudeMdFiles) {
329
+ const bytes = Buffer.byteLength(fs.readFileSync(p, 'utf8'), 'utf8');
330
+ if (bytes > NESTED_CLAUDE_MD_BUDGET_BYTES) {
331
+ findings.push({
332
+ type: 'NESTED_CLAUDE_MD_OVER_BUDGET',
333
+ file: path.relative(targetCwd, p),
334
+ detail: `${path.relative(targetCwd, p)} is ${bytes} bytes, over the ${NESTED_CLAUDE_MD_BUDGET_BYTES}-byte nested-CLAUDE.md cap by ${bytes - NESTED_CLAUDE_MD_BUDGET_BYTES}`,
335
+ });
336
+ }
337
+ }
338
+
339
+ process.stdout.write(JSON.stringify({
340
+ targetCwd,
341
+ opsRoot: OPS_ROOT,
342
+ hasClaudeMd: !!claudeMd,
343
+ claudeMdSize,
344
+ ownershipDocPath: path.relative(targetCwd, ownershipDocPath),
345
+ opsPathLint,
346
+ nestedClaudeMdFiles: nestedClaudeMdFiles.map((p) => path.relative(targetCwd, p)),
347
+ epicIndexDrift: {
348
+ orphan_rows: epicIndexDrift.orphan_rows,
349
+ orphan_files: epicIndexDrift.orphan_files,
350
+ unmirrored: epicIndexDrift.unmirrored,
351
+ },
352
+ namespaceCount: namespaces.length,
353
+ namespaces,
354
+ findings,
355
+ }, null, 2) + '\n');