session-orchestrator 5.11.0 → 5.13.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 (394) hide show
  1. package/.claude-plugin/marketplace.json +12 -1
  2. package/.claude-plugin/plugin.json +4 -1
  3. package/.codex-plugin/agents/explorer.toml +1 -1
  4. package/.codex-plugin/agents/session-reviewer.toml +2 -2
  5. package/.codex-plugin/agents/wave-worker.toml +1 -1
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.cursor-plugin/plugin.json +1 -1
  8. package/.orchestrator/policy/blocked-commands.json +8 -0
  9. package/.orchestrator/policy/public-export.json +30 -0
  10. package/CHANGELOG.md +180 -3
  11. package/README.md +126 -117
  12. package/agents/analyst.md +7 -2
  13. package/agents/architect-reviewer.md +7 -2
  14. package/agents/code-implementer.md +4 -2
  15. package/agents/db-specialist.md +9 -2
  16. package/agents/dialectic-deriver.md +9 -3
  17. package/agents/docs-writer.md +7 -2
  18. package/agents/eval-judge.md +7 -1
  19. package/agents/qa-strategist.md +7 -2
  20. package/agents/security-reviewer.md +9 -2
  21. package/agents/session-reviewer.md +18 -2
  22. package/agents/skill-applied-judge.md +7 -1
  23. package/agents/test-writer.md +9 -2
  24. package/agents/ui-developer.md +9 -2
  25. package/agents/ux-evaluator.md +6 -1
  26. package/commands/session.md +4 -2
  27. package/commands/templates-ack.md +89 -18
  28. package/docs/agent-authoring.md +30 -11
  29. package/docs/ci-setup.md +38 -2
  30. package/docs/codex-setup.md +33 -9
  31. package/docs/components.md +13 -2
  32. package/docs/cursor-setup.md +3 -3
  33. package/docs/events-schema.md +5 -4
  34. package/docs/evidence-candidates.md +63 -0
  35. package/docs/fleet-checkin-audit.md +7 -4
  36. package/docs/github-mirror-protection.md +150 -3
  37. package/docs/install.md +102 -25
  38. package/docs/instruction-delivery.md +1 -1
  39. package/docs/migration-v4.md +2 -2
  40. package/docs/mods.md +118 -0
  41. package/docs/owner-config-schema.md +1 -0
  42. package/docs/plugin-architecture-v3.md +1 -1
  43. package/docs/pm-skills-marketplace.md +4 -3
  44. package/docs/rule-authoring.md +24 -13
  45. package/docs/scope-collision-guard.md +2 -1
  46. package/docs/session-config-reference.md +55 -21
  47. package/docs/session-config-template.md +32 -24
  48. package/docs/sessions-ledger-repair.md +137 -0
  49. package/docs/vault-docs-architecture.md +1 -1
  50. package/hooks/_lib/hook-import-set.json +406 -73
  51. package/hooks/_lib/hook-root.mjs +301 -0
  52. package/hooks/_lib/lock-bootstrap.mjs +434 -4
  53. package/hooks/_lib/orchestrator-ignore.mjs +3 -0
  54. package/hooks/_lib/subagent-transcript.mjs +2 -2
  55. package/hooks/_lib/vcs-create-matcher.mjs +13 -17
  56. package/hooks/cwd-change-restore.mjs +5 -1
  57. package/hooks/enforce-commands.mjs +4 -3
  58. package/hooks/enforce-scope.mjs +11 -1
  59. package/hooks/hooks-codex.json +48 -2
  60. package/hooks/hooks.json +31 -31
  61. package/hooks/on-session-end.mjs +38 -15
  62. package/hooks/on-session-start.mjs +158 -59
  63. package/hooks/on-stop.mjs +11 -6
  64. package/hooks/post-bash-issue-budget-refund.mjs +6 -1
  65. package/hooks/post-bash-write-verify.mjs +8 -4
  66. package/hooks/post-edit-validate.mjs +45 -52
  67. package/hooks/post-subagent-discovery-validator.mjs +2 -2
  68. package/hooks/post-tool-batch-wave-signal.mjs +8 -4
  69. package/hooks/post-tool-failure-corrective-context.mjs +5 -1
  70. package/hooks/pre-bash-destructive-guard.mjs +220 -8
  71. package/hooks/pre-bash-issue-budget.mjs +6 -1
  72. package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
  73. package/hooks/pre-bash-staging-fence.mjs +258 -14
  74. package/hooks/pre-bash-templates-first.mjs +28 -27
  75. package/hooks/run-node.sh +1 -1
  76. package/hooks/skill-invocation-telemetry.mjs +15 -8
  77. package/hooks/subagent-telemetry.mjs +77 -14
  78. package/hooks/wave-scope-commit-guard.mjs +143 -14
  79. package/mods/so-hud/.claude-plugin/plugin.json +11 -0
  80. package/mods/so-hud/README.md +57 -0
  81. package/mods/so-hud/hooks/beacon-io.mjs +101 -0
  82. package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
  83. package/mods/so-hud/hooks/hooks.json +1 -0
  84. package/mods/so-hud/hooks/logic.ts +182 -0
  85. package/mods/so-hud/hooks/register.tsx +670 -0
  86. package/mods/so-hud/hooks/rules.ts +2998 -0
  87. package/mods/so-hud/tsconfig.json +8 -0
  88. package/mods/so-hud/types/index.d.ts +36 -0
  89. package/package.json +4 -3
  90. package/rules/_index.md +1 -1
  91. package/rules/always-on/ask-via-tool.md +1 -1
  92. package/rules/always-on/loop-and-monitor.md +17 -194
  93. package/rules/always-on/parallel-sessions.md +3 -1
  94. package/rules/opt-in-stack/backend-data.md +1 -1
  95. package/rules/opt-in-stack/backend.md +1 -1
  96. package/rules/opt-in-stack/security-web.md +5 -1
  97. package/scripts/agent-usage-census.mjs +79 -0
  98. package/scripts/apply-session-learnings.mjs +34 -6
  99. package/scripts/archive-closed-prds.mjs +21 -5
  100. package/scripts/autopilot.mjs +11 -5
  101. package/scripts/backfill-abandoned-sessions.mjs +13 -3
  102. package/scripts/backfill-evidence-digest.mjs +2 -1
  103. package/scripts/backfill-learnings-from-vault.mjs +61 -6
  104. package/scripts/backfill-sessions.mjs +16 -2
  105. package/scripts/check-package-manager.mjs +26 -8
  106. package/scripts/check-sessions-integrity.mjs +4 -1
  107. package/scripts/ci/assert-vitest-green.mjs +3 -0
  108. package/scripts/ci/windows-smoke.mjs +201 -0
  109. package/scripts/close-precheck.mjs +55 -0
  110. package/scripts/dialectic-deriver.mjs +20 -12
  111. package/scripts/emit-event.mjs +2 -2
  112. package/scripts/emit-session.mjs +239 -20
  113. package/scripts/eval-session.mjs +17 -3
  114. package/scripts/export-hw-learnings.mjs +29 -2
  115. package/scripts/fleet-checkin-audit.mjs +8 -4
  116. package/scripts/fleet-checkin.mjs +73 -3
  117. package/scripts/gc-stale-worktrees.mjs +6 -1
  118. package/scripts/github-export.mjs +128 -0
  119. package/scripts/github-protection-audit.mjs +3 -1
  120. package/scripts/handover-queue-check.mjs +209 -0
  121. package/scripts/harness-audit.mjs +6 -4
  122. package/scripts/lib/agent-frontmatter.mjs +57 -10
  123. package/scripts/lib/agent-status.mjs +125 -7
  124. package/scripts/lib/agent-usage-census.mjs +394 -0
  125. package/scripts/lib/auto-dialectic.mjs +17 -10
  126. package/scripts/lib/auto-dream.mjs +22 -14
  127. package/scripts/lib/autopilot/loop.mjs +3 -1
  128. package/scripts/lib/autopilot/recent-runs.mjs +4 -2
  129. package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
  130. package/scripts/lib/autopilot/telemetry.mjs +5 -1
  131. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  132. package/scripts/lib/baseline-archetypes.mjs +10 -2
  133. package/scripts/lib/blocked-commands-policy.mjs +4 -3
  134. package/scripts/lib/build-live-signals.mjs +13 -9
  135. package/scripts/lib/ci-status-banner.mjs +329 -24
  136. package/scripts/lib/codex/plugin-contract.mjs +8 -0
  137. package/scripts/lib/cold-start-detector.mjs +5 -2
  138. package/scripts/lib/command-blocker.mjs +603 -43
  139. package/scripts/lib/common.mjs +3 -2
  140. package/scripts/lib/config/cross-repo.mjs +74 -7
  141. package/scripts/lib/config/host-paths.mjs +6 -2
  142. package/scripts/lib/config/remote-hosts.mjs +2 -2
  143. package/scripts/lib/config/section-extractor.mjs +65 -11
  144. package/scripts/lib/config/template-keys.mjs +153 -0
  145. package/scripts/lib/config/vault-integration.mjs +1 -0
  146. package/scripts/lib/config.mjs +3 -3
  147. package/scripts/lib/coordinator-snapshot.mjs +1 -1
  148. package/scripts/lib/discovery/triage-state.mjs +2 -1
  149. package/scripts/lib/dispatcher/rank.mjs +5 -7
  150. package/scripts/lib/ecosystem-health.mjs +2 -2
  151. package/scripts/lib/eval/engine.mjs +3 -2
  152. package/scripts/lib/eval/session-resolve.mjs +6 -2
  153. package/scripts/lib/eval/sink.mjs +4 -3
  154. package/scripts/lib/events-retention-banner.mjs +3 -2
  155. package/scripts/lib/events-schema.mjs +10 -0
  156. package/scripts/lib/events.mjs +23 -12
  157. package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
  158. package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
  159. package/scripts/lib/fleet-protocol.mjs +21 -0
  160. package/scripts/lib/gates/gate-full.mjs +172 -11
  161. package/scripts/lib/gates/gate-helpers.mjs +236 -23
  162. package/scripts/lib/git-config-drift.mjs +141 -7
  163. package/scripts/lib/github-export.mjs +447 -0
  164. package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
  165. package/scripts/lib/handover-queue-marks.mjs +159 -0
  166. package/scripts/lib/hardware-pattern-detector.mjs +103 -13
  167. package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
  168. package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
  169. package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
  170. package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
  171. package/scripts/lib/host-identity.mjs +9 -5
  172. package/scripts/lib/http-request.mjs +64 -0
  173. package/scripts/lib/instruction-budget-guard.mjs +30 -22
  174. package/scripts/lib/io.mjs +6 -3
  175. package/scripts/lib/issue-budget-reconcile.mjs +3 -2
  176. package/scripts/lib/issue-budget.mjs +36 -7
  177. package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
  178. package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
  179. package/scripts/lib/learnings/io.mjs +289 -15
  180. package/scripts/lib/learnings/select.mjs +53 -4
  181. package/scripts/lib/learnings/store-role.mjs +209 -0
  182. package/scripts/lib/learnings/surface.mjs +46 -7
  183. package/scripts/lib/ledger-path.mjs +113 -0
  184. package/scripts/lib/maintenance-due-banner.mjs +63 -12
  185. package/scripts/lib/memory-banner.mjs +5 -6
  186. package/scripts/lib/memory-proposals/collector.mjs +5 -2
  187. package/scripts/lib/memory-proposals/sink.mjs +7 -4
  188. package/scripts/lib/memory-proposals/store.mjs +22 -5
  189. package/scripts/lib/mode-selector-accuracy.mjs +2 -1
  190. package/scripts/lib/no-follow-open.mjs +57 -0
  191. package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
  192. package/scripts/lib/own-lock-root.mjs +95 -0
  193. package/scripts/lib/owner-interview.mjs +2 -1
  194. package/scripts/lib/owner-yaml.mjs +70 -7
  195. package/scripts/lib/platform.mjs +161 -9
  196. package/scripts/lib/plugin-update-banner.mjs +15 -11
  197. package/scripts/lib/pre-push-local-partial.mjs +117 -0
  198. package/scripts/lib/pre-push-remote-gate.mjs +151 -25
  199. package/scripts/lib/process-group.mjs +270 -34
  200. package/scripts/lib/project-hygiene.mjs +28 -0
  201. package/scripts/lib/quality-gate.mjs +33 -9
  202. package/scripts/lib/reconcile/backlog.mjs +29 -15
  203. package/scripts/lib/reconcile/engine.mjs +26 -2
  204. package/scripts/lib/reconcile/idempotency.mjs +65 -3
  205. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
  206. package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
  207. package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
  208. package/scripts/lib/resource-probe/evaluate.mjs +32 -9
  209. package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
  210. package/scripts/lib/rule-loader.mjs +20 -10
  211. package/scripts/lib/scope-baseline.mjs +123 -8
  212. package/scripts/lib/scope-echo.mjs +5 -2
  213. package/scripts/lib/scope-gate.mjs +55 -20
  214. package/scripts/lib/session-close-backfill.mjs +75 -7
  215. package/scripts/lib/session-end/close-precheck.mjs +151 -0
  216. package/scripts/lib/session-end/phase-skip.mjs +8 -7
  217. package/scripts/lib/session-end/tail-runner.mjs +7 -2
  218. package/scripts/lib/session-end/vault-orphans.mjs +205 -0
  219. package/scripts/lib/session-id.mjs +9 -1
  220. package/scripts/lib/session-identity/own-session.mjs +63 -0
  221. package/scripts/lib/session-invocation.mjs +14 -3
  222. package/scripts/lib/session-lock.mjs +114 -0
  223. package/scripts/lib/session-record-repair.mjs +245 -35
  224. package/scripts/lib/session-registry.mjs +85 -12
  225. package/scripts/lib/session-schema/constants.mjs +9 -0
  226. package/scripts/lib/session-schema/filters.mjs +56 -1
  227. package/scripts/lib/session-schema/validator.mjs +15 -5
  228. package/scripts/lib/session-start-probes.mjs +149 -22
  229. package/scripts/lib/session-token-rollup.mjs +2 -1
  230. package/scripts/lib/session-work-trees.mjs +166 -0
  231. package/scripts/lib/sessions-canonical.mjs +61 -3
  232. package/scripts/lib/sessions-integrity-banner.mjs +17 -39
  233. package/scripts/lib/sessions-staleness-banner.mjs +8 -5
  234. package/scripts/lib/sh-quote.mjs +1 -0
  235. package/scripts/lib/skill-evidence-window.mjs +5 -1
  236. package/scripts/lib/skill-health/join.mjs +10 -9
  237. package/scripts/lib/skill-invocations-schema.mjs +13 -0
  238. package/scripts/lib/skill-judgments-schema.mjs +6 -2
  239. package/scripts/lib/snapshot-probe.mjs +414 -0
  240. package/scripts/lib/so-hud/collect.mjs +174 -0
  241. package/scripts/lib/so-hud/runtime.mjs +87 -0
  242. package/scripts/lib/spiral-carryover.mjs +8 -1
  243. package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
  244. package/scripts/lib/state-md/idle-reset.mjs +248 -0
  245. package/scripts/lib/state-md/mission-status.mjs +5 -1
  246. package/scripts/lib/state-md/recommendations.mjs +26 -4
  247. package/scripts/lib/state-md-peer-guard.mjs +2 -0
  248. package/scripts/lib/state-md.mjs +31 -8
  249. package/scripts/lib/sunset/walker.mjs +4 -3
  250. package/scripts/lib/telemetry/consent.mjs +4 -2
  251. package/scripts/lib/telemetry/queue.mjs +3 -3
  252. package/scripts/lib/telemetry/sync.mjs +45 -9
  253. package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
  254. package/scripts/lib/tmux-layout/layouts.mjs +20 -3
  255. package/scripts/lib/ux-grill/paths.mjs +5 -1
  256. package/scripts/lib/validate/check-agents.mjs +37 -6
  257. package/scripts/lib/validate/check-component-paths.mjs +25 -1
  258. package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
  259. package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
  260. package/scripts/lib/validate/check-json-files.mjs +3 -1
  261. package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
  262. package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
  263. package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
  264. package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
  265. package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
  266. package/scripts/lib/validate/check-unwired-features.mjs +4 -2
  267. package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
  268. package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
  269. package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
  270. package/scripts/lib/validate/confidential-names.mjs +3 -1
  271. package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
  272. package/scripts/lib/vault-mirror/namespace.mjs +22 -9
  273. package/scripts/lib/vault-mirror/process.mjs +197 -65
  274. package/scripts/lib/vault-mirror/prune.mjs +5 -2
  275. package/scripts/lib/vault-mirror/render-learnings.mjs +8 -7
  276. package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
  277. package/scripts/lib/vault-mirror/retention.mjs +2 -2
  278. package/scripts/lib/vault-mirror/utils.mjs +106 -13
  279. package/scripts/lib/vault-staleness-banner.mjs +4 -7
  280. package/scripts/lib/vault-status/board-writer.mjs +34 -5
  281. package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
  282. package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
  283. package/scripts/lib/win-process-tree.mjs +137 -0
  284. package/scripts/lib/workspace.mjs +1 -1
  285. package/scripts/lib/worktree/constants.mjs +1 -1
  286. package/scripts/lib/worktree/lifecycle.mjs +1 -1
  287. package/scripts/lib/worktree/listing.mjs +1 -1
  288. package/scripts/lib/zx.mjs +50 -0
  289. package/scripts/mcp-server.mjs +16 -7
  290. package/scripts/mcp-server.sh +13 -83
  291. package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
  292. package/scripts/memory-propose.mjs +10 -3
  293. package/scripts/migrate-cold-start-seed.mjs +7 -0
  294. package/scripts/migrate-legacy-learnings.sh +22 -0
  295. package/scripts/migrate-sessions-jsonl.mjs +16 -2
  296. package/scripts/migrate-vault-paths.mjs +29 -13
  297. package/scripts/parse-config.mjs +71 -14
  298. package/scripts/print-learnings-index.mjs +25 -2
  299. package/scripts/release-session-lock.mjs +94 -7
  300. package/scripts/release.mjs +451 -24
  301. package/scripts/relocate-vault-corpus.mjs +11 -7
  302. package/scripts/repair-invalid-sessions.mjs +68 -3
  303. package/scripts/resolve-session-invocation.mjs +57 -8
  304. package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
  305. package/scripts/run-quality-gate.mjs +33 -7
  306. package/scripts/self-update.mjs +56 -7
  307. package/scripts/session-commit-files.mjs +133 -0
  308. package/scripts/site-numbers.mjs +52 -9
  309. package/scripts/snapshot-probe.mjs +156 -0
  310. package/scripts/so-hud.mjs +68 -0
  311. package/scripts/state-md-idle-reset.mjs +82 -0
  312. package/scripts/sweep-expired-learnings.mjs +54 -6
  313. package/scripts/sweep-expired-rules.mjs +4 -1
  314. package/scripts/validate-plugin.mjs +22 -0
  315. package/scripts/vault-integration-watcher.mjs +54 -6
  316. package/scripts/vault-mirror.mjs +9 -0
  317. package/scripts/vault-orphans.mjs +171 -0
  318. package/skills/_shared/fleet-protocol.md +166 -3
  319. package/skills/_shared/instruction-file-resolution.md +1 -1
  320. package/skills/_shared/monitor-patterns.md +44 -25
  321. package/skills/_shared/platform-tools.md +7 -0
  322. package/skills/_shared/state-ownership.md +5 -4
  323. package/skills/architecture/references/domain-model.md +1 -1
  324. package/skills/autopilot/SKILL.md +3 -2
  325. package/skills/bootstrap/SKILL.md +3 -1
  326. package/skills/bootstrap/_shared-template.md +4 -4
  327. package/skills/bootstrap/fast-template.md +1 -1
  328. package/skills/bootstrap/private-contract.md +1 -1
  329. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
  330. package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
  331. package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
  332. package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
  333. package/skills/claude-md-drift-check/SKILL.md +3 -3
  334. package/skills/claude-md-drift-check/checker.mjs +61 -101
  335. package/skills/close/SKILL.md +20 -7
  336. package/skills/discovery/probes/docs-staleness.mjs +7 -5
  337. package/skills/discovery/probes/frontend-slop.mjs +7 -3
  338. package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
  339. package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
  340. package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
  341. package/skills/discovery/probes/vault-staleness.mjs +5 -3
  342. package/skills/discovery/probes-session.md +3 -0
  343. package/skills/discovery/probes-vault.md +2 -0
  344. package/skills/dispatcher/SKILL.md +1 -1
  345. package/skills/eval/SKILL.md +3 -1
  346. package/skills/evolve/SKILL.md +2 -0
  347. package/skills/evolve/references/evolve-analyze-mode.md +3 -3
  348. package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
  349. package/skills/gitlab-ops/SKILL.md +7 -6
  350. package/skills/gitlab-portfolio/SKILL.md +2 -2
  351. package/skills/go/SKILL.md +13 -1
  352. package/skills/npm-publish/SKILL.md +5 -2
  353. package/skills/plan/SKILL.md +23 -10
  354. package/skills/plan/mode-feature.md +13 -8
  355. package/skills/plan/mode-new.md +6 -14
  356. package/skills/plan/prd-feature-template.md +19 -0
  357. package/skills/plan/prd-full-template.md +12 -5
  358. package/skills/plan/prd-premortem-panel.md +48 -0
  359. package/skills/plan/prd-reviewer-prompt.md +11 -3
  360. package/skills/plan/soul.md +1 -1
  361. package/skills/portfolio/SKILL.md +5 -6
  362. package/skills/quality-gates/SKILL.md +2 -4
  363. package/skills/reconcile/SKILL.md +27 -4
  364. package/skills/release/SKILL.md +16 -8
  365. package/skills/session-end/SKILL.md +97 -13
  366. package/skills/session-end/phase-3-2-docs-verification.md +1 -0
  367. package/skills/session-end/phase-3-6-tail.md +11 -7
  368. package/skills/session-end/phase-3-7a-recommendations.md +9 -2
  369. package/skills/session-end/plan-verification.md +16 -1
  370. package/skills/session-end/references/phase-3-7e-vault-orphans.md +63 -0
  371. package/skills/session-end/references/phase-3-documentation-updates.md +25 -13
  372. package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
  373. package/skills/session-end/references/session-summary-template.md +8 -0
  374. package/skills/session-end/session-metrics-write.md +7 -4
  375. package/skills/session-plan/SKILL.md +5 -3
  376. package/skills/session-start/SKILL.md +10 -4
  377. package/skills/session-start/phase-4-5-resource-health.md +11 -5
  378. package/skills/session-start/phase-7-5-mode-selector.md +3 -1
  379. package/skills/session-start/phase-8-5-express-path.md +4 -3
  380. package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
  381. package/skills/session-start/references/phase-1-5-session-continuity.md +13 -5
  382. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  383. package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
  384. package/skills/tmux-layout/SKILL.md +2 -2
  385. package/skills/vault-sync/validator.mjs +3 -2
  386. package/skills/wave-executor/SKILL.md +5 -4
  387. package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
  388. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
  389. package/skills/wave-executor/references/wave-loop-review.md +13 -5
  390. package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
  391. package/AGENTS.md +0 -207
  392. package/assets/og-card.png +0 -0
  393. package/assets/og-card.svg +0 -47
  394. package/assets/wave-lifecycle.svg +0 -98
