session-orchestrator 5.2.0 → 5.4.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 (427) hide show
  1. package/.agents/skills/architecture/SKILL.md +3 -1
  2. package/.agents/skills/autopilot/SKILL.md +5 -1
  3. package/.agents/skills/autopilot/agents/openai.yaml +5 -0
  4. package/.agents/skills/bootstrap/SKILL.md +5 -1
  5. package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
  6. package/.agents/skills/brainstorm/SKILL.md +5 -1
  7. package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
  8. package/.agents/skills/claude-md-drift-check/SKILL.md +4 -2
  9. package/.agents/skills/close/SKILL.md +5 -1
  10. package/.agents/skills/close/agents/openai.yaml +5 -0
  11. package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
  12. package/.agents/skills/debug/SKILL.md +5 -1
  13. package/.agents/skills/debug/agents/openai.yaml +5 -0
  14. package/.agents/skills/discovery/SKILL.md +5 -1
  15. package/.agents/skills/discovery/agents/openai.yaml +5 -0
  16. package/.agents/skills/dispatcher/SKILL.md +5 -1
  17. package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
  18. package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
  19. package/.agents/skills/ecosystem-health/SKILL.md +3 -1
  20. package/.agents/skills/eli5/SKILL.md +5 -1
  21. package/.agents/skills/eli5/agents/openai.yaml +5 -0
  22. package/.agents/skills/eval/SKILL.md +6 -2
  23. package/.agents/skills/eval/agents/openai.yaml +5 -0
  24. package/.agents/skills/evolve/SKILL.md +6 -2
  25. package/.agents/skills/evolve/agents/openai.yaml +5 -0
  26. package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
  27. package/.agents/skills/gitlab-ops/SKILL.md +3 -1
  28. package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
  29. package/.agents/skills/go/SKILL.md +5 -1
  30. package/.agents/skills/go/agents/openai.yaml +5 -0
  31. package/.agents/skills/grill/SKILL.md +5 -1
  32. package/.agents/skills/grill/agents/openai.yaml +5 -0
  33. package/.agents/skills/harness-audit/SKILL.md +5 -1
  34. package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
  35. package/.agents/skills/hook-development/SKILL.md +3 -1
  36. package/.agents/skills/mcp-builder/SKILL.md +3 -1
  37. package/.agents/skills/memory-cleanup/SKILL.md +5 -1
  38. package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
  39. package/.agents/skills/mode-selector/SKILL.md +3 -1
  40. package/.agents/skills/navigator/SKILL.md +22 -0
  41. package/.agents/skills/navigator/agents/openai.yaml +5 -0
  42. package/.agents/skills/npm-publish/SKILL.md +4 -2
  43. package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
  44. package/.agents/skills/persona-panel/SKILL.md +5 -1
  45. package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
  46. package/.agents/skills/plan/SKILL.md +5 -1
  47. package/.agents/skills/plan/agents/openai.yaml +5 -0
  48. package/.agents/skills/playwright-driver/SKILL.md +3 -1
  49. package/.agents/skills/portfolio/SKILL.md +5 -1
  50. package/.agents/skills/portfolio/agents/openai.yaml +5 -0
  51. package/.agents/skills/quality-gates/SKILL.md +3 -1
  52. package/.agents/skills/reconcile/SKILL.md +5 -1
  53. package/.agents/skills/reconcile/agents/openai.yaml +5 -0
  54. package/.agents/skills/release/SKILL.md +5 -1
  55. package/.agents/skills/release/agents/openai.yaml +5 -0
  56. package/.agents/skills/remote-offload/SKILL.md +3 -1
  57. package/.agents/skills/repo-audit/SKILL.md +5 -1
  58. package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
  59. package/.agents/skills/session/SKILL.md +21 -0
  60. package/.agents/skills/session/agents/openai.yaml +5 -0
  61. package/.agents/skills/session-end/SKILL.md +3 -1
  62. package/.agents/skills/session-plan/SKILL.md +3 -1
  63. package/.agents/skills/session-start/SKILL.md +3 -1
  64. package/.agents/skills/spinout/SKILL.md +5 -1
  65. package/.agents/skills/spinout/agents/openai.yaml +5 -0
  66. package/.agents/skills/sunset-review/SKILL.md +5 -1
  67. package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
  68. package/.agents/skills/templates-ack/SKILL.md +21 -0
  69. package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
  70. package/.agents/skills/test/SKILL.md +5 -1
  71. package/.agents/skills/test/agents/openai.yaml +5 -0
  72. package/.agents/skills/test-audit/SKILL.md +21 -0
  73. package/.agents/skills/test-audit/agents/openai.yaml +5 -0
  74. package/.agents/skills/test-runner/SKILL.md +3 -1
  75. package/.agents/skills/tmux-layout/SKILL.md +3 -1
  76. package/.agents/skills/using-orchestrator/SKILL.md +3 -1
  77. package/.agents/skills/ux-grill/SKILL.md +5 -1
  78. package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
  79. package/.agents/skills/vault-mirror/SKILL.md +3 -1
  80. package/.agents/skills/vault-sync/SKILL.md +3 -1
  81. package/.agents/skills/wave-executor/SKILL.md +3 -1
  82. package/.agents/skills/write-executable-plan/SKILL.md +3 -1
  83. package/.claude-plugin/marketplace.json +1 -1
  84. package/.claude-plugin/plugin.json +1 -1
  85. package/.codex-plugin/plugin.json +4 -4
  86. package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
  87. package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
  88. package/.codex-plugin/skills/eval/SKILL.md +1 -1
  89. package/.codex-plugin/skills/evolve/SKILL.md +1 -1
  90. package/.codex-plugin/skills/navigator/SKILL.md +22 -0
  91. package/.codex-plugin/skills/{convergence-monitoring → navigator}/agents/openai.yaml +2 -2
  92. package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
  93. package/.codex-plugin/skills/session/SKILL.md +1 -1
  94. package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
  95. package/.codex-plugin/skills/{npm-publish → test-audit}/agents/openai.yaml +1 -1
  96. package/.cursor/commands/eval.md +1 -1
  97. package/.cursor/commands/navigator.md +15 -0
  98. package/.cursor/commands/session.md +1 -1
  99. package/.cursor/commands/test-audit.md +14 -0
  100. package/.cursor/rules/000-session-orchestrator.mdc +0 -2
  101. package/.cursor/rules/050-plan.mdc +1 -1
  102. package/.cursor/rules/060-evolve.mdc +27 -12
  103. package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
  104. package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
  105. package/.cursor/skills/eval/SKILL.md +1 -1
  106. package/.cursor/skills/navigator/SKILL.md +13 -0
  107. package/.cursor/skills/npm-publish/SKILL.md +1 -0
  108. package/.cursor/skills/test-audit/SKILL.md +12 -0
  109. package/.cursor-plugin/plugin.json +1 -1
  110. package/.orchestrator/policy/blocked-commands.json +12 -3
  111. package/AGENTS.md +9 -30
  112. package/CHANGELOG.md +206 -0
  113. package/README.md +13 -9
  114. package/SECURITY.md +12 -0
  115. package/agents/dialectic-deriver.md +13 -10
  116. package/agents/eval-judge.md +67 -45
  117. package/agents/skill-applied-judge.md +34 -19
  118. package/commands/session.md +7 -3
  119. package/docs/agent-authoring.md +2 -2
  120. package/docs/baseline.md +22 -7
  121. package/docs/ci-setup.md +57 -4
  122. package/docs/codex-setup.md +14 -2
  123. package/docs/components.md +12 -10
  124. package/docs/events-schema.md +65 -15
  125. package/docs/persona-panel.md +5 -4
  126. package/docs/rule-authoring.md +84 -17
  127. package/docs/scope-collision-guard.md +26 -4
  128. package/docs/session-config-reference.md +154 -12
  129. package/docs/session-config-template.md +41 -3
  130. package/docs/telemetry.md +2 -0
  131. package/hooks/_lib/hook-import-set.json +87 -6
  132. package/hooks/_lib/subagent-transcript.mjs +582 -31
  133. package/hooks/config-protection.mjs +11 -3
  134. package/hooks/cwd-change-restore.mjs +11 -3
  135. package/hooks/enforce-commands.mjs +70 -23
  136. package/hooks/enforce-scope.mjs +143 -33
  137. package/hooks/hooks-codex.json +1 -1
  138. package/hooks/hooks-cursor.json +5 -0
  139. package/hooks/hooks-pi.json +5 -0
  140. package/hooks/hooks.json +6 -1
  141. package/hooks/loop-guard.mjs +11 -3
  142. package/hooks/on-session-end.mjs +58 -23
  143. package/hooks/on-session-start.mjs +101 -11
  144. package/hooks/on-stop.mjs +170 -22
  145. package/hooks/operator-steer.mjs +11 -3
  146. package/hooks/post-bash-issue-budget-refund.mjs +18 -8
  147. package/hooks/post-bash-write-verify.mjs +3 -2
  148. package/hooks/post-edit-import-probe.mjs +17 -9
  149. package/hooks/post-edit-validate.mjs +13 -5
  150. package/hooks/post-subagent-discovery-validator.mjs +98 -13
  151. package/hooks/post-tool-batch-wave-signal.mjs +202 -38
  152. package/hooks/post-tool-failure-corrective-context.mjs +11 -5
  153. package/hooks/post-tooluse-frontend-slop.mjs +10 -4
  154. package/hooks/pre-auq-clarity.mjs +15 -2
  155. package/hooks/pre-bash-destructive-guard.mjs +80 -9
  156. package/hooks/pre-bash-issue-budget.mjs +16 -11
  157. package/hooks/pre-bash-memory-propose-audit.mjs +137 -56
  158. package/hooks/pre-bash-sessions-ledger-guard.mjs +419 -22
  159. package/hooks/pre-bash-staging-fence.mjs +335 -31
  160. package/hooks/pre-bash-templates-first.mjs +19 -14
  161. package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
  162. package/hooks/pre-task-scope-disjoint.mjs +792 -74
  163. package/hooks/subagent-telemetry.mjs +111 -38
  164. package/hooks/wave-scope-commit-guard.mjs +197 -100
  165. package/monitors/monitors.json +1 -1
  166. package/output-styles/wave-summary.md +1 -1
  167. package/package.json +3 -3
  168. package/pi/prompts/eval.md +1 -1
  169. package/pi/prompts/navigator.md +12 -0
  170. package/pi/prompts/session.md +1 -1
  171. package/pi/prompts/test-audit.md +12 -0
  172. package/rules/README.md +7 -2
  173. package/rules/_index.md +3 -3
  174. package/rules/always-on/bash-harness-pitfalls.md +7 -1
  175. package/rules/opt-in-domain/prompt-caching.md +6 -2
  176. package/rules/opt-in-stack/backend-data.md +10 -2
  177. package/rules/opt-in-stack/backend.md +10 -4
  178. package/rules/opt-in-stack/frontend.md +7 -2
  179. package/rules/opt-in-stack/security-web.md +10 -4
  180. package/rules/opt-in-stack/swift.md +6 -2
  181. package/scripts/apply-session-learnings.mjs +492 -0
  182. package/scripts/archive-closed-prds.mjs +2 -1
  183. package/scripts/autopilot.mjs +70 -53
  184. package/scripts/backfill-abandoned-sessions.mjs +118 -16
  185. package/scripts/baseline-archetypes.mjs +1 -1
  186. package/scripts/check-package-manager.mjs +62 -5
  187. package/scripts/check-sessions-integrity.mjs +300 -0
  188. package/scripts/cursor-install.mjs +1 -4
  189. package/scripts/dialectic-deriver.mjs +50 -13
  190. package/scripts/emit-session.mjs +371 -31
  191. package/scripts/eval-session.mjs +65 -3
  192. package/scripts/export-hw-learnings.mjs +22 -3
  193. package/scripts/fleet-checkin.mjs +147 -0
  194. package/scripts/generate-agents-skills.mjs +102 -29
  195. package/scripts/generate-cursor-adapter.mjs +61 -16
  196. package/scripts/lib/agent-frontmatter.mjs +3 -3
  197. package/scripts/lib/agent-status.mjs +2 -31
  198. package/scripts/lib/auq/clarity.mjs +10 -2
  199. package/scripts/lib/auq/parse.mjs +12 -31
  200. package/scripts/lib/auq/schema.mjs +56 -41
  201. package/scripts/lib/auto-dialectic.mjs +304 -15
  202. package/scripts/lib/auto-dream.mjs +7 -6
  203. package/scripts/lib/autopilot/flags.mjs +18 -2
  204. package/scripts/lib/autopilot/kill-switches.mjs +6 -3
  205. package/scripts/lib/autopilot/loop.mjs +19 -2
  206. package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
  207. package/scripts/lib/baseline-archetypes.mjs +41 -22
  208. package/scripts/lib/ci-status-banner.mjs +376 -16
  209. package/scripts/lib/command-blocker.mjs +275 -28
  210. package/scripts/lib/config/dialectic.mjs +12 -3
  211. package/scripts/lib/config/gate.mjs +74 -0
  212. package/scripts/lib/config/host-paths.mjs +79 -0
  213. package/scripts/lib/config/persona-gate-wave.mjs +3 -3
  214. package/scripts/lib/config/reaper.mjs +163 -0
  215. package/scripts/lib/config-schema.mjs +13 -0
  216. package/scripts/lib/config.mjs +85 -10
  217. package/scripts/lib/convergence-monitor.mjs +74 -11
  218. package/scripts/lib/ecosystem-health.mjs +11 -0
  219. package/scripts/lib/eval/engine.mjs +421 -53
  220. package/scripts/lib/eval/judge.mjs +463 -40
  221. package/scripts/lib/eval/schema.mjs +10 -1
  222. package/scripts/lib/events-rotation.mjs +221 -25
  223. package/scripts/lib/events-schema.mjs +114 -0
  224. package/scripts/lib/events.mjs +545 -7
  225. package/scripts/lib/fleet-protocol.mjs +270 -0
  226. package/scripts/lib/frontmatter-guard.mjs +21 -10
  227. package/scripts/lib/gates/gate-baseline.mjs +40 -5
  228. package/scripts/lib/gates/gate-full.mjs +35 -5
  229. package/scripts/lib/gates/gate-helpers.mjs +336 -23
  230. package/scripts/lib/gates/gate-incremental.mjs +49 -23
  231. package/scripts/lib/gates/gate-per-file.mjs +42 -8
  232. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
  233. package/scripts/lib/instruction-budget-guard.mjs +208 -8
  234. package/scripts/lib/io.mjs +42 -8
  235. package/scripts/lib/issue-close-strip-labels.mjs +207 -49
  236. package/scripts/lib/js-mask.mjs +197 -0
  237. package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
  238. package/scripts/lib/learnings/expiry-sweep.mjs +34 -3
  239. package/scripts/lib/learnings/io.mjs +154 -26
  240. package/scripts/lib/learnings/schema.mjs +163 -15
  241. package/scripts/lib/learnings/sizing-subject.mjs +2 -2
  242. package/scripts/lib/maintenance-due-banner.mjs +53 -88
  243. package/scripts/lib/memory-proposals/schema.mjs +11 -5
  244. package/scripts/lib/named-baseline-resolver.mjs +53 -28
  245. package/scripts/lib/named-vault-resolver.mjs +104 -5
  246. package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
  247. package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
  248. package/scripts/lib/orphan-reaper/reaper-audit.mjs +291 -0
  249. package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
  250. package/scripts/lib/orphan-reaper/scan-throttle.mjs +92 -0
  251. package/scripts/lib/orphan-reaper/scan.mjs +506 -0
  252. package/scripts/lib/orphan-reaper.mjs +316 -0
  253. package/scripts/lib/owner-yaml.mjs +104 -6
  254. package/scripts/lib/peer-cards/merger.mjs +48 -10
  255. package/scripts/lib/peer-cards/reader.mjs +78 -2
  256. package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
  257. package/scripts/lib/process-group.mjs +950 -0
  258. package/scripts/lib/project-hygiene.mjs +21 -1
  259. package/scripts/lib/quality-gate.mjs +152 -33
  260. package/scripts/lib/reconcile/backlog.mjs +368 -0
  261. package/scripts/lib/reconcile/engine.mjs +55 -188
  262. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
  263. package/scripts/lib/reconcile/sanitize.mjs +69 -3
  264. package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
  265. package/scripts/lib/resource-probe/parsers.mjs +31 -0
  266. package/scripts/lib/resource-probe.mjs +1 -1
  267. package/scripts/lib/rule-loader.mjs +141 -74
  268. package/scripts/lib/rules-sync.mjs +27 -12
  269. package/scripts/lib/scope-baseline.mjs +3 -4
  270. package/scripts/lib/scope-echo.mjs +39 -2
  271. package/scripts/lib/scope-gate.mjs +605 -1
  272. package/scripts/lib/session-close-backfill.mjs +33 -6
  273. package/scripts/lib/session-id.mjs +9 -20
  274. package/scripts/lib/session-invocation.mjs +20 -0
  275. package/scripts/lib/session-record-repair.mjs +183 -26
  276. package/scripts/lib/session-registry.mjs +27 -2
  277. package/scripts/lib/session-schema/constants.mjs +56 -2
  278. package/scripts/lib/session-schema/normalizer.mjs +56 -4
  279. package/scripts/lib/session-schema/validator.mjs +28 -1
  280. package/scripts/lib/session-schema.mjs +8 -3
  281. package/scripts/lib/session-start-probes.mjs +95 -10
  282. package/scripts/lib/session-token-rollup.mjs +245 -52
  283. package/scripts/lib/sessions-canonical.mjs +118 -4
  284. package/scripts/lib/sessions-integrity-banner.mjs +7 -1
  285. package/scripts/lib/sessions-staleness-banner.mjs +193 -51
  286. package/scripts/lib/sh-quote.mjs +29 -0
  287. package/scripts/lib/skill-evidence-window.mjs +891 -0
  288. package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
  289. package/scripts/lib/skill-evolution/engine.mjs +18 -9
  290. package/scripts/lib/skill-judge.mjs +45 -3
  291. package/scripts/lib/subagents-schema.mjs +30 -3
  292. package/scripts/lib/tail-window.mjs +56 -0
  293. package/scripts/lib/telemetry/pricing.mjs +118 -71
  294. package/scripts/lib/telemetry/schema.mjs +30 -0
  295. package/scripts/lib/telemetry/sync.mjs +61 -6
  296. package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
  297. package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
  298. package/scripts/lib/tests-src-ratio.mjs +140 -23
  299. package/scripts/lib/tmux-layout/layouts.mjs +1 -11
  300. package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
  301. package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
  302. package/scripts/lib/user-invocable-skills.mjs +23 -3
  303. package/scripts/lib/ux-grill/reconcile.mjs +48 -22
  304. package/scripts/lib/validate/check-agents-skills.mjs +26 -15
  305. package/scripts/lib/validate/check-agents.mjs +1 -1
  306. package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
  307. package/scripts/lib/validate/check-entry-guard.mjs +13 -50
  308. package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
  309. package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
  310. package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
  311. package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
  312. package/scripts/lib/validate/check-rules.mjs +7 -5
  313. package/scripts/lib/validate/check-skill-links.mjs +9 -1
  314. package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
  315. package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
  316. package/scripts/lib/validate/check-test-value-bans.mjs +87 -14
  317. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  318. package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
  319. package/scripts/lib/validate/check-unwired-features.mjs +130 -27
  320. package/scripts/lib/validate/check-validator-registration.mjs +34 -10
  321. package/scripts/lib/validate/confidential-names.mjs +10 -0
  322. package/scripts/lib/validate-vendored-rules.mjs +210 -84
  323. package/scripts/lib/vault-mirror/namespace.mjs +46 -8
  324. package/scripts/lib/vault-mirror/process.mjs +10 -3
  325. package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
  326. package/scripts/lib/vault-mirror/telemetry.mjs +8 -1
  327. package/scripts/lib/vault-status/board-writer.mjs +48 -11
  328. package/scripts/lib/vault-status/narrative-mirror.mjs +71 -16
  329. package/scripts/lib/vault-yaml.mjs +118 -0
  330. package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
  331. package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
  332. package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
  333. package/scripts/lib/wave-resource-gate.mjs +34 -8
  334. package/scripts/lib/worktree/lifecycle.mjs +153 -1
  335. package/scripts/memory-propose.mjs +18 -5
  336. package/scripts/release-session-lock.mjs +306 -0
  337. package/scripts/release.mjs +40 -8
  338. package/scripts/repair-invalid-sessions.mjs +37 -4
  339. package/scripts/resolve-session-invocation.mjs +59 -0
  340. package/scripts/run-migrate-v2-cross-repo.mjs +158 -30
  341. package/scripts/run-quality-gate.mjs +357 -197
  342. package/scripts/site-numbers.mjs +163 -5
  343. package/scripts/sweep-expired-learnings.mjs +56 -5
  344. package/scripts/sweep-expired-rules.mjs +16 -4
  345. package/scripts/validate-plugin.mjs +19 -0
  346. package/scripts/validate-wave-scope.mjs +32 -105
  347. package/scripts/vault-integration-watcher.mjs +2 -1
  348. package/scripts/vault-mirror.mjs +81 -47
  349. package/scripts/wave-scope-binding.mjs +2 -1
  350. package/skills/_shared/config-reading.md +3 -3
  351. package/skills/_shared/fleet-protocol.md +131 -0
  352. package/skills/_shared/platform-tools.md +23 -11
  353. package/skills/_shared/state-ownership.md +5 -0
  354. package/skills/autopilot/SKILL.md +47 -18
  355. package/skills/bootstrap/SKILL.md +1 -1
  356. package/skills/bootstrap/_shared-template.md +1 -1
  357. package/skills/bootstrap/fast-template.md +1 -1
  358. package/skills/bootstrap/private-contract.md +2 -1
  359. package/skills/bootstrap/public-fallback.md +6 -3
  360. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
  361. package/skills/claude-md-drift-check/SKILL.md +10 -10
  362. package/skills/claude-md-drift-check/checker.mjs +26 -18
  363. package/skills/convergence-monitoring/README.md +8 -1
  364. package/skills/convergence-monitoring/SIGNALS.md +50 -6
  365. package/skills/convergence-monitoring/SKILL.md +15 -6
  366. package/skills/eval/SKILL.md +39 -24
  367. package/skills/eval/rubric-v1.md +1 -0
  368. package/skills/eval/rubric-v2.md +457 -0
  369. package/skills/evolve/SKILL.md +3 -3
  370. package/skills/evolve/references/evolve-analyze-mode.md +10 -6
  371. package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
  372. package/skills/gitlab-ops/SKILL.md +3 -2
  373. package/skills/memory-cleanup/SKILL.md +1 -1
  374. package/skills/navigator/SKILL.md +193 -0
  375. package/skills/navigator/references/account-check.sh +87 -0
  376. package/skills/navigator/references/fleet-decisions.mjs +231 -0
  377. package/skills/navigator/references/slot-check.sh +114 -0
  378. package/skills/npm-publish/SKILL.md +1 -1
  379. package/skills/persona-panel/SKILL.md +2 -2
  380. package/skills/persona-panel/persona-format.md +2 -2
  381. package/skills/persona-panel/presets/designer-lens.md +1 -1
  382. package/skills/persona-panel/presets/engineer-lens.md +1 -1
  383. package/skills/persona-panel/presets/pm-lens.md +1 -1
  384. package/skills/plan/mode-retro.md +19 -4
  385. package/skills/quality-gates/SKILL.md +6 -4
  386. package/skills/reconcile/SKILL.md +11 -0
  387. package/skills/remote-offload/SKILL.md +4 -2
  388. package/skills/session-end/SKILL.md +13 -16
  389. package/skills/session-end/discovery-scan.md +1 -1
  390. package/skills/session-end/learning-patterns.md +22 -12
  391. package/skills/session-end/metrics-collection.md +5 -6
  392. package/skills/session-end/phase-3-6-tail.md +55 -9
  393. package/skills/session-end/references/phase-3-documentation-updates.md +2 -2
  394. package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
  395. package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
  396. package/skills/session-end/references/session-summary-template.md +1 -1
  397. package/skills/session-end/session-metrics-write.md +32 -60
  398. package/skills/session-plan/SKILL.md +17 -8
  399. package/skills/session-plan/references/session-plan-task-classification.md +2 -2
  400. package/skills/session-start/SKILL.md +6 -0
  401. package/skills/session-start/references/operations-contract.md +4 -0
  402. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  403. package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
  404. package/skills/test-audit/SKILL.md +152 -0
  405. package/skills/test-audit/references/campaign.md +111 -0
  406. package/skills/test-audit/references/mutate.sh +50 -0
  407. package/skills/ux-grill/SKILL.md +1 -1
  408. package/skills/wave-executor/SKILL.md +9 -5
  409. package/skills/wave-executor/circuit-breaker.md +2 -0
  410. package/skills/wave-executor/references/wave-executor-state-init.md +8 -3
  411. package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
  412. package/skills/wave-executor/references/wave-loop-review.md +3 -5
  413. package/templates/personas/README.md +5 -5
  414. package/templates/personas/accounting-compliance.v1.md +1 -1
  415. package/templates/personas/accounting-tax-advisor.v1.md +1 -1
  416. package/templates/personas/buyer-p1-cto.v1.md +1 -1
  417. package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
  418. package/templates/personas/buyer-p3-build.v1.md +1 -1
  419. package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
  420. package/templates/personas/buyer-p5-solo.v1.md +1 -1
  421. package/templates/personas/buyer-p6-ld.v1.md +1 -1
  422. package/templates/personas/klima-ai-expert.v1.md +1 -1
  423. package/templates/personas/klima-physicist.v1.md +1 -1
  424. package/.cursor/commands/convergence-monitoring.md +0 -13
  425. package/.cursor/commands/npm-publish.md +0 -13
  426. package/pi/prompts/convergence-monitoring.md +0 -11
  427. package/pi/prompts/npm-publish.md +0 -11
