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
@@ -0,0 +1,224 @@
1
+ /**
2
+ * verdict-ledger.mjs — append-only record of reconcile verdicts and runs (#1710).
3
+ *
4
+ * The idempotency sidecar (`.orchestrator/runtime/reconcile-candidates.jsonl`,
5
+ * `./idempotency.mjs`) is gitignored and resolved per checkout, so a fresh git
6
+ * worktree or another host starts without it: an operator's explicit
7
+ * `rejected` verdict was lost there and `/reconcile` re-proposed the declined
8
+ * rule, while the session-start nudge reported "no reconcile run on record".
9
+ *
10
+ * This ledger lives at `.orchestrator/metrics/reconcile-verdicts.jsonl` under
11
+ * `resolveEventsLedgerRoot(repoRoot)` — a linked worktree maps to its MAIN
12
+ * checkout — beside `learnings.jsonl`, so it shares learnings' tracking status
13
+ * in every repo (tracked + `merge=union` in the vault, host-local here). It is
14
+ * NEVER rewritten, only appended. Two record shapes, one JSON object per line:
15
+ * { kind: 'verdict', learning_key, outcome, processed_at, recorded_at }
16
+ * { kind: 'run', at, learnings } (`learnings`: count the run loaded; optional on older lines)
17
+ *
18
+ * Verdict lines carry DECISIONS only (see {@link BRANCH_SCOPED_OUTCOMES}): an
19
+ * outcome that describes one branch's tree (`written`, `already-on-disk`, or no
20
+ * outcome at all) already has a cross-tree witness in the tracked rule file's
21
+ * provenance marker, and mirroring it from a worktree whose branch is later
22
+ * abandoned would make the learning permanently non-re-proposable in main.
23
+ * Run recency (`lastRunAt`) comes from `run` lines only — a verdict is not a run.
24
+ *
25
+ * Writes happen ONLY for an explicit non-empty `repoRoot` — never a
26
+ * `process.cwd()` fallback, so a test without a root can never append to this
27
+ * repo's real ledger. Readers without a root return the empty result.
28
+ *
29
+ * Ceiling (BV-004): no lock. Concurrent worktree sessions append through
30
+ * O_APPEND with small lines (one `appendFileSync` per call), which keeps lines
31
+ * whole in practice; the sidecar stays the checkout-local work queue and this
32
+ * file only carries terminality and run recency. Revisit trigger: an
33
+ * interleaved or garbled ledger line observed (`skipped > 0` on a ledger no
34
+ * human edited).
35
+ */
36
+
37
+ import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
38
+ import { dirname, join } from 'node:path';
39
+
40
+ import { resolveEventsLedgerRoot } from '../platform.mjs';
41
+
42
+ /** Repo-relative location of the verdict ledger (resolved against the MAIN checkout). */
43
+ export const VERDICT_LEDGER_PATH = '.orchestrator/metrics/reconcile-verdicts.jsonl';
44
+
45
+ /**
46
+ * Terminal outcomes that describe ONE branch's tree, not a decision, and are
47
+ * therefore never mirrored into the ledger: `written` (writer.mjs persisted the
48
+ * rule file), `already-on-disk` (engine.mjs found its provenance marker) and the
49
+ * empty string (a record without `outcome` — pre-#1042, read as `written`). Every
50
+ * other outcome is a decision (today `rejected`, `expired-swept`).
51
+ */
52
+ export const BRANCH_SCOPED_OUTCOMES = Object.freeze(new Set(['', 'written', 'already-on-disk']));
53
+
54
+ /**
55
+ * @param {unknown} outcome - a sidecar record's `outcome` (absent/null ⇒ branch-scoped).
56
+ * @returns {boolean} true iff the outcome is a decision the ledger carries.
57
+ */
58
+ export function isDecisionOutcome(outcome) {
59
+ return typeof outcome === 'string' && !BRANCH_SCOPED_OUTCOMES.has(outcome);
60
+ }
61
+
62
+ /**
63
+ * @param {unknown} repoRoot
64
+ * @returns {string|null} absolute ledger path, or null without an explicit root
65
+ */
66
+ function ledgerPathFor(repoRoot) {
67
+ if (typeof repoRoot !== 'string' || repoRoot.length === 0) return null;
68
+ return join(resolveEventsLedgerRoot(repoRoot), VERDICT_LEDGER_PATH);
69
+ }
70
+
71
+ /**
72
+ * @typedef {Object} VerdictLedger
73
+ * @property {Map<string, string>} terminalKeys - learning_key → outcome of its first recorded verdict.
74
+ * @property {string|null} lastRunAt - max over run records' `at`, ISO (a verdict is not a run).
75
+ * @property {number|null} lastRunLearnings - `learnings` of the run record with the latest `at`;
76
+ * null when that record (or every run record) predates the field or carries no valid integer.
77
+ * @property {number} skipped - lines that were not a valid verdict/run record (counted, never silent).
78
+ * @property {boolean} present - the file was read.
79
+ * @property {boolean} readError - the file exists (or may) but could not be read (non-ENOENT).
80
+ */
81
+
82
+ /** @returns {VerdictLedger} */
83
+ function emptyLedger() {
84
+ return {
85
+ terminalKeys: new Map(),
86
+ lastRunAt: null,
87
+ lastRunLearnings: null,
88
+ skipped: 0,
89
+ present: false,
90
+ readError: false,
91
+ };
92
+ }
93
+
94
+ /** `<path>\0<skipped>` pairs already warned about in this process. */
95
+ const warnedSkipped = new Set();
96
+
97
+ /**
98
+ * Load the verdict ledger for `repoRoot`. A missing file (ENOENT) is the empty
99
+ * result, silently; any other read failure is the empty result plus ONE stderr
100
+ * WARN with `readError: true` (same split as `defaultLoadLearnings`). Blank lines
101
+ * are ignored; every other line that is not a valid verdict/run record is
102
+ * counted in `skipped`, and a non-zero count emits ONE stderr WARN naming the
103
+ * path and the count — once per path and count per process (no production
104
+ * caller reads `skipped`). Never throws.
105
+ *
106
+ * @param {string|undefined} repoRoot - explicit root; absent → empty result.
107
+ * @returns {VerdictLedger}
108
+ */
109
+ export function loadVerdictLedger(repoRoot) {
110
+ const result = emptyLedger();
111
+ const absPath = ledgerPathFor(repoRoot);
112
+ if (absPath === null) return result;
113
+
114
+ let raw;
115
+ try {
116
+ raw = readFileSync(absPath, 'utf8');
117
+ } catch (err) {
118
+ if (!err || err.code !== 'ENOENT') {
119
+ result.readError = true;
120
+ process.stderr.write(
121
+ `⚠ loadVerdictLedger: cannot read ${absPath} ` +
122
+ `(${err?.code ?? '?'}: ${err?.message ?? String(err)}) — treating as EMPTY\n`,
123
+ );
124
+ }
125
+ return result;
126
+ }
127
+ result.present = true;
128
+
129
+ let maxMs = -Infinity;
130
+ let latestRunMs = -Infinity;
131
+ for (const line of raw.split('\n')) {
132
+ const trimmed = line.trim();
133
+ if (trimmed.length === 0) continue;
134
+ let rec;
135
+ try {
136
+ rec = JSON.parse(trimmed);
137
+ } catch {
138
+ result.skipped += 1;
139
+ continue;
140
+ }
141
+ if (rec && rec.kind === 'verdict' && typeof rec.learning_key === 'string' && rec.learning_key.length > 0) {
142
+ if (!result.terminalKeys.has(rec.learning_key)) {
143
+ result.terminalKeys.set(rec.learning_key, typeof rec.outcome === 'string' ? rec.outcome : '');
144
+ }
145
+ } else if (rec && rec.kind === 'run' && typeof rec.at === 'string' && Number.isFinite(Date.parse(rec.at))) {
146
+ const t = Date.parse(rec.at);
147
+ if (t > maxMs) maxMs = t;
148
+ if (t >= latestRunMs) {
149
+ latestRunMs = t;
150
+ result.lastRunLearnings =
151
+ Number.isSafeInteger(rec.learnings) && rec.learnings >= 0 ? rec.learnings : null;
152
+ }
153
+ } else {
154
+ result.skipped += 1;
155
+ }
156
+ }
157
+ if (maxMs > -Infinity) result.lastRunAt = new Date(maxMs).toISOString();
158
+ // Once per path and count per process: one /reconcile reads the ledger once per
159
+ // stamp, and the same bad line must not print N+2 identical warnings.
160
+ const warnKey = `${absPath}\0${result.skipped}`;
161
+ if (result.skipped > 0 && !warnedSkipped.has(warnKey)) {
162
+ warnedSkipped.add(warnKey);
163
+ process.stderr.write(
164
+ `⚠ loadVerdictLedger: ${result.skipped} line(s) in ${absPath} are not a valid verdict/run record — ignored\n`,
165
+ );
166
+ }
167
+ return result;
168
+ }
169
+
170
+ /**
171
+ * Append all `records` as JSONL in ONE write (mkdir -p first). Never throws.
172
+ * @param {string|undefined} repoRoot
173
+ * @param {Array<Record<string, unknown>>} records
174
+ * @returns {{ ok: boolean, appended: number, error?: string }}
175
+ */
176
+ function appendRecords(repoRoot, records) {
177
+ const absPath = ledgerPathFor(repoRoot);
178
+ if (absPath === null) return { ok: false, appended: 0, error: 'no repoRoot' };
179
+ if (records.length === 0) return { ok: true, appended: 0 };
180
+ try {
181
+ mkdirSync(dirname(absPath), { recursive: true });
182
+ appendFileSync(absPath, records.map((r) => JSON.stringify(r)).join('\n') + '\n', 'utf8');
183
+ return { ok: true, appended: records.length };
184
+ } catch (err) {
185
+ return { ok: false, appended: 0, error: err && err.message ? err.message : String(err) };
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Append one verdict line per terminal candidate (`processed_at` set). The
191
+ * caller decides which records are new; this function does not dedupe.
192
+ *
193
+ * @param {string|undefined} repoRoot - explicit root; absent → nothing written.
194
+ * @param {Array<{ learning_key?: unknown, outcome?: unknown, processed_at?: unknown }>} candidates
195
+ * @param {string} [recordedAt] - ISO; defaults to now.
196
+ * @returns {{ ok: boolean, appended: number, error?: string }}
197
+ */
198
+ export function appendVerdicts(repoRoot, candidates, recordedAt) {
199
+ const at = typeof recordedAt === 'string' && recordedAt.length > 0 ? recordedAt : new Date().toISOString();
200
+ const records = (Array.isArray(candidates) ? candidates : [])
201
+ .filter((c) => c && typeof c.learning_key === 'string' && c.learning_key.length > 0 && typeof c.processed_at === 'string')
202
+ .map((c) => ({
203
+ kind: 'verdict',
204
+ learning_key: c.learning_key,
205
+ outcome: typeof c.outcome === 'string' ? c.outcome : '',
206
+ processed_at: c.processed_at,
207
+ recorded_at: at,
208
+ }));
209
+ return appendRecords(repoRoot, records);
210
+ }
211
+
212
+ /**
213
+ * Append one run record.
214
+ * @param {string|undefined} repoRoot - explicit root; absent → nothing written.
215
+ * @param {string} at - ISO timestamp of the run.
216
+ * @param {number} [learnings] - how many learnings the run loaded; omitted from the
217
+ * record unless a non-negative integer (the nudge's "new since last run" baseline).
218
+ * @returns {{ ok: boolean, appended: number, error?: string }}
219
+ */
220
+ export function appendRun(repoRoot, at, learnings) {
221
+ const rec = { kind: 'run', at };
222
+ if (Number.isSafeInteger(learnings) && learnings >= 0) rec.learnings = learnings;
223
+ return appendRecords(repoRoot, [rec]);
224
+ }
@@ -35,8 +35,18 @@
35
35
  * high-water mark across all runs to date), used for the "new learnings
36
36
  * since last run" delta heuristic.
37
37
  * There is no separate `reconcile`-scoped event in `.orchestrator/metrics/events.jsonl`
38
- * — the idempotency sidecar IS the durable run record, so this module reads
39
- * that directly rather than re-deriving a signal from the generic event log.
38
+ * — the idempotency sidecar is the durable checkout-local run record, so this
39
+ * module reads that directly rather than re-deriving a signal from the generic
40
+ * event log. Since #1710 a second, checkout-independent run record exists: the
41
+ * append-only verdict ledger (`./reconcile/verdict-ledger.mjs`, main checkout
42
+ * via `resolveEventsLedgerRoot`). Its `run` lines carry `at` and the number of
43
+ * learnings that run loaded; the ledger's run recency comes from those `run`
44
+ * lines ONLY (a verdict line is not a run — a ledger holding only hand stamps or
45
+ * `sweep-expired-rules --apply` verdicts reports no run, so rule (a) says so
46
+ * honestly instead of rule (b) counting every learning as new). `lastRunAt` is
47
+ * the later of both sources and the delta baseline is the larger of the sidecar
48
+ * row count and that learnings count, so a fresh worktree without the sidecar
49
+ * still sees the run.
40
50
  *
41
51
  * Reuses the existing learnings-corpus helpers rather than re-implementing
42
52
  * filter logic:
@@ -72,12 +82,13 @@
72
82
  * - Issue #723.
73
83
  */
74
84
 
75
- import { join } from 'node:path';
76
85
 
86
+ import { resolveLedgerPath } from './ledger-path.mjs';
77
87
  import { readLearnings } from './learnings/io.mjs';
78
88
  import { surfaceTopN } from './learnings/surface.mjs';
79
89
  import { countReconcileBacklog } from './reconcile/backlog.mjs';
80
90
  import { loadCandidates } from './reconcile/idempotency.mjs';
91
+ import { loadVerdictLedger } from './reconcile/verdict-ledger.mjs';
81
92
  import { readConfigFile } from './config/io.mjs';
82
93
  import { _parseReconcile } from './config/reconcile.mjs';
83
94
 
@@ -169,6 +180,17 @@ function _lastRunAt(candidates) {
169
180
  return Number.isFinite(maxMs) && maxMs > -Infinity ? new Date(maxMs).toISOString() : null;
170
181
  }
171
182
 
183
+ /**
184
+ * @param {string|null} a - ISO timestamp or null
185
+ * @param {string|null} b - ISO timestamp or null
186
+ * @returns {string|null} the later of the two, or null when both are null
187
+ */
188
+ function _laterIso(a, b) {
189
+ if (a === null) return b;
190
+ if (b === null) return a;
191
+ return Date.parse(b) > Date.parse(a) ? b : a;
192
+ }
193
+
172
194
  /**
173
195
  * Pure computation — always returns the full shape (never null, never throws).
174
196
  * Touches disk only via the injected/default learnings + candidates readers,
@@ -215,7 +237,7 @@ export async function computeReconcileNudge(opts = {}) {
215
237
  };
216
238
 
217
239
  const repoRoot = typeof opts.repoRoot === 'string' && opts.repoRoot.length > 0 ? opts.repoRoot : process.cwd();
218
- const learningsPath = join(repoRoot, LEARNINGS_PATH);
240
+ const learningsPath = resolveLedgerPath(LEARNINGS_PATH, repoRoot);
219
241
 
220
242
  /** @type {object[]} */
221
243
  let entries;
@@ -282,6 +304,9 @@ export async function computeReconcileNudge(opts = {}) {
282
304
  // sentence (HR-106). `repoRoot` stays required — the sidecar and the
283
305
  // `.claude/rules/` provenance scan are resolved from it.
284
306
  // Never throws (all-zero on failure).
307
+ // #1710 — the append-only verdict ledger (main checkout, outlives the
308
+ // checkout-local sidecar) is read ONCE and handed to the count as well.
309
+ const ledger = loadVerdictLedger(repoRoot);
285
310
  const {
286
311
  eligible: eligibleCount,
287
312
  alreadyMaterialized,
@@ -290,12 +315,21 @@ export async function computeReconcileNudge(opts = {}) {
290
315
  repoRoot,
291
316
  learnings: entries,
292
317
  ...(candidatesRead ? { existingCandidates: candidates } : {}),
318
+ ledgerKeys: ledger.terminalKeys,
293
319
  now: opts.now,
294
320
  minInsightChars,
295
321
  });
296
322
 
297
- const lastRunAt = _lastRunAt(candidates);
298
- const lastRunCandidateCount = Array.isArray(candidates) ? candidates.length : 0;
323
+ // The later of the sidecar's and the ledger's run evidence: a fresh worktree
324
+ // has no sidecar, yet the run on record in the ledger is still a run.
325
+ const lastRunAt = _laterIso(_lastRunAt(candidates), ledger.lastRunAt);
326
+ // The ledger's run record carries the learnings count that run saw, so a
327
+ // fresh worktree (no sidecar) does not report every learning as new. The max
328
+ // keeps the main checkout's delta unchanged — never larger than before.
329
+ const lastRunCandidateCount = Math.max(
330
+ Array.isArray(candidates) ? candidates.length : 0,
331
+ ledger.lastRunLearnings ?? 0,
332
+ );
299
333
  const delta = entries.length - lastRunCandidateCount;
300
334
  const quarantined = typeof skippedCandidates === 'number' ? skippedCandidates : 0;
301
335
 
@@ -26,8 +26,16 @@
26
26
  * number"), one noting "five consecutive sessions wrongly capped at 2".
27
27
  * 2. `concurrent-sessions-warn` is named for SESSIONS but was compared against
28
28
  * `claude_processes_count`. Measured ratio processes:sessions = 6.0. That
29
- * threshold fired on 93.6% of starts; against the real peer count it fires
30
- * on 4.2%.
29
+ * threshold fired on 93.6% of starts; against the real peer count it fired
30
+ * on 4.2% (2026-08-21). That rate did not hold: re-measured 2026-10-17 over
31
+ * starts since 2026-08-24, `peer_count >= 5` fires on 880/1305 = 67.4%
32
+ * (M4) and 255/991 = 25.7% (M5) — #1650. The session-start banner's WARN
33
+ * icon was re-aimed to a start wave (`hooks/on-session-start.mjs`,
34
+ * START_WAVE_*). #1654 finished the re-aim here: a state present on two
35
+ * thirds of M4 starts is a standing condition, and HR-104 forbids letting
36
+ * one count as a second opinion. concurrencySignal() below therefore still
37
+ * REPORTS the peer count (an `info:` reason) but no longer contributes a
38
+ * soft signal — neither the registry branch nor the process fallback.
31
39
  * 3. Any ONE soft signal capped agents-per-wave at 2 outright, so the union of
32
40
  * three noisy axes decided wave size on its noisiest member.
33
41
  *
@@ -51,6 +59,8 @@
51
59
  * when present. `claude_processes_count` survives only as a fallback, scaled
52
60
  * by the measured PROCESSES_PER_SESSION factor so the configured threshold
53
61
  * keeps meaning "sessions" on hosts where the registry is unavailable.
62
+ * Since #1654 the concurrency axis is REPORT-ONLY: it never enters
63
+ * `signals.soft`, so it can neither cap alone nor pair into a cap.
54
64
  * - TWO-SIGNAL RULE: a single soft signal is informational — reported in
55
65
  * `reasons`, capping nothing. A cap needs two INDEPENDENT soft signals
56
66
  * agreeing, or one hard signal. Simulated on the same 1477-sample corpus:
@@ -219,12 +229,24 @@ function memorySignal(snapshot, ramMin, ramCrit) {
219
229
  }
220
230
 
221
231
  /**
222
- * Resolve the concurrency signal, preferring the live session registry count
232
+ * Resolve the concurrency report, preferring the live session registry count
223
233
  * over the raw process count. Returns `null` when neither is available.
224
234
  *
235
+ * REPORT-ONLY since #1654: `soft` is always `false`. The peer count is a
236
+ * standing condition, not live load — `peer_count >= 5` held on 880/1305 =
237
+ * 67.4% of M4 and 255/991 = 25.7% of M5 starts since 2026-08-24 (#1650,
238
+ * re-measured 2026-10-17: 78/115 = 67.8% M4). HR-104: a usually-present signal
239
+ * reports and NEVER counts as a second opinion, so letting it pair with one
240
+ * real soft signal would restore the one-signal cap under a two-signal name
241
+ * (the same defect zombies had). The rare, decision-changing concurrency signal
242
+ * is the start wave in `hooks/on-session-start.mjs` (START_WAVE_*), which is
243
+ * not a snapshot field. The process-count fallback measures the same standing
244
+ * state and is report-only for the same reason.
245
+ *
225
246
  * @param {object} snapshot
226
247
  * @param {number} concWarn — configured threshold, denominated in SESSIONS
227
- * @returns {{soft: boolean, reason: string|null}|null}
248
+ * @returns {{soft: false, reason: string|null}|null} — `reason` set when the
249
+ * threshold is reached (caller reports it as `info:`); never a soft signal.
228
250
  */
229
251
  function concurrencySignal(snapshot, concWarn) {
230
252
  const { peer_sessions_count, claude_processes_count } = snapshot;
@@ -234,7 +256,7 @@ function concurrencySignal(snapshot, concWarn) {
234
256
  if (hasPeers) {
235
257
  if (peer_sessions_count >= concWarn) {
236
258
  return {
237
- soft: true,
259
+ soft: false,
238
260
  reason: `${peer_sessions_count} peer session(s) live in the registry (threshold: ${concWarn}) — consider sequencing this session after others finish.`,
239
261
  };
240
262
  }
@@ -252,7 +274,7 @@ function concurrencySignal(snapshot, concWarn) {
252
274
  const procThreshold = concWarn * PROCESSES_PER_SESSION;
253
275
  if (claude_processes_count >= procThreshold) {
254
276
  return {
255
- soft: true,
277
+ soft: false,
256
278
  reason: `${claude_processes_count} Claude processes running (fallback threshold ${procThreshold} = ${concWarn} sessions × ${PROCESSES_PER_SESSION} processes/session; the live session registry was unavailable).`,
257
279
  };
258
280
  }
@@ -339,10 +361,11 @@ export function evaluate(snapshot, thresholds, options = {}) {
339
361
  }
340
362
 
341
363
  // --- Concurrency axis: registry sessions, not raw process count -----------
364
+ // REPORT-ONLY (#1654): a standing condition on 67.4% of M4 starts must not
365
+ // become the second signal that caps a wave (HR-104, see concurrencySignal).
342
366
  const conc = concurrencySignal(snapshot, concWarn);
343
- if (conc && conc.soft) {
344
- softSignals.push('concurrency');
345
- reasons.push(conc.reason);
367
+ if (conc && conc.reason) {
368
+ reasons.push(`info: ${conc.reason} Not counted as a capacity signal (standing condition, #1654).`);
346
369
  }
347
370
 
348
371
  // --- Swap: only meaningful when the memory signal is NOT healthy ----------
@@ -306,7 +306,9 @@ export async function ramAvailableGb() {
306
306
  * #1089 the threshold was compared against `claude_processes_count` instead —
307
307
  * a measured 6x unit error (median processes:sessions = 6.0 over 1461 paired
308
308
  * samples), which is why a threshold of 5 fired on 93.6% of session starts
309
- * while the same threshold against the real count fires on 4.2%.
309
+ * while the same threshold against the real count fired on 4.2% at the time.
310
+ * Re-measured 2026-10-17 (#1650) it fires on 67.4% (M4) / 25.7% (M5) — a
311
+ * standing signal (HR-101), so the banner icon follows a start wave instead.
310
312
  *
311
313
  * Semantics mirror `detectPeers()` exactly: EXCLUDES the calling session and
312
314
  * counts only entries whose `last_heartbeat` is within `freshnessMin` minutes.
@@ -203,8 +203,8 @@ function unquoteYamlScalar(valuePart) {
203
203
  * its `paths:` alias (issue #795), and the scalar activation keys (issue #694).
204
204
  *
205
205
  * Returns:
206
- * - `{ globs: string[] | null, meta: object }` on success
207
- * - `{ globs: null, meta: {}, defect: 'frontmatter-not-at-top' }` when a
206
+ * - `{ globs: string[] | null, paths: string[] | null, meta: object }` on success
207
+ * - `{ globs: null, paths: null, meta: {}, defect: 'frontmatter-not-at-top' }` when a
208
208
  * frontmatter block exists but does not start on line 1 (#1449)
209
209
  * - throws `Error` on malformed frontmatter so the caller can fall back
210
210
  *
@@ -212,8 +212,11 @@ function unquoteYamlScalar(valuePart) {
212
212
  * present (always-on). `paths:` is parsed with the identical inline-array and
213
213
  * block-list forms as `globs:`; when BOTH keys are present on the same rule,
214
214
  * `globs:` wins silently and `paths:` is discarded (no merge, no warning —
215
- * see module doc). The returned shape is unchanged either way — callers never
216
- * see which key produced the value. `meta` carries only the recognised scalar
215
+ * see module doc). `globs` never tells callers which key produced the value;
216
+ * the additive `paths` field does carry the raw `paths:` list on its own
217
+ * (`null` when the key is absent), because Claude Code reads ONLY `paths:` and
218
+ * ignores `globs:` (#1638) — a caller asking "what does Claude Code scope?"
219
+ * needs that key alone, not the merged value. `meta` carries only the recognised scalar
217
220
  * keys that were present, with type coercion applied:
218
221
  * - `alwaysApply`, `auto-generated` → boolean ('true'/'false')
219
222
  * - `confidence` → number (undefined when NaN)
@@ -226,15 +229,15 @@ function unquoteYamlScalar(valuePart) {
226
229
  * the `defect` field names the misplacement (see {@link FRONTMATTER_NOT_AT_TOP}).
227
230
  *
228
231
  * @param {string} contents - raw file contents
229
- * @returns {{ globs: string[] | null, meta: Record<string, unknown>, defect?: 'frontmatter-not-at-top' }}
232
+ * @returns {{ globs: string[] | null, paths: string[] | null, meta: Record<string, unknown>, defect?: 'frontmatter-not-at-top' }}
230
233
  */
231
234
  export function parseGlobsFrontmatter(contents) {
232
235
  const match = FRONTMATTER_RE.exec(contents);
233
236
  if (match) return parseFrontmatterBlock(match[1]);
234
237
  if (hasDisplacedFrontmatter(contents)) {
235
- return { globs: null, meta: {}, defect: FRONTMATTER_NOT_AT_TOP };
238
+ return { globs: null, paths: null, meta: {}, defect: FRONTMATTER_NOT_AT_TOP };
236
239
  }
237
- return { globs: null, meta: {} };
240
+ return { globs: null, paths: null, meta: {} };
238
241
  }
239
242
 
240
243
  /**
@@ -274,7 +277,7 @@ function hasDisplacedFrontmatter(contents) {
274
277
  * Throws on a malformed top-level line — see {@link parseGlobsFrontmatter}.
275
278
  *
276
279
  * @param {string} fmText - frontmatter body without its fences
277
- * @returns {{ globs: string[] | null, meta: Record<string, unknown> }}
280
+ * @returns {{ globs: string[] | null, paths: string[] | null, meta: Record<string, unknown> }}
278
281
  */
279
282
  function parseFrontmatterBlock(fmText) {
280
283
  const lines = fmText.split(/\r?\n/);
@@ -370,7 +373,7 @@ function parseFrontmatterBlock(fmText) {
370
373
  // Precedence (issue #795): `globs:` wins silently when both keys are present.
371
374
  const globs = globsValue !== null ? globsValue : pathsValue;
372
375
 
373
- return { globs, meta };
376
+ return { globs, paths: pathsValue, meta };
374
377
  }
375
378
 
376
379
  /**
@@ -565,6 +568,10 @@ function applyGates(meta, filePath, mode, hostClass, now, context = null) {
565
568
  * @param {string|null} [opts.context] - 'wave' | 'coordinator' | null; when non-null, applies
566
569
  * tier gating: 'wave' excludes coordinator-only rules, 'coordinator' excludes wave-only rules.
567
570
  * null (default) disables tier gating entirely — backward-compatible with all existing callers.
571
+ * @param {'merged'|'paths'} [opts.scopeKey] - which frontmatter key scopes a rule. 'merged'
572
+ * (default) is this loader's own wave-injection semantics: `globs:`, else its `paths:` alias.
573
+ * 'paths' is Claude Code's native semantics (#1638): only `paths:` scopes, so a rule with
574
+ * `globs:` but no `paths:` is always-on — Claude Code ignores `globs:` without an error.
568
575
  * @returns {RuleEntry[]}
569
576
  */
570
577
  export function loadApplicableRules({
@@ -574,6 +581,7 @@ export function loadApplicableRules({
574
581
  hostClass = null,
575
582
  now = Date.now(),
576
583
  context = null,
584
+ scopeKey = 'merged',
577
585
  }) {
578
586
  /** @type {string[]} */
579
587
  let entries;
@@ -622,7 +630,9 @@ export function loadApplicableRules({
622
630
  let parseError = false;
623
631
 
624
632
  try {
625
- ({ globs, meta, defect } = parseGlobsFrontmatter(content));
633
+ const parsed = parseGlobsFrontmatter(content);
634
+ ({ meta, defect } = parsed);
635
+ globs = scopeKey === 'paths' ? parsed.paths : parsed.globs;
626
636
  } catch (err) {
627
637
  process.stderr.write(
628
638
  `[rule-loader] Frontmatter parse error in ${filePath}: ${err.message} — treating as always-on\n`,