@@ -9,8 +9,10 @@
9
9
  * 1. oom-kill — stop events with exitCode 137 or "Out of memory" marker
10
10
  * 2. heartbeat-gap — registry sweep.log entries for the same host
11
11
  * (session disappeared without a clean stop — freeze proxy)
12
- * 3. concurrent-session-pressure — session events where peer_count crossed
13
- * the configured concurrent-sessions-warn threshold
12
+ * 3. concurrent-session-pressure — session-start events inside a start wave:
13
+ * `recent_starts_count` (foreign starts in the last 5 min,
14
+ * emitted since #1650) >= startWaveMin. Re-aimed in #1654
15
+ * off `peer_count`, a standing state (see the detector).
14
16
  * 4. disk-full — events with ENOSPC / "no space left" markers
15
17
  * 5. thermal-throttle — sustained high CPU load + unexpected slow-wave marker
16
18
  *
@@ -28,8 +30,14 @@
28
30
  export const DEFAULT_THRESHOLDS = Object.freeze({
29
31
  /** Minimum recurrence for a candidate to be emitted (same host_class). */
30
32
  minOccurrences: 2,
31
- /** Minimum peer_count to count as concurrent-session pressure. */
32
- concurrentSessionsWarn: 5,
33
+ /**
34
+ * Minimum `recent_starts_count` (foreign session starts in the 5 min before
35
+ * this start) to count as concurrent-session pressure. Mirrors START_WAVE_MIN
36
+ * in hooks/on-session-start.mjs (#1650). Replaces `concurrentSessionsWarn`
37
+ * (#1654), which was compared against `peer_count`; a caller still passing
38
+ * that key is ignored on purpose rather than silently fed the old meaning.
39
+ */
40
+ startWaveMin: 5,
33
41
  /** CPU load percentage that marks a thermal-throttle signal. */
34
42
  thermalCpuLoadPct: 85,
35
43
  /** Heartbeat gap threshold in minutes above which a session is "frozen". */
@@ -93,15 +101,97 @@ function detectHeartbeatGap(sweepLogEntries, thresholdMin) {
93
101
  }));
94
102
  }