@@ -45,6 +45,14 @@
45
45
  * - `surfaceTopN` (`./learnings/surface.mjs`) — the canonical active-learning
46
46
  * filter (confidence > floor, not expired); called with a large cap to get
47
47
  * the full active set rather than a top-N slice.
48
+ * - `countReconcileBacklog` (`./reconcile/backlog.mjs`) — the SAME eligibility +
49
+ * materialization partition `/reconcile` applies, imported from the LEAF module
50
+ * rather than from `./reconcile/engine.mjs`. The engine is WRITE-capable (it
51
+ * merges the candidate store) and drags the emitter, renderer and unicode
52
+ * validator behind it: importing the count from there took this probe's static
53
+ * closure from 11 modules / 151,946 B to 19 / 322,897 B (+112 %, measured
54
+ * 2026-09-18), inherited unasked by `maintenance-due-banner.mjs`. A banner must
55
+ * not be able to reach a writer.
48
56
  * - `filterEligible` (`./reconcile/eligibility.mjs`) — the SAME type/file_paths
49
57
  * allow-list gate the reconcile engine itself uses (deliberately NOT
50
58
  * confidence-gated — mirrors `runReconcile`'s own posture, see
@@ -68,7 +76,7 @@ import { join } from 'node:path';
68
76
 
69
77
  import { readLearnings } from './learnings/io.mjs';
70
78
  import { surfaceTopN } from './learnings/surface.mjs';
71
- import { filterEligible } from './reconcile/eligibility.mjs';
79
+ import { countReconcileBacklog } from './reconcile/backlog.mjs';
72
80
  import { loadCandidates } from './reconcile/idempotency.mjs';
73
81
  import { readConfigFile } from './config/io.mjs';
74
82
  import { _parseReconcile } from './config/reconcile.mjs';
@@ -82,9 +90,67 @@ export const NUDGE_MIN_LEARNINGS = 20;
82
90
  /** Nudge threshold (b): new learnings accrued since the last determinable reconcile run. */
83
91
  export const NUDGE_MIN_DELTA = 15;
84
92
 
85
- /** Nudge threshold (c): rule-eligible learnings (type + file_paths allow-list), regardless of confidence. */
93
+ /**
94
+ * Nudge threshold (c): reconcile BACKLOG — rule-eligible learnings (type + file_paths
95
+ * allow-list, not expired, regardless of confidence) MINUS those already materialized
96
+ * in the idempotency sidecar or a `.claude/rules/` provenance marker. Counting the
97
+ * materialized ones too made the probe un-greenable (#1380: 98 eligible, 35 already
98
+ * materialized, and no `/reconcile` run could ever clear it).
99
+ */
86
100
  export const NUDGE_MIN_ELIGIBLE = 3;
87
101
 
102
+ /**
103
+ * Resolve the `reconcile` Session Config settings this module needs, in ONE read.
104
+ *
105
+ * Both consumers go through here, so the nudge can never judge on a different
106
+ * population than `/reconcile` does: the skill reads `reconcile.min-insight-chars`
107
+ * (default 24) and forwards it to the engine, and so must this probe. Before #1380
108
+ * follow-up the value was only read AFTER the backlog had already been counted, which
109
+ * left the placeholder-insight gate practically OFF for the banner.
110
+ *
111
+ * Precedence: an injected already-parsed Session Config wins per KEY (DI seam, avoids a
112
+ * second CLAUDE.md read); any key it does not carry falls back to parsing
113
+ * CLAUDE.md/AGENTS.md; an unreadable config falls back to `_parseReconcile('')`, i.e.
114
+ * the parser's OWN documented defaults — never a literal repeated here.
115
+ *
116
+ * Never throws.
117
+ *
118
+ * @param {string} repoRoot
119
+ * @param {unknown} [config] already-parsed Session Config
120
+ * @returns {Promise<{enabled: boolean, minInsightChars: number}>}
121
+ */
122
+ async function _resolveReconcileSettings(repoRoot, config) {
123
+ const injected =
124
+ config && typeof config === 'object' && /** @type {any} */ (config).reconcile &&
125
+ typeof (/** @type {any} */ (config).reconcile) === 'object'
126
+ ? /** @type {Record<string, unknown>} */ (/** @type {any} */ (config).reconcile)
127
+ : null;
128
+
129
+ const hasEnabled = injected !== null && typeof injected.enabled === 'boolean';
130
+ const hasChars = injected !== null && Number.isFinite(injected['min-insight-chars']);
131
+ if (hasEnabled && hasChars) {
132
+ return {
133
+ enabled: /** @type {boolean} */ (injected.enabled),
134
+ minInsightChars: Number(injected['min-insight-chars']),
135
+ };
136
+ }
137
+
138
+ let parsed;
139
+ try {
140
+ parsed = _parseReconcile(await readConfigFile(repoRoot));
141
+ } catch {
142
+ // Config unreadable — take the parser's own defaults (single source of truth).
143
+ parsed = _parseReconcile('');
144
+ }
145
+
146
+ return {
147
+ enabled: hasEnabled ? /** @type {boolean} */ (injected.enabled) : parsed.enabled,
148
+ minInsightChars: hasChars
149
+ ? Number(injected['min-insight-chars'])
150
+ : parsed['min-insight-chars'],
151
+ };
152
+ }
153
+
88
154
  /**
89
155
  * Resolve the max `created_at` across reconcile-candidate sidecar records —
90
156
  * the most recent reconcile run's timestamp, or `null` when no run is on
@@ -110,11 +176,19 @@ function _lastRunAt(candidates) {
110
176
  *
111
177
  * @param {object} [opts]
112
178
  * @param {string} [opts.repoRoot] — project root (defaults to process.cwd()).
113
- * @param {Date|number} [opts.now] — injectable clock, forwarded to the active-learning filter.
179
+ * @param {Date|number} [opts.now] — injectable clock, forwarded to the active-learning
180
+ * filter AND the eligibility expiry gate.
181
+ * @param {number} [opts.minInsightChars] — forwarded to the eligibility filter. When
182
+ * OMITTED the value is read from Session Config (`reconcile.min-insight-chars`,
183
+ * default 24) rather than left inert, so the nudge and `/reconcile` count the SAME
184
+ * population — see `_resolveReconcileSettings`.
185
+ * @param {object} [opts.config] — optional already-parsed Session Config (DI seam).
114
186
  * @returns {Promise<{
115
187
  * totalLearnings: number,
116
188
  * activeLearnings: number,
117
189
  * eligibleCount: number,
190
+ * alreadyMaterialized: number,
191
+ * backlogCount: number,
118
192
  * lastRunAt: string|null,
119
193
  * lastRunCandidateCount: number,
120
194
  * delta: number,
@@ -131,6 +205,8 @@ export async function computeReconcileNudge(opts = {}) {
131
205
  totalLearnings: 0,
132
206
  activeLearnings: 0,
133
207
  eligibleCount: 0,
208
+ alreadyMaterialized: 0,
209
+ backlogCount: 0,
134
210
  lastRunAt: null,
135
211
  lastRunCandidateCount: 0,
136
212
  delta: 0,
@@ -166,20 +242,29 @@ export async function computeReconcileNudge(opts = {}) {
166
242
 
167
243
  if (active.length === 0) return empty;
168
244
 
169
- let eligibleCount;
170
- try {
171
- eligibleCount = filterEligible(entries).eligible.length;
172
- } catch {
173
- eligibleCount = 0;
174
- }
245
+ // Same partition a real /reconcile run applies (engine.mjs step 3/3a) — one
246
+ // truth, so the nudge can only fire on work /reconcile is able to clear.
247
+ // That includes the placeholder-insight gate: when the caller did not supply
248
+ // `minInsightChars`, read it from Session Config BEFORE counting, or the
249
+ // banner counts learnings `/reconcile` would reject.
250
+ const minInsightChars = Number.isFinite(opts.minInsightChars)
251
+ ? Number(opts.minInsightChars)
252
+ : (await _resolveReconcileSettings(repoRoot, opts.config)).minInsightChars;
175
253
 
254
+ // The candidate store is read ONCE, BEFORE the backlog count — the count needs
255
+ // the same records for its sidecar-dedupe half, and reading them here lets it
256
+ // be handed down instead of read a second time (`candidatesRead` says whether
257
+ // the hand-down is trustworthy; on a failed read we hand nothing down and let
258
+ // `countReconcileBacklog` decide for itself rather than pass a fake empty set).
176
259
  /** @type {Array<Record<string, unknown>>} */
177
260
  let candidates;
178
261
  /** @type {number|undefined} */
179
262
  let skippedCandidates;
263
+ let candidatesRead = false;
180
264
  try {
181
265
  const diag = loadCandidates({ repoRoot });
182
266
  candidates = Array.isArray(diag?.records) ? diag.records : [];
267
+ candidatesRead = Array.isArray(diag?.records);
183
268
  // Absence-preserving, mirroring `engine.mjs` `summary.skipped`: only a
184
269
  // finite count means "the store was inspected". Absent ⇒ never checked,
185
270
  // 0 ⇒ checked and clean.
@@ -190,6 +275,25 @@ export async function computeReconcileNudge(opts = {}) {
190
275
  // fabricating a clean 0.
191
276
  }
192
277
 
278
+ // ONE corpus, ONE population. `entries` (read above via `readLearnings`) is
279
+ // handed down instead of letting the count re-read and re-normalize the same
280
+ // file: the banner line names `activeLearnings` and `backlogCount` side by
281
+ // side, and two independent reads of one file are two populations wearing one
282
+ // sentence (HR-106). `repoRoot` stays required — the sidecar and the
283
+ // `.claude/rules/` provenance scan are resolved from it.
284
+ // Never throws (all-zero on failure).
285
+ const {
286
+ eligible: eligibleCount,
287
+ alreadyMaterialized,
288
+ backlog: backlogCount,
289
+ } = countReconcileBacklog({
290
+ repoRoot,
291
+ learnings: entries,
292
+ ...(candidatesRead ? { existingCandidates: candidates } : {}),
293
+ now: opts.now,
294
+ minInsightChars,
295
+ });
296
+
193
297
  const lastRunAt = _lastRunAt(candidates);
194
298
  const lastRunCandidateCount = Array.isArray(candidates) ? candidates.length : 0;
195
299
  const delta = entries.length - lastRunCandidateCount;
@@ -213,15 +317,18 @@ export async function computeReconcileNudge(opts = {}) {
213
317
  if (lastRunAt !== null && delta > NUDGE_MIN_DELTA) {
214
318
  reasons.push(`${delta} new learnings since the last reconcile run`);
215
319
  }
216
- // (c) — enough rule-eligible learnings to be worth a batch, independent of confidence.
217
- if (eligibleCount >= NUDGE_MIN_ELIGIBLE) {
218
- reasons.push(`${eligibleCount} rule-eligible learnings`);
320
+ // (c) — enough NOT-YET-MATERIALIZED rule-eligible learnings to be worth a
321
+ // batch, independent of confidence. HR-106: the reason names the number judged.
322
+ if (backlogCount >= NUDGE_MIN_ELIGIBLE) {
323
+ reasons.push(`${backlogCount} rule-eligible learnings awaiting a rule`);
219
324
  }
220
325
 
221
326
  const computed = {
222
327
  totalLearnings: entries.length,
223
328
  activeLearnings: active.length,
224
329
  eligibleCount,
330
+ alreadyMaterialized,
331
+ backlogCount,
225
332
  lastRunAt,
226
333
  lastRunCandidateCount,
227
334
  delta,
@@ -236,25 +343,6 @@ export async function computeReconcileNudge(opts = {}) {
236
343
  return computed;
237
344
  }
238
345
 
239
- /**
240
- * Best-effort extraction of `reconcile.enabled` from an already-parsed Session
241
- * Config object (DI/test seam — avoids re-reading CLAUDE.md when the caller
242
- * already has it). Returns `null` when unresolvable from `config` alone, in
243
- * which case `checkReconcileNudge` falls back to reading CLAUDE.md/AGENTS.md.
244
- *
245
- * @param {unknown} config
246
- * @returns {boolean|null}
247
- */
248
- function _reconcileEnabledFromConfig(config) {
249
- if (config && typeof config === 'object') {
250
- const rc = /** @type {Record<string, unknown>} */ (config).reconcile;
251
- if (rc && typeof rc === 'object' && typeof (/** @type {any} */ (rc).enabled) === 'boolean') {
252
- return /** @type {any} */ (rc).enabled;
253
- }
254
- }
255
- return null;
256
- }
257
-
258
346
  /**
259
347
  * Check reconcile-nudge readiness and produce a session-start banner.
260
348
  *
@@ -274,26 +362,30 @@ export async function checkReconcileNudge(opts = {}) {
274
362
  const { repoRoot, config, now } = opts;
275
363
  if (!repoRoot || typeof repoRoot !== 'string') return null;
276
364
 
365
+ // ONE config read for both settings, BEFORE the backlog is counted — the
366
+ // `min-insight-chars` gate must reach `countReconcileBacklog`, and reading the
367
+ // config afterwards (as this did before) left it inert.
368
+ let reconcileEnabled = false;
369
+ let minInsightChars;
370
+ try {
371
+ ({ enabled: reconcileEnabled, minInsightChars } = await _resolveReconcileSettings(
372
+ repoRoot,
373
+ config,
374
+ ));
375
+ } catch {
376
+ // Unreachable in practice (_resolveReconcileSettings never throws) — keep the
377
+ // conservative default and let computeReconcileNudge resolve the gate itself.
378
+ reconcileEnabled = false;
379
+ }
380
+
277
381
  let computed;
278
382
  try {
279
- computed = await computeReconcileNudge({ repoRoot, now });
383
+ computed = await computeReconcileNudge({ repoRoot, now, minInsightChars });
280
384
  } catch {
281
385
  return null;
282
386
  }
283
387
  if (!computed || computed.nudge !== true) return null;
284
388
 
285
- let reconcileEnabled = _reconcileEnabledFromConfig(config);
286
- if (reconcileEnabled === null) {
287
- try {
288
- const content = await readConfigFile(repoRoot);
289
- reconcileEnabled = _parseReconcile(content).enabled;
290
- } catch {
291
- // Config unreadable — fall back to the documented config default (false)
292
- // so the informational parenthetical still renders conservatively.
293
- reconcileEnabled = false;
294
- }
295
- }
296
-
297
389
  // Three-state last-run label. `never` is a claim about history and must only
298
390
  // be made when the store was inspected and held nothing: a store whose lines
299
391
  // were quarantined by the shape guard is EVIDENCE OF A RUN that can no longer
@@ -323,7 +415,8 @@ export async function checkReconcileNudge(opts = {}) {
323
415
  }
324
416
 
325
417
  const lines = [
326
- `⚠ reconcile-nudge: ${computed.activeLearnings} active learnings, ${computed.eligibleCount} rule-eligible, ` +
418
+ `⚠ reconcile-nudge: ${computed.activeLearnings} active learnings, ${computed.backlogCount} rule-eligible awaiting a rule ` +
419
+ `(${computed.alreadyMaterialized} already materialized), ` +
327
420
  `last reconcile run: ${lastRunLabel} — run /reconcile to convert learnings into rules.`,
328
421
  ];
329
422
  if (reconcileEnabled === false) {
@@ -30,6 +30,37 @@ export function parseEtimeToMinutes(etime) {
30
30
  return days * 24 * 60 + hours * 60 + mins;
31
31
  }
32
32
 
33
+ /**
34
+ * Parse a `ps` etime field (`[[DD-]HH:]MM:SS`) into whole SECONDS.
35
+ *
36
+ * Twin of {@link parseEtimeToMinutes}, which drops the seconds field because
37
+ * minute resolution is all the zombie counter needs. The orphan reaper
38
+ * (`scripts/lib/orphan-reaper.mjs`) cannot use that: its minimum age is 300
39
+ * seconds and its identity check reconstructs a start time as
40
+ * `now - etimeSeconds * 1000`, where a discarded seconds field would be a
41
+ * systematic error of up to 59 s against a 2 s tolerance.
42
+ *
43
+ * Deliberately a sibling rather than a refactor of the twin: `parseEtimeToMinutes`
44
+ * has live callers (`countZombieProcesses`) whose behaviour must not shift.
45
+ *
46
+ * Returns null when the format is unrecognised. Pure function.
47
+ * @param {string} etime
48
+ * @returns {number|null}
49
+ */
50
+ export function parseEtimeToSeconds(etime) {
51
+ if (typeof etime !== 'string') return null;
52
+ const s = etime.trim();
53
+ // Same shape as parseEtimeToMinutes: optional "DD-", optional "HH:", then "MM:SS".
54
+ const m = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(s);
55
+ if (!m) return null;
56
+ const days = parseInt(m[1] ?? '0', 10);
57
+ const hours = parseInt(m[2] ?? '0', 10);
58
+ const mins = parseInt(m[3], 10);
59
+ const secs = parseInt(m[4], 10);
60
+ if ([days, hours, mins, secs].some(Number.isNaN)) return null;
61
+ return days * 86400 + hours * 3600 + mins * 60 + secs;
62
+ }
63
+
33
64
  /**
34
65
  * Parse the detailed ps output and count Claude/Node zombie candidates.
35
66
  * A zombie candidate is a process matching "claude" or "node" that:
@@ -49,7 +49,7 @@ import { ramSnapshot, cpuSnapshot, processCounts, swapUsedMb, memoryPressurePctF
49
49
 
50
50
  export { evaluate, PROCESSES_PER_SESSION, PRESSURE_HARD_PCT, PRESSURE_SOFT_PCT, PRESSURE_HEALTHY_PCT, DEFAULT_RESOURCE_THRESHOLDS } from './resource-probe/evaluate.mjs';
51
51
  export { peerSessionsCount } from './resource-probe/probe-platform.mjs';
52
- export { parseEtimeToMinutes, countZombieProcesses, countProcessMatches, parseSwapUsageOutput, parseMemoryPressureOutput, parseVmStatAvailableGb } from './resource-probe/parsers.mjs';
52
+ export { parseEtimeToMinutes, parseEtimeToSeconds, countZombieProcesses, countProcessMatches, parseSwapUsageOutput, parseMemoryPressureOutput, parseVmStatAvailableGb } from './resource-probe/parsers.mjs';
53
53
 
54
54
  // ---------------------------------------------------------------------------
55
55
  // Public API — probe()
@@ -11,24 +11,26 @@
11
11
  * when at least one `scopePath` matches at least one glob pattern.
12
12
  *
13
13
  * `paths:` is a same-shape alias for `globs:` (issue #795) — some repos use a
14
- * `paths:` frontmatter convention instead of `globs:` (e.g. projects-baseline:
15
- * 26 rule files, all `paths:`, 0 `globs:`). Before #795 these repos'
14
+ * `paths:` frontmatter convention instead of `globs:` (projects-baseline did
15
+ * when #795 landed; it now carries both keys — measured count:
16
+ * docs/baseline.md). Before #795 these repos'
16
17
  * path-scoped rules were silently misclassified as always-on, inflating the
17
18
  * `instruction-budget-guard.mjs` (#687) always-on count with a false positive.
18
19
  * `paths:` supports the identical inline-array and block-list forms as
19
20
  * `globs:`. Precedence when BOTH keys are present on the same rule: `globs:`
20
21
  * wins (silently — no merge, no warning) and `paths:` is ignored entirely.
21
22
  *
22
- * A rule file MAY carry a leading single-line provenance header before its
23
- * frontmatter block — the vendoring pipeline (`scripts/rules-sync.mjs`)
24
- * requires a first-line `<!-- source: session-orchestrator plugin
25
- * (canonical: ...) -->` comment on every vendored rule. The frontmatter
26
- * parser tolerates any leading run of blank lines and/or single-line HTML
27
- * comments before the opening `---`, so a vendored rule keeps its `globs:`
28
- * scoping and scalar meta instead of silently falling back to always-on.
29
- * Only single-line comments are tolerated (no multi-line comment blocks);
30
- * files with no header, or starting directly with `---`, parse exactly as
31
- * before.
23
+ * Frontmatter counts ONLY when its opening `---` is the very first line of the
24
+ * file (#1449) — the same rule Claude Code applies when it reads `paths:`.
25
+ * Until #1449 this parser skipped leading blank lines and single-line HTML
26
+ * comments (the vendoring pipeline's `<!-- source: … -->` provenance header),
27
+ * so a rule Claude Code loads ALWAYS-ON was classified path-scoped here and
28
+ * `instruction-budget-guard.mjs` under-counted the always-on surface. A
29
+ * provenance comment now belongs directly AFTER the closing `---` (body text,
30
+ * inert). A frontmatter-shaped block that is NOT on line 1 — any text,
31
+ * comment or blank line before it — yields no globs and no meta at all, and
32
+ * the entry carries `defect: 'frontmatter-not-at-top'` so callers can report
33
+ * the file instead of silently loading it always-on.
32
34
  *
33
35
  * Beyond the `globs:` key (issue #336), the frontmatter parser also captures
34
36
  * these scalar activation keys (issue #694), surfaced on each RuleEntry:
@@ -149,34 +151,15 @@ function matchGlob(filePath, globPattern) {
149
151
  // Minimal YAML frontmatter parser (matches state-md.mjs style)
150
152
  // ---------------------------------------------------------------------------
151
153
 
154
+ // Anchored at offset 0: frontmatter exists only when `---` is line 1 (#1449).
152
155
  const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
153
156
 
154
- // A single header line is either blank or a complete single-line HTML
155
- // comment (`<!-- ... -->` on one line). Multi-line comment blocks are
156
- // deliberately NOT supported — the vendoring convention is a single first
157
- // line, and tolerating a few stacked comment lines (each matching this RE)
158
- // is sufficient.
159
- const HEADER_LINE_RE = /^[ \t]*(?:<!--.*-->)?[ \t]*$/;
160
-
161
157
  /**
162
- * Skips a leading run of blank lines and/or single-line HTML comments
163
- * (`<!-- ... -->`) at the very start of a rule file's contents, so the
164
- * frontmatter opener (`---`) can be found even when the vendoring pipeline's
165
- * mandatory provenance header precedes it. Stops at the first line that is
166
- * neither blank nor a complete single-line comment.
167
- *
168
- * @param {string} contents - raw file contents
169
- * @returns {string} contents starting at the first non-header line (empty
170
- * string when the entire content is header lines)
158
+ * Defect code for a frontmatter block that does not start on line 1 (#1449).
159
+ * Claude Code ignores such a block, so the rule loads always-on there; the
160
+ * loader mirrors that and tags the entry with this code.
171
161
  */
172
- function stripLeadingProvenanceHeader(contents) {
173
- const lines = contents.split(/\r?\n/);
174
- let idx = 0;
175
- while (idx < lines.length && HEADER_LINE_RE.test(lines[idx])) {
176
- idx++;
177
- }
178
- return lines.slice(idx).join('\n');
179
- }
162
+ export const FRONTMATTER_NOT_AT_TOP = 'frontmatter-not-at-top';
180
163
 
181
164
  // Scalar activation keys captured (issue #694 + #692). The `globs` key keeps
182
165
  // its dedicated sequence/flow handling and is NOT routed through this table.
@@ -192,12 +175,36 @@ const SCALAR_META_KEYS = new Set([
192
175
  'tier',
193
176
  ]);
194
177
 
178
+ /**
179
+ * Undo the ONE quoting style the renderer emits for a scalar (#1041 follow-up).
180
+ *
181
+ * `scripts/lib/reconcile/renderer.mjs` serialises `description:` through
182
+ * js-yaml, which picks SINGLE-quote style whenever the plain form would be
183
+ * ambiguous (a `: ` inside) and escapes an interior `'` as `''`. A bare
184
+ * leading/trailing-quote strip then returns `it''s` for `it's` — measured
185
+ * 2026-08-23 on 40 of 146 live learning insights carrying both characters.
186
+ * Single-quoted: strip + un-double. Double-quoted: strip + unescape `\"` and
187
+ * `\\`. Unquoted: returned verbatim (the pre-#1041 behaviour, unchanged).
188
+ */
189
+ function unquoteYamlScalar(valuePart) {
190
+ const v = valuePart.trim();
191
+ if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
192
+ return v.slice(1, -1).replace(/''/g, "'");
193
+ }
194
+ if (v.length >= 2 && v.startsWith('"') && v.endsWith('"')) {
195
+ return v.slice(1, -1).replace(/\\(["\\])/g, '$1');
196
+ }
197
+ return v.replace(/^["']|["']$/g, '');
198
+ }
199
+
195
200
  /**
196
201
  * Parses the YAML frontmatter block for the `globs:` field (issue #336),
197
202
  * its `paths:` alias (issue #795), and the scalar activation keys (issue #694).
198
203
  *
199
204
  * Returns:
200
205
  * - `{ globs: string[] | null, meta: object }` on success
206
+ * - `{ globs: null, meta: {}, defect: 'frontmatter-not-at-top' }` when a
207
+ * frontmatter block exists but does not start on line 1 (#1449)
201
208
  * - throws `Error` on malformed frontmatter so the caller can fall back
202
209
  *
203
210
  * `globs` is `null` when no frontmatter, and neither `globs:` nor `paths:` is
@@ -212,41 +219,63 @@ const SCALAR_META_KEYS = new Set([
212
219
  * - all other recognised keys → quote-stripped strings
213
220
  * Unknown keys are ignored without error.
214
221
  *
215
- * A leading run of blank lines and/or single-line HTML comments (e.g. the
216
- * vendoring pipeline's provenance header) is tolerated before the opening
217
- * `---` — see `stripLeadingProvenanceHeader`. Files with no such header parse
218
- * identically to before this tolerance was added.
222
+ * Only a block whose opening `---` is line 1 is frontmatter (#1449), exactly
223
+ * as Claude Code reads it. Anything before the opener — a provenance comment,
224
+ * a blank line, any text — makes the block body text: no globs, no meta, and
225
+ * the `defect` field names the misplacement (see {@link FRONTMATTER_NOT_AT_TOP}).
219
226
  *
220
227
  * @param {string} contents - raw file contents
221
- * @returns {{ globs: string[] | null, meta: Record<string, unknown> }}
228
+ * @returns {{ globs: string[] | null, meta: Record<string, unknown>, defect?: 'frontmatter-not-at-top' }}
222
229
  */
230
+ export function parseGlobsFrontmatter(contents) {
231
+ const match = FRONTMATTER_RE.exec(contents);
232
+ if (match) return parseFrontmatterBlock(match[1]);
233
+ if (hasDisplacedFrontmatter(contents)) {
234
+ return { globs: null, meta: {}, defect: FRONTMATTER_NOT_AT_TOP };
235
+ }
236
+ return { globs: null, meta: {} };
237
+ }
238
+
223
239
  /**
224
- * Undo the ONE quoting style the renderer emits for a scalar (#1041 follow-up).
240
+ * Does `contents` carry a frontmatter block that is NOT on line 1 (#1449)?
225
241
  *
226
- * `scripts/lib/reconcile/renderer.mjs` serialises `description:` through
227
- * js-yaml, which picks SINGLE-quote style whenever the plain form would be
228
- * ambiguous (a `: ` inside) and escapes an interior `'` as `''`. A bare
229
- * leading/trailing-quote strip then returns `it''s` for `it's` — measured
230
- * 2026-08-23 on 40 of 146 live learning insights carrying both characters.
231
- * Single-quoted: strip + un-double. Double-quoted: strip + unescape `\"` and
232
- * `\\`. Unquoted: returned verbatim (the pre-#1041 behaviour, unchanged).
242
+ * Only the FIRST bare `---` line is considered, and its block must parse
243
+ * cleanly AND carry at least one key this loader honours (`globs`, `paths`,
244
+ * or a scalar activation key). That second condition keeps a horizontal rule
245
+ * pair around ordinary prose from being reported: prose between two `---`
246
+ * lines almost always holds a colon-less line (a heading, a sentence), which
247
+ * throws, and a recognised key at column 0 is what makes the misplacement
248
+ * change behaviour at all. The known blind spot is a displaced block that is
249
+ * itself malformed — it is treated as body text, which is what Claude Code
250
+ * does with it too.
251
+ *
252
+ * @param {string} contents - raw file contents
253
+ * @returns {boolean}
233
254
  */
234
- function unquoteYamlScalar(valuePart) {
235
- const v = valuePart.trim();
236
- if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
237
- return v.slice(1, -1).replace(/''/g, "'");
238
- }
239
- if (v.length >= 2 && v.startsWith('"') && v.endsWith('"')) {
240
- return v.slice(1, -1).replace(/\\(["\\])/g, '$1');
255
+ function hasDisplacedFrontmatter(contents) {
256
+ const lines = contents.split(/\r?\n/);
257
+ const opener = lines.indexOf('---');
258
+ // -1: no opener at all. 0: the opener IS line 1, so FRONTMATTER_RE already
259
+ // had its chance (an unclosed block) — not a displacement.
260
+ if (opener <= 0) return false;
261
+ const block = FRONTMATTER_RE.exec(lines.slice(opener).join('\n'));
262
+ if (!block) return false;
263
+ try {
264
+ const { globs, meta } = parseFrontmatterBlock(block[1]);
265
+ return globs !== null || Object.keys(meta).length > 0;
266
+ } catch {
267
+ return false;
241
268
  }
242
- return v.replace(/^["']|["']$/g, '');
243
269
  }
244
270
 
245
- export function parseGlobsFrontmatter(contents) {
246
- const match = FRONTMATTER_RE.exec(stripLeadingProvenanceHeader(contents));
247
- if (!match) return { globs: null, meta: {} };
248
-
249
- const fmText = match[1];
271
+ /**
272
+ * Parses the text BETWEEN the two `---` fences (the frontmatter body).
273
+ * Throws on a malformed top-level line — see {@link parseGlobsFrontmatter}.
274
+ *
275
+ * @param {string} fmText - frontmatter body without its fences
276
+ * @returns {{ globs: string[] | null, meta: Record<string, unknown> }}
277
+ */
278
+ function parseFrontmatterBlock(fmText) {
250
279
  const lines = fmText.split(/\r?\n/);
251
280
 
252
281
  let globsValue = null;
@@ -365,6 +394,35 @@ function metaToEntryFields(meta) {
365
394
  return out;
366
395
  }
367
396
 
397
+ /**
398
+ * The expiry predicate, shared by the injection gate below and the `/reconcile`
399
+ * provenance reader (`scripts/lib/reconcile/backlog.mjs`) — one `Date.parse`
400
+ * for both, so "expired" can never mean two different things to the two
401
+ * consumers (#1387).
402
+ *
403
+ * FAIL-OPEN by design: an absent, empty or unparseable `expires-at` is NOT
404
+ * expired. Fail-closed here would un-inject (and, in the reconcile reader,
405
+ * un-materialize) the whole corpus on a single typo.
406
+ *
407
+ * Pure apart from the optional `onUnparseable` callback, which exists so the
408
+ * injection path can keep its stderr WARN while the reconcile reader stays
409
+ * silent.
410
+ *
411
+ * @param {unknown} rawExpiry - the raw `expires-at` frontmatter value (or undefined)
412
+ * @param {number} [now] - epoch ms; defaults to `Date.now()`
413
+ * @param {(raw: unknown) => void} [onUnparseable] - called when `expires-at` is present but unparseable
414
+ * @returns {boolean} true only when a PARSEABLE `expires-at` lies before `now`
415
+ */
416
+ export function isRuleExpired(rawExpiry, now = Date.now(), onUnparseable) {
417
+ if (rawExpiry === undefined || rawExpiry === null) return false;
418
+ const ts = Date.parse(String(rawExpiry));
419
+ if (Number.isNaN(ts)) {
420
+ if (typeof onUnparseable === 'function') onUnparseable(rawExpiry);
421
+ return false;
422
+ }
423
+ return ts < now;
424
+ }
425
+
368
426
  /**
369
427
  * Deterministic gating check (issue #694 + #692). Returns `{ excluded: boolean,
370
428
  * reason?: string }`. Applied to BOTH always-on and glob-matched rules.
@@ -378,18 +436,17 @@ function metaToEntryFields(meta) {
378
436
  * @returns {{ excluded: boolean }}
379
437
  */
380
438
  function applyGates(meta, filePath, mode, hostClass, now, context = null) {
381
- // Expiry gate — fail-open on a malformed `expires-at`.
439
+ // Expiry gate — fail-open on a malformed `expires-at` ({@link isRuleExpired}).
382
440
  if (Object.prototype.hasOwnProperty.call(meta, 'expires-at')) {
383
441
  const rawExpiry = meta['expires-at'];
384
- const ts = Date.parse(String(rawExpiry));
385
- if (Number.isNaN(ts)) {
386
- process.stderr.write(
387
- `[rule-loader] Rule ${filePath} has unparseable expires-at ${JSON.stringify(rawExpiry)} — ignoring expiry\n`,
388
- );
389
- } else if (ts < now) {
390
- process.stderr.write(
391
- `[rule-loader] Rule ${filePath} expired at ${rawExpiry} — excluded\n`,
392
- );
442
+ if (
443
+ isRuleExpired(rawExpiry, now, (raw) =>
444
+ process.stderr.write(
445
+ `[rule-loader] Rule ${filePath} has unparseable expires-at ${JSON.stringify(raw)} — ignoring expiry\n`,
446
+ ),
447
+ )
448
+ ) {
449
+ process.stderr.write(`[rule-loader] Rule ${filePath} expired at ${rawExpiry} — excluded\n`);
393
450
  return { excluded: true };
394
451
  }
395
452
  }
@@ -437,6 +494,9 @@ function applyGates(meta, filePath, mode, hostClass, now, context = null) {
437
494
  * from the `alwaysApply` frontmatter key)
438
495
  * @property {string[]} matchedGlobs - globs that matched; empty when alwaysOn
439
496
  * @property {true} [_parseError] - present only on frontmatter parse-error entries
497
+ * @property {'frontmatter-not-at-top'} [defect] - present only when a frontmatter
498
+ * block exists but does not start on line 1 (#1449); such an entry is always
499
+ * `alwaysOn: true` and carries no meta fields, mirroring Claude Code
440
500
  * @property {string} [description] - frontmatter `description` (issue #694), when present
441
501
  * @property {string} [mode] - frontmatter `mode` (issue #694), when present
442
502
  * @property {string} [hostClass] - frontmatter `host-class` (issue #694), when present
@@ -455,6 +515,10 @@ function applyGates(meta, filePath, mode, hostClass, now, context = null) {
455
515
  * Rules without `globs:` (or its `paths:` alias — issue #795) frontmatter are
456
516
  * always included (alwaysOn: true). Rules with `globs:`/`paths:` are included
457
517
  * only when at least one scopePath matches at least one glob pattern.
518
+ * A rule whose frontmatter does not start on line 1 is always-on with no meta
519
+ * (so no gate excludes it) and carries `defect: 'frontmatter-not-at-top'`
520
+ * (#1449) — no stderr line, because callers such as the instruction-budget
521
+ * guard load the directory several times per run and report the defect themselves.
458
522
  *
459
523
  * After a successful frontmatter parse, deterministic gates (issue #694) are
460
524
  * applied to BOTH always-on and glob-matched rules — a rule must pass ALL
@@ -528,10 +592,11 @@ export function loadApplicableRules({
528
592
 
529
593
  let globs;
530
594
  let meta;
595
+ let defect;
531
596
  let parseError = false;
532
597
 
533
598
  try {
534
- ({ globs, meta } = parseGlobsFrontmatter(content));
599
+ ({ globs, meta, defect } = parseGlobsFrontmatter(content));
535
600
  } catch (err) {
536
601
  process.stderr.write(
537
602
  `[rule-loader] Frontmatter parse error in ${filePath}: ${err.message} — treating as always-on\n`,
@@ -552,7 +617,8 @@ export function loadApplicableRules({
552
617
  const metaFields = metaToEntryFields(meta);
553
618
 
554
619
  if (globs === null) {
555
- // No globs frontmatter (or parse error) → always-on
620
+ // No globs frontmatter (or parse error, or frontmatter not on line 1 —
621
+ // #1449) → always-on
556
622
  results.push({
557
623
  path: filePath,
558
624
  content,
@@ -560,6 +626,7 @@ export function loadApplicableRules({
560
626
  matchedGlobs: [],
561
627
  ...metaFields,
562
628
  ...(parseError ? { _parseError: true } : {}),
629
+ ...(defect ? { defect } : {}),
563
630
  });
564
631
  continue;
565
632
  }