95
103
 
104
+ /**
105
+ * Fallback wave window for start events that predate the producer writing
106
+ * `recent_starts_window_min` beside `recent_starts_count`. Mirrors the default
107
+ * of `countRecentStarts()` in scripts/lib/session-registry.mjs; the producer's
108
+ * own constant (START_WAVE_WINDOW_MIN in hooks/on-session-start.mjs) is
109
+ * module-private to a hook script and arrives per event instead, so the event
110
+ * field always wins over this value.
111
+ */
112
+ const FALLBACK_START_WAVE_WINDOW_MIN = 5;
113
+
114
+ /**
115
+ * Start-wave detector (#1654). Keyed on `recent_starts_count`, NOT `peer_count`:
116
+ * `peer_count >= 5` held on 880/1305 = 67.4% of M4 and 255/991 = 25.7% of M5
117
+ * starts since 2026-08-24 (#1650) — a standing condition, so every learning
118
+ * derived from it was noise (HR-101). A start wave fired on 2.7% / 5.2%.
119
+ *
120
+ * Events without a numeric `recent_starts_count` (every pre-#1650 record) carry
121
+ * no value on this branch: they are skipped, never read as 0 and never as a hit.
122
+ * The signal name stays `concurrent-session-pressure` so existing learnings'
123
+ * subjects (`concurrent-session-pressure::<host_class>`) remain stable.
124
+ *
125
+ * One hit per WAVE, not per event (#1664): with >= 7 starts in one wave, the
126
+ * 6th and 7th start each see >= 5 foreign starts, so counting events let a
127
+ * single wave satisfy `minOccurrences: 2` on its own. See clusterStartWaves().
128
+ */
96
129
  function detectConcurrentSessionPressure(events, threshold) {
97
- return events
98
- .filter((e) => e.event === 'orchestrator.session.started' && typeof e.peer_count === 'number' && e.peer_count >= threshold)
99
- .map((e) => ({
100
- signal: 'concurrent-session-pressure',
101
- host_class: e.host_class ?? null,
102
- timestamp: e.timestamp,
103
- raw: { peer_count: e.peer_count },
104
- }));
130
+ const qualifying = events.filter(
131
+ (e) =>
132
+ e.event === 'orchestrator.session.started' &&
133
+ typeof e.recent_starts_count === 'number' &&
134
+ e.recent_starts_count >= threshold,
135
+ );
136
+ return clusterStartWaves(qualifying).map((wave) => ({
137
+ signal: 'concurrent-session-pressure',
138
+ host_class: wave[0].host_class ?? null,
139
+ timestamp: wave[0].timestamp,
140
+ // Peak count of the wave: same `{ recent_starts_count }` sample shape as
141
+ // the per-event hits this replaces.
142
+ raw: { recent_starts_count: Math.max(...wave.map((e) => e.recent_starts_count)) },
143
+ }));
144
+ }
145
+
146
+ /**
147
+ * Cluster qualifying start events into waves, per `host_class` (events without
148
+ * a host share one `null` bucket; aggregateCandidates() drops those hits later).
149
+ * Chain rule: sorted by time, an event joins the current wave when it lies
150
+ * less than the window after the PREVIOUS qualifying event — the window being
151
+ * the span `recent_starts_count` itself looks back over (event field
152
+ * `recent_starts_window_min`, else FALLBACK_START_WAVE_WINDOW_MIN). Events with
153
+ * an unparseable timestamp cannot be placed in any wave and are DROPPED:
154
+ * counting each as its own wave would re-open the per-event over-count.
155
+ *
156
+ * Each wave's hit carries the wave's FIRST timestamp, so downstream
157
+ * `last_seen` means "start of the last wave", not "last qualifying start".
158
+ *
159
+ * Ceiling (BV-004): chaining is unbounded, so qualifying starts that keep
160
+ * arriving less than one window apart for an hour merge into ONE occurrence —
161
+ * a sustained-pressure stretch undercounts rather than overcounts. Revisit if
162
+ * the ledger shows a chained wave on one host spanning more than 30 min, which
163
+ * would mean sustained load, not a burst, and deserves its own count.
164
+ *
165
+ * @param {object[]} qualifying — start events already past the threshold
166
+ * @returns {object[][]} waves, each a non-empty array of events in time order
167
+ */
168
+ function clusterStartWaves(qualifying) {
169
+ const byHost = new Map();
170
+ for (const e of qualifying) {
171
+ const key = e.host_class ?? null;
172
+ if (!byHost.has(key)) byHost.set(key, []);
173
+ byHost.get(key).push({ e, ms: Date.parse(e.timestamp) });
174
+ }
175
+ const waves = [];
176
+ for (const list of byHost.values()) {
177
+ const timed = list.filter((x) => Number.isFinite(x.ms)).sort((a, b) => a.ms - b.ms);
178
+ let current = null;
179
+ let prevMs = 0;
180
+ for (const x of timed) {
181
+ const windowMin =
182
+ typeof x.e.recent_starts_window_min === 'number' && x.e.recent_starts_window_min > 0
183
+ ? x.e.recent_starts_window_min
184
+ : FALLBACK_START_WAVE_WINDOW_MIN;
185
+ if (current && x.ms - prevMs < windowMin * 60_000) {
186
+ current.push(x.e);
187
+ } else {
188
+ current = [x.e];
189
+ waves.push(current);
190
+ }
191
+ prevMs = x.ms;
192
+ }
193
+ }
194
+ return waves;
105
195
  }
106
196
 
107
197
  function detectDiskFull(events) {
@@ -210,7 +300,7 @@ export function detectHardwarePatterns({
210
300
  const hits = [
211
301
  ...detectOomKill(events),
212
302
  ...detectHeartbeatGap(sweepLogEntries, t.heartbeatGapMinutes),
213
- ...detectConcurrentSessionPressure(events, t.concurrentSessionsWarn),
303
+ ...detectConcurrentSessionPressure(events, t.startWaveMin),
214
304
  ...detectDiskFull(events),
215
305
  ...detectThermalThrottle(events, t.thermalCpuLoadPct),
216
306
  ];
@@ -5,13 +5,15 @@
5
5
  * orchestrator-layout
6
6
  *
7
7
  * Stdlib + intra-repo helpers only (node:fs, node:path, ./helpers.mjs,
8
- * ../../sessions-canonical.mjs).
8
+ * ../../platform.mjs, ../../sessions-canonical.mjs).
9
9
  */
10
10
 
11
11
  import { existsSync } from 'node:fs';
12
12
  import { join, relative, sep } from 'node:path';
13
13
 
14
14
  import { parseFrontmatter, safeRead, parseJsonl, pass, fail } from './helpers.mjs';
15
+ import { resolveEventsLedgerRoot } from '../../platform.mjs';
16
+ import { resolveLedgerPath } from '../../ledger-path.mjs';
15
17
  import { canonicalizeSessions } from '../../sessions-canonical.mjs';
16
18
 
17
19
  export function runCategory1(root) {
@@ -54,8 +56,12 @@ export function runCategory1(root) {
54
56
 
55
57
  // c1.2 sessions-jsonl-growth
56
58
  {
57
- const p = join(root, '.orchestrator/metrics/sessions.jsonl');
59
+ // #1592: the repo's ONE sessions ledger, as c4.2 reads it — a linked
60
+ // worktree holds no ledger of its own since #1584, so `<root>/…` would
61
+ // report "missing" for a repo that records every session.
62
+ const p = join(resolveEventsLedgerRoot(root), '.orchestrator/metrics/sessions.jsonl');
58
63
  const text = safeRead(p);
64
+ // ledger-root: display relPath for the finding, the read above is resolved
59
65
  const relPath = '.orchestrator/metrics/sessions.jsonl';
60
66
  if (!text) {
61
67
  checks.push(fail('sessions-jsonl-growth', 3, relPath,
@@ -87,8 +93,9 @@ export function runCategory1(root) {
87
93
 
88
94
  // c1.3 learnings-jsonl-nonempty
89
95
  {
90
- const p = join(root, '.orchestrator/metrics/learnings.jsonl');
96
+ const p = resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', root);
91
97
  const text = safeRead(p);
98
+ // ledger-root: display relPath for the finding, the read above is resolved
92
99
  const relPath = '.orchestrator/metrics/learnings.jsonl';
93
100
  if (!text) {
94
101
  checks.push(fail('learnings-jsonl-nonempty', 2, relPath,
@@ -115,9 +122,14 @@ export function runCategory1(root) {
115
122
  '.orchestrator',
116
123
  '.orchestrator/bootstrap.lock',
117
124
  '.orchestrator/policy',
125
+ // ledger-root: existence check only, resolved under resolveEventsLedgerRoot() below
118
126
  '.orchestrator/metrics',
119
127
  ];
120
- const presentPaths = requiredPaths.filter((rp) => existsSync(join(root, rp)));
128
+ // #1722: a linked worktree of a repo that ignores its ledgers has no
129
+ // metrics dir of its own — they live in the main checkout.
130
+ const presentPaths = requiredPaths.filter((rp) =>
131
+ existsSync(rp === '.orchestrator/metrics' ? join(resolveEventsLedgerRoot(root), rp) : join(root, rp)),
132
+ );
121
133
  if (presentPaths.length === requiredPaths.length) {
122
134
  checks.push(pass('orchestrator-layout', 2, 2, '.orchestrator/',
123
135
  { presentPaths },
@@ -5,7 +5,7 @@
5
5
  * vault-sync-validator, orphaned-session-lock
6
6
  *
7
7
  * Stdlib + intra-repo helpers only (node:fs, node:path, ./helpers.mjs,
8
- * ../../session-schema/filters.mjs, ../../sessions-canonical.mjs,
8
+ * ../../platform.mjs, ../../sessions-canonical.mjs,
9
9
  * ../../config/section-extractor.mjs).
10
10
  */
11
11
 
@@ -13,8 +13,9 @@ import { existsSync } from 'node:fs';
13
13
  import { join, relative, sep } from 'node:path';
14
14
 
15
15
  import { parseFrontmatter, safeRead, parseJsonl, pass, fail } from './helpers.mjs';
16
- import { isRealSession } from '../../session-schema/filters.mjs';
17
- import { canonicalizeSessions } from '../../sessions-canonical.mjs';
16
+ import { resolveEventsLedgerRoot } from '../../platform.mjs';
17
+ import { resolveLedgerPath } from '../../ledger-path.mjs';
18
+ import { canonicalizeSessions, isMeasuredSession } from '../../sessions-canonical.mjs';
18
19
  import { findSessionConfigBlock } from '../../config/section-extractor.mjs';
19
20
 
20
21
  // Default session-lock TTL in hours — mirrors DEFAULT_TTL_HOURS in
@@ -86,8 +87,9 @@ export function runCategory4(root) {
86
87
 
87
88
  // c4.2 sessions-jsonl-recent
88
89
  {
89
- const p = join(root, '.orchestrator/metrics/sessions.jsonl');
90
+ const p = join(resolveEventsLedgerRoot(root), '.orchestrator/metrics/sessions.jsonl');
90
91
  const text = safeRead(p);
92
+ // ledger-root: display relPath for the finding, the read above is resolved
91
93
  const relPath = '.orchestrator/metrics/sessions.jsonl';
92
94
  if (!text) {
93
95
  checks.push(fail('sessions-jsonl-recent', 3, relPath,
@@ -100,13 +102,13 @@ export function runCategory4(root) {
100
102
  { latestCompletedAt: null, ageInDays: null },
101
103
  'sessions.jsonl is empty'));
102
104
  } else {
103
- // Most-recent REAL session by completed_at, over the CANONICAL record
105
+ // Most-recent MEASURED session by completed_at, over the CANONICAL record
104
106
  // set (#1209b) — canonicalizeSessions() already collapses duplicate
105
107
  // `session_id` lines to the newest occurrence, drops the systemic
106
108
  // double-stub twin, and removes any record a surviving one
107
109
  // `supersedes`. A raw backward-scan-to-last-real-LINE (the pre-#1209b
108
110
  // approach) cannot see this: a duplicate id whose LAST occurrence was
109
- // retroactively corrected to `status: 'abandoned'` still let an
111
+ // retroactively corrected to a synthetic stub still let an
110
112
  // EARLIER, now-superseded raw line for the same identity count as
111
113
  // "recent". Max, not backward-scan, because canonical order is
112
114
  // first-appearance order per id, not completed_at order.
@@ -123,7 +125,7 @@ export function runCategory4(root) {
123
125
  let lastObj = null;
124
126
  let lastMs = -Infinity;
125
127
  for (const rec of records) {
126
- if (!isRealSession(rec)) continue;
128
+ if (!isMeasuredSession(rec)) continue;
127
129
  const ms = Date.parse(rec?.completed_at);
128
130
  if (Number.isFinite(ms) && ms > lastMs) {
129
131
  lastMs = ms;
@@ -154,8 +156,9 @@ export function runCategory4(root) {
154
156
 
155
157
  // c4.3 learnings-prunable
156
158
  {
157
- const p = join(root, '.orchestrator/metrics/learnings.jsonl');
159
+ const p = resolveLedgerPath('.orchestrator/metrics/learnings.jsonl', root);
158
160
  const text = safeRead(p);
161
+ // ledger-root: display relPath for the finding, the read above is resolved
159
162
  const relPath = '.orchestrator/metrics/learnings.jsonl';
160
163
  if (!text) {
161
164
  checks.push(fail('learnings-prunable', 2, relPath,
@@ -14,6 +14,8 @@ import { execFileSync } from 'node:child_process';
14
14
  import { safeRead, lineCount, pass, fail } from './helpers.mjs';
15
15
  import { resolveInstructionFile } from '../../common.mjs';
16
16
  import { listRemotes } from '../../vcs-repo-spec.mjs';
17
+ import { _extractConfigSection, _parseKV } from '../../config/section-extractor.mjs';
18
+ import { parseSourceCommitTrailer } from '../../github-export.mjs';
17
19
 
18
20
  export function runCategory6(root) {
19
21
  const checks = [];
@@ -231,14 +233,32 @@ export function runCategory6(root) {
231
233
  // git not honouring its own contract — equally unmeasurable, and
232
234
  // splitting it off would leave the identical fail-open standing behind a
233
235
  // smaller trigger. `aheadCountQuery` records which of the two occurred.
234
- const revListOut = runGit(['rev-list', '--count', `github/${mirrorBranch}..HEAD`]);
236
+ // `mirror: curated` (#1631): the published commit is a snapshot, not an
237
+ // ancestor of HEAD, so `github/<branch>..HEAD` would count the whole private
238
+ // history. Count from the snapshot's `Source-Commit` trailer instead; a
239
+ // mirror that carries no trailer yet is still a plain ancestor (pre-export).
240
+ const instrText = instr ? safeRead(instrPath) : null;
241
+ const curated = instrText !== null && _parseKV(_extractConfigSection(instrText)).get('mirror') === 'curated';
242
+ let aheadBase = `github/${mirrorBranch}`;
243
+ let revListOut;
244
+ if (curated) {
245
+ const mirrorCommit = runGit(['cat-file', 'commit', `github/${mirrorBranch}`]);
246
+ aheadBase = (mirrorCommit !== null && parseSourceCommitTrailer(mirrorCommit)) || aheadBase;
247
+ revListOut = mirrorCommit === null ? null : runGit(['rev-list', '--count', `${aheadBase}..HEAD`]);
248
+ } else {
249
+ revListOut = runGit(['rev-list', '--count', `${aheadBase}..HEAD`]);
250
+ }
235
251
  const aheadCount = revListOut !== null ? Number.parseInt(revListOut, 10) : NaN;
252
+ const curatedEvidence = curated ? { mirrorMode: 'curated', aheadBase } : {};
253
+ const pushHint = curated
254
+ ? 'Run /close on main — session-end § 4.4 publishes the curated snapshot with its checks (never push the export by hand)'
255
+ : 'Run: git push github HEAD';
236
256
 
237
257
  if (Number.isNaN(aheadCount)) {
238
258
  const aheadCountQuery = revListOut === null ? 'git-error' : 'unparseable-output';
239
259
  checks.push(fail({
240
260
  checkId, maxPoints: 2, path: relPath,
241
- evidence: { hasGithubRemote: true, mirrorBranch, resolvedVia, aheadCount: null, aheadCountQuery },
261
+ evidence: { hasGithubRemote: true, mirrorBranch, resolvedVia, aheadCount: null, aheadCountQuery, ...curatedEvidence },
242
262
  message:
243
263
  `unable to determine ahead-count vs github/${mirrorBranch} (${aheadCountQuery}) — ` +
244
264
  'mirror drift not measurable',
@@ -246,14 +266,14 @@ export function runCategory6(root) {
246
266
  } else if (aheadCount === 0) {
247
267
  checks.push(pass({
248
268
  checkId, points: 2, maxPoints: 2, path: relPath,
249
- evidence: { hasGithubRemote: true, mirrorBranch, resolvedVia, aheadCount },
269
+ evidence: { hasGithubRemote: true, mirrorBranch, resolvedVia, aheadCount, ...curatedEvidence },
250
270
  message: `HEAD is fully mirrored to github/${mirrorBranch}`,
251
271
  }));
252
272
  } else {
253
273
  checks.push(pass({
254
274
  checkId, points: 1, maxPoints: 2, path: relPath,
255
- evidence: { hasGithubRemote: true, mirrorBranch, resolvedVia, aheadCount },
256
- message: `${aheadCount} local commit(s) not pushed to github mirror (github/${mirrorBranch}). Run: git push github HEAD`,
275
+ evidence: { hasGithubRemote: true, mirrorBranch, resolvedVia, aheadCount, ...curatedEvidence },
276
+ message: `${aheadCount} local commit(s) not pushed to github mirror (github/${mirrorBranch}). ${pushHint}`,
257
277
  }));
258
278
  }
259
279
  }
@@ -21,7 +21,8 @@
21
21
  * status field is only 'pass' / 'fail' — partial tiers emit a reduced-points
22
22
  * pass() (there is no 'partial' constructor in helpers.mjs).
23
23
  *
24
- * Stdlib only: node:fs, node:path, node:child_process. The advisory tally in
24
+ * Stdlib (node:fs, node:path, node:child_process) plus the auditor's own
25
+ * ../../platform.mjs for the sessions-ledger root. The advisory tally in
25
26
  * c9.3 imports the AUDITOR's OWN (trusted) join/score modules in a short-lived
26
27
  * worker subprocess and runs them over the TARGET repo's telemetry DATA files —
27
28
  * it never imports code rooted at the audited repo (review N2b: RCE-on-audit).
@@ -34,6 +35,8 @@ import { join } from 'node:path';
34
35
  import { spawnSync } from 'node:child_process';
35
36
 
36
37
  import { pass, fail } from './helpers.mjs';
38
+ import { resolveEventsLedgerRoot } from '../../platform.mjs';
39
+ import { resolveLedgerPath } from '../../ledger-path.mjs';
37
40
 
38
41
  const TELEMETRY_REL = '.orchestrator/metrics/skill-invocations.jsonl';
39
42
  const SESSIONS_REL = '.orchestrator/metrics/sessions.jsonl';
@@ -80,7 +83,7 @@ export function runCategory9(root) {
80
83
  // points (with a quoted malformed-line count) when some lines are malformed.
81
84
  {
82
85
  const checkId = 'skill-telemetry-hygiene';
83
- const telemetryPath = join(root, TELEMETRY_REL);
86
+ const telemetryPath = resolveLedgerPath(TELEMETRY_REL, root);
84
87
  if (!existsSync(telemetryPath)) {
85
88
  checks.push(pass({
86
89
  checkId, points: 4, maxPoints: 4, path: TELEMETRY_REL,
@@ -186,7 +189,7 @@ function buildAdvisoryCheck(root) {
186
189
 
187
190
  const scorerPath = join(root, SCORER_REL);
188
191
  const joinPath = join(root, JOIN_REL);
189
- const telemetryPath = join(root, TELEMETRY_REL);
192
+ const telemetryPath = resolveLedgerPath(TELEMETRY_REL, root);
190
193
 
191
194
  // No module(s) or no telemetry → advisory surface has nothing to say, but
192
195
  // that is a healthy "insufficient signal" state. Pass full points.
@@ -250,7 +253,10 @@ function buildAdvisoryCheck(root) {
250
253
  // for caller symmetry. The CODE imported here comes from the auditor's OWN modules
251
254
  // (derived from import.meta.url below), never from the audited target (review N2b).
252
255
  function computeAdvisoryTally(scorerPath, joinPath, telemetryPath, root) {
253
- const sessionsPath = join(root, SESSIONS_REL);
256
+ // #1592: the repo's ONE sessions ledger (main checkout of a linked worktree),
257
+ // matching join.mjs's own default and c4.2 — telemetry stays at `root`, its
258
+ // hook writer is not mapped.
259
+ const sessionsPath = join(resolveEventsLedgerRoot(root), SESSIONS_REL);
254
260
  // Import the AUDITOR's OWN trusted modules — derived from THIS file's location,
255
261
  // NEVER from the audited `root`. The target's join.mjs/score.mjs are existence-
256
262
  // checked by the caller but never imported (would execute target code → N2b).
@@ -216,7 +216,9 @@ const HOST_ALIASES_MAX = 16;
216
216
  * @param {number} mode
217
217
  */
218
218
  function _writeJsonAtomicSync(filePath, data, mode) {
219
- mkdirSync(path.dirname(filePath), { recursive: true });
219
+ // 0o700: the ledger lives in the private config dir, which must not come out
220
+ // 0777 under umask 000 (#1629).
221
+ mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
220
222
  const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
221
223
  try {
222
224
  // `flag: 'wx'` + `mode` on the CREATE, never a chmod after the rename.
@@ -486,10 +488,11 @@ async function _readJsonSafe(p) {
486
488
  }
487
489
  }
488
490
 
489
- async function _writeJsonAtomic(filePath, data, { mode } = {}) {
490
- await mkdir(path.dirname(filePath), { recursive: true });
491
+ async function _writeJsonAtomic(filePath, data, { mode, dirMode } = {}) {
492
+ await mkdir(path.dirname(filePath), dirMode === undefined ? { recursive: true } : { recursive: true, mode: dirMode });
491
493
  const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
492
- await writeFile(tmp, JSON.stringify(data, null, 2) + '\n', 'utf8');
494
+ // mode at creation closes the 0666 window before the chmod below (#1632).
495
+ await writeFile(tmp, JSON.stringify(data, null, 2) + '\n', mode === undefined ? 'utf8' : { encoding: 'utf8', mode });
493
496
  const { rename } = await import('node:fs/promises');
494
497
  await rename(tmp, filePath);
495
498
  if (mode !== undefined) {
@@ -541,6 +544,7 @@ export async function getPrivateHostInfo(projectRoot) {
541
544
  return cached;
542
545
  }
543
546
  const fresh = collectPrivateInfo(projectRoot);
544
- await _writeJsonAtomic(f, fresh, { mode: 0o600 });
547
+ // dirMode only here: the public twin goes to the repo's .orchestrator/ (#1629).
548
+ await _writeJsonAtomic(f, fresh, { mode: 0o600, dirMode: 0o700 });
545
549
  return fresh;
546
550
  }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Buffered HTTP response subset for hook networking (#1612). Stdlib requests
3
+ * avoid Undici async-handle teardown crashes on Windows at process.exit().
4
+ * Internal configured endpoints only; no redirects are followed (SEC-014).
5
+ */
6
+ import http from 'node:http';
7
+ import https from 'node:https';
8
+
9
+ export async function httpRequest(url, {
10
+ method = 'GET', headers = {}, body, timeoutMs = 3000, maxBytes = 1024 * 1024,
11
+ } = {}) {
12
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new Error('invalid maxBytes');
13
+ const target = new URL(url);
14
+ if (target.protocol !== 'http:' && target.protocol !== 'https:') {
15
+ throw new Error('HTTP request requires http: or https:');
16
+ }
17
+ return new Promise((resolve, reject) => {
18
+ const transport = target.protocol === 'https:' ? https : http;
19
+ const req = transport.request(target, { method, headers, agent: false }, res => {
20
+ const chunks = [];
21
+ let bytes = 0;
22
+ let overflow = false;
23
+ res.on('data', chunk => {
24
+ if (overflow) return;
25
+ bytes += chunk.length;
26
+ if (bytes > maxBytes) {
27
+ overflow = true;
28
+ chunks.length = 0;
29
+ const error = new Error(`HTTP response exceeded maxBytes (${maxBytes})`);
30
+ clearTimeout(timer);
31
+ reject(error);
32
+ res.destroy();
33
+ req.destroy(error);
34
+ return;
35
+ }
36
+ chunks.push(chunk);
37
+ });
38
+ res.on('error', reject);
39
+ res.on('end', () => {
40
+ clearTimeout(timer);
41
+ if (overflow) return;
42
+ const text = Buffer.concat(chunks).toString('utf8');
43
+ const status = res.statusCode;
44
+ resolve({
45
+ ok: status >= 200 && status < 300,
46
+ status,
47
+ text: async () => text,
48
+ json: async () => JSON.parse(text),
49
+ });
50
+ });
51
+ });
52
+ // Wall-clock ceiling includes DNS, connection and body, unlike a socket
53
+ // inactivity timeout. Destroying closes the socket even for a stalled body.
54
+ const timer = setTimeout(() => {
55
+ req.destroy(new Error('HTTP request timed out'));
56
+ }, timeoutMs);
57
+ req.on('error', error => {
58
+ clearTimeout(timer);
59
+ reject(error);
60
+ });
61
+ req.on('close', () => clearTimeout(timer));
62
+ req.end(body);
63
+ });
64
+ }
@@ -3,10 +3,12 @@
3
3
  *
4
4
  * Lightweight directive-budget guard for always-on `.claude/rules/*.md`.
5
5
  *
6
- * Sums the always-on directive count across the rule files that the
7
- * rule-loader classifies as always-on (no `globs:` frontmatter, and no
8
- * `paths:` alias either — issue #795: `paths:`-scoped rules are NOT
9
- * always-on and must not inflate this count) and warns when the total
6
+ * Sums the always-on directive count across the rule files that Claude Code
7
+ * loads always-on: every rule WITHOUT a `paths:` frontmatter key (#1638).
8
+ * Claude Code reads `paths:` only and ignores `globs:` without an error, so a
9
+ * `globs:`-only rule is always-on there and counts here, even though the
10
+ * rule-loader's own wave injection scopes it. Measured through
11
+ * `loadApplicableRules({ scopeKey: 'paths' })`. It warns when the total
10
12
  * exceeds a ceiling. "Mechanism over discipline" — the #668 instruction-
11
13
  * budget audit recommends this as a silent-now growth ratchet that only
12
14
  * fires when NEW always-on directives are added.
@@ -66,11 +68,13 @@
66
68
  * - SISTER GUARD / KNOWN DIVERGENCE (#906.3): the projects-baseline repo
67
69
  * (resolved via `plan-baseline-path` / owner.yaml `baseline-path`) ships
68
70
  * `scripts/check-instruction-budget.sh` under rule CCU-009c. It measures the
69
- * SAME `.claude/rules/*.md` corpus and its total is NOT comparable to ours —
70
- * it diverges on BOTH axes, in OPPOSITE directions:
71
+ * SAME `.claude/rules/*.md` corpus and its total is NOT comparable to ours.
72
+ * As of 2026-07-30 it diverged on BOTH axes, in OPPOSITE directions:
71
73
  * (a) MEMBERSHIP — it classifies a file as path-scoped only on a `paths:`
72
- * frontmatter key, so this repo's `globs:`-scoped rules stay inside ITS
73
- * always-on set: 26 files where `loadApplicableRules` yields 12 here.
74
+ * frontmatter key, so this repo's then `globs:`-scoped rules stayed
75
+ * inside ITS always-on set: 26 files where we counted 12. That was
76
+ * Claude Code's semantics and ours was wrong; since #1638 this guard
77
+ * also scopes on `paths:` only, so the membership axis is aligned.
74
78
  * (b) HEURISTIC — it counts only rule-ID anchors and imperative-keyword
75
79
  * BULLET lines outside code fences, where `countDirectives` below
76
80
  * counts every bullet, ordered-list item and `##`-or-deeper heading.
@@ -957,15 +961,16 @@ export function listMachineGeneratedRules(opts = {}) {
957
961
  * ({@link isMachineGeneratedRule}). This is the population
958
962
  * {@link DEFAULT_GENERATED_BYTE_CEILING} is a ceiling FOR: the thing that
959
963
  * grows on its own, without anyone deciding to add a rule.
960
- * - `pathScoped` — every rule with `globs:` (or its `paths:` alias, #795),
961
- * i.e. the complement of the always-on set the three tier surfaces measure.
964
+ * - `pathScoped` — every rule with a `paths:` key (#1638: Claude Code's only
965
+ * scope key; a `globs:`-only rule is always-on there), i.e. the complement
966
+ * of the always-on set the three tier surfaces measure.
962
967
  * A real quantity, and the one this axis USED to report under the name
963
968
  * `generated` — see the ceiling's docblock for what that cost.
964
969
  *
965
970
  * The two overlap but neither contains the other by construction: a
966
971
  * hand-written rule can be path-scoped (3 of them are here), and a generated
967
972
  * rule could in principle carry a `host-class` activation axis instead of
968
- * `globs:`.
973
+ * `paths:`.
969
974
  *
970
975
  * Neither can reuse `loadApplicableRules`: that loader takes a `scopePaths`
971
976
  * list and returns the rules APPLICABLE to it, so with `scopePaths: []` it
@@ -1005,10 +1010,10 @@ function measureRuleCorpora(rulesDir) {
1005
1010
  } catch {
1006
1011
  continue; // unreadable file — skip, never throw
1007
1012
  }
1008
- let globs;
1013
+ let paths;
1009
1014
  let meta;
1010
1015
  try {
1011
- ({ globs, meta } = parseGlobsFrontmatter(content));
1016
+ ({ paths, meta } = parseGlobsFrontmatter(content));
1012
1017
  } catch {
1013
1018
  continue;
1014
1019
  }
@@ -1017,8 +1022,9 @@ function measureRuleCorpora(rulesDir) {
1017
1022
  generated.files += 1;
1018
1023
  generated.bytes += bytes;
1019
1024
  }
1020
- // `globs === null` → always-on, already counted by the tier surfaces.
1021
- if (globs !== null) {
1025
+ // `paths === null` → always-on for Claude Code, already counted by the
1026
+ // tier surfaces (which load with `scopeKey: 'paths'` — same split).
1027
+ if (paths !== null) {
1022
1028
  pathScoped.files += 1;
1023
1029
  pathScoped.bytes += bytes;
1024
1030
  }
@@ -1130,8 +1136,8 @@ function measureRuleCorpora(rulesDir) {
1130
1136
  * reconciliation PROVENANCE marker (`auto-generated: true` /
1131
1137
  * `learning-key` / `expires-at`) — the corpus
1132
1138
  * {@link DEFAULT_GENERATED_BYTE_CEILING} judges (#1297).
1133
- * bySurface.pathScoped === {bytes, files} of every rule with `globs:`
1134
- * (or its `paths:` alias) — the complement of the always-on set, and
1139
+ * bySurface.pathScoped === {bytes, files} of every rule with a `paths:`
1140
+ * key (#1638) — the complement of the always-on set, and
1135
1141
  * the number an operator reproduces from `ls .claude/rules/` (HR-106).
1136
1142
  * Judged against {@link DEFAULT_PATH_SCOPED_BYTE_CEILING} into
1137
1143
  * `overPathScopedBudget`, an `overBudget` term since #1316 (see that
@@ -1207,10 +1213,12 @@ export function computeInstructionBudget(opts = {}) {
1207
1213
  // — reused, not reimplemented. All three lists are loaded unconditionally
1208
1214
  // (not just the one matching `opts.context`) because `bySurface` reports
1209
1215
  // all three surfaces regardless of which `context` was requested for the
1210
- // PRIMARY totals (see doc above).
1211
- allEntries = loadApplicableRules({ rulesDir, scopePaths: [] });
1212
- waveEntries = loadApplicableRules({ rulesDir, scopePaths: [], context: 'wave' });
1213
- coordinatorEntries = loadApplicableRules({ rulesDir, scopePaths: [], context: 'coordinator' });
1216
+ // PRIMARY totals (see doc above). `scopeKey: 'paths'` (#1638): the budget
1217
+ // measures what Claude Code loads, and Claude Code scopes on `paths:` only.
1218
+ const scopeKey = 'paths';
1219
+ allEntries = loadApplicableRules({ rulesDir, scopePaths: [], scopeKey });
1220
+ waveEntries = loadApplicableRules({ rulesDir, scopePaths: [], context: 'wave', scopeKey });
1221
+ coordinatorEntries = loadApplicableRules({ rulesDir, scopePaths: [], context: 'coordinator', scopeKey });
1214
1222
  } catch {
1215
1223
  return empty;
1216
1224
  }
@@ -1242,7 +1250,7 @@ export function computeInstructionBudget(opts = {}) {
1242
1250
  // `pathScoped` keeps the old measurement under its honest name. Both are
1243
1251
  // disjoint from `totalBytes`, which counts always-on rules exclusively —
1244
1252
  // except that a generated rule activated by `host-class` rather than
1245
- // `globs:` would be always-on and therefore counted in both.
1253
+ // `paths:` would be always-on and therefore counted in both.
1246
1254
  ...measureRuleCorpora(rulesDir),
1247
1255
  };
1248
1256
 
@@ -888,13 +888,14 @@ export async function writeJsonAtomic(filePath, value, opts = {}) {
888
888
  * @param {object} [opts]
889
889
  * @param {number} [opts.indent=2] JSON.stringify indent.
890
890
  * @param {string} [opts.tmpPrefix='.tmp'] Tmp-file prefix (callers pick their domain prefix).
891
+ * @param {number} [opts.mode] File mode of the tmp file, kept by the rename (#1632).
891
892
  * @returns {{ ok: true } | { ok: false, reason: 'fs-error', error: string, code: string|null }}
892
893
  * `code` is the failing call's `err.code` (`'ENOSPC'`, `'EACCES'`, …), or `null`
893
894
  * when the error carried none (a serialization TypeError, the partial-adapter
894
895
  * refusal). Additive (#1032): `error` keeps its exact text.
895
896
  */
896
897
  export function writeJsonAtomicSync(filePath, data, opts = {}) {
897
- const { indent = 2, tmpPrefix = '.tmp' } = opts;
898
+ const { indent = 2, tmpPrefix = '.tmp', mode } = opts;
898
899
 
899
900
  let body;
900
901
  try {
@@ -903,7 +904,7 @@ export function writeJsonAtomicSync(filePath, data, opts = {}) {
903
904
  return _fsErrorEnvelope(err);
904
905
  }
905
906
 
906
- const res = atomicWriteWithBackup(filePath, body, { tmpPrefix });
907
+ const res = atomicWriteWithBackup(filePath, body, { tmpPrefix, mode });
907
908
  return res.ok ? { ok: true } : res;
908
909
  }
909
910
 
@@ -980,6 +981,7 @@ export function writeJsonAtomicSync(filePath, data, opts = {}) {
980
981
  * @param {BufferEncoding} [opts.encoding='utf8'] Encoding for a string `body`.
981
982
  * @param {boolean} [opts.backup=false] Snapshot the existing file to `.bak-<ISO>` first.
982
983
  * @param {string} [opts.tmpPrefix='.tmp'] Tmp-file prefix (callers pick their domain prefix).
984
+ * @param {number} [opts.mode] File mode of the tmp file, kept by the rename (#1632).
983
985
  * @param {Date} [opts.now] Clock seam for the backup stamp (tests).
984
986
  * @param {{ mkdirSync?: Function, writeFileSync?: Function, renameSync?: Function,
985
987
  * copyFileSync?: Function, existsSync?: Function, unlinkSync?: Function }} [opts.fs]
@@ -995,6 +997,7 @@ export function atomicWriteWithBackup(filePath, body, opts = {}) {
995
997
  encoding = 'utf8',
996
998
  backup = false,
997
999
  tmpPrefix = '.tmp',
1000
+ mode,
998
1001
  now = new Date(),
999
1002
  fs: injectedFs,
1000
1003
  } = opts;
@@ -1043,7 +1046,7 @@ export function atomicWriteWithBackup(filePath, body, opts = {}) {
1043
1046
  }
1044
1047
 
1045
1048
  tmpFile = _tmpPathFor(dir, tmpPrefix);
1046
- fsWriteFile(tmpFile, body, encoding);
1049
+ fsWriteFile(tmpFile, body, mode === undefined ? encoding : { encoding, mode });
1047
1050
  fsRename(tmpFile, filePath);
1048
1051
 
1049
1052
  return { ok: true, path: filePath, bytes: _byteLength(body, encoding), backupPath };