session-orchestrator 3.22.0 → 3.24.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 (316) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/commands/autopilot-multi.md +14 -0
  5. package/.cursor/commands/autopilot.md +14 -0
  6. package/.cursor/commands/bootstrap.md +14 -0
  7. package/.cursor/commands/brainstorm.md +14 -0
  8. package/.cursor/commands/close.md +13 -0
  9. package/.cursor/commands/contract-version-bump.md +14 -0
  10. package/.cursor/commands/debug.md +14 -0
  11. package/.cursor/commands/discovery.md +14 -0
  12. package/.cursor/commands/dispatcher.md +14 -0
  13. package/.cursor/commands/eli5.md +14 -0
  14. package/.cursor/commands/eval.md +14 -0
  15. package/.cursor/commands/evolve.md +14 -0
  16. package/.cursor/commands/go.md +14 -0
  17. package/.cursor/commands/grill.md +14 -0
  18. package/.cursor/commands/harness-audit.md +13 -0
  19. package/.cursor/commands/journey-audit.md +14 -0
  20. package/.cursor/commands/memory-cleanup.md +14 -0
  21. package/.cursor/commands/persona-panel.md +14 -0
  22. package/.cursor/commands/plan.md +14 -0
  23. package/.cursor/commands/portfolio.md +14 -0
  24. package/.cursor/commands/reconcile.md +14 -0
  25. package/.cursor/commands/release.md +14 -0
  26. package/.cursor/commands/repo-audit.md +13 -0
  27. package/.cursor/commands/session.md +14 -0
  28. package/.cursor/commands/spinout.md +14 -0
  29. package/.cursor/commands/sunset-review.md +14 -0
  30. package/.cursor/commands/templates-ack.md +14 -0
  31. package/.cursor/commands/test.md +14 -0
  32. package/.cursor/hooks.json +60 -0
  33. package/.cursor/rules/000-session-orchestrator.mdc +8 -0
  34. package/.cursor/rules/010-session-workflow.mdc +9 -1
  35. package/.cursor/rules/020-quality-gates.mdc +1 -1
  36. package/.cursor/rules/030-wave-execution.mdc +1 -1
  37. package/.cursor/rules/050-plan.mdc +2 -2
  38. package/.cursor/rules/070-gitlab-ops.mdc +73 -57
  39. package/.cursor/rules/080-ecosystem-health.mdc +7 -7
  40. package/.cursor/skills/architecture/SKILL.md +13 -0
  41. package/.cursor/skills/autopilot/SKILL.md +12 -0
  42. package/.cursor/skills/bootstrap/SKILL.md +12 -0
  43. package/.cursor/skills/brainstorm/SKILL.md +13 -0
  44. package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
  45. package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
  46. package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
  47. package/.cursor/skills/daily/SKILL.md +12 -0
  48. package/.cursor/skills/debug/SKILL.md +13 -0
  49. package/.cursor/skills/discovery/SKILL.md +13 -0
  50. package/.cursor/skills/dispatcher/SKILL.md +13 -0
  51. package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
  52. package/.cursor/skills/domain-model/SKILL.md +13 -0
  53. package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
  54. package/.cursor/skills/eli5/SKILL.md +13 -0
  55. package/.cursor/skills/eval/SKILL.md +12 -0
  56. package/.cursor/skills/evolve/SKILL.md +13 -0
  57. package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
  58. package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
  59. package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
  60. package/.cursor/skills/grill/SKILL.md +13 -0
  61. package/.cursor/skills/hook-development/SKILL.md +13 -0
  62. package/.cursor/skills/journey-audit/SKILL.md +13 -0
  63. package/.cursor/skills/mcp-builder/SKILL.md +13 -0
  64. package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
  65. package/.cursor/skills/mode-selector/SKILL.md +13 -0
  66. package/.cursor/skills/npm-publish/SKILL.md +12 -0
  67. package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
  68. package/.cursor/skills/persona-panel/SKILL.md +12 -0
  69. package/.cursor/skills/plan/SKILL.md +13 -0
  70. package/.cursor/skills/playwright-driver/SKILL.md +13 -0
  71. package/.cursor/skills/quality-gates/SKILL.md +13 -0
  72. package/.cursor/skills/reconcile/SKILL.md +12 -0
  73. package/.cursor/skills/remote-offload/SKILL.md +13 -0
  74. package/.cursor/skills/repo-audit/SKILL.md +13 -0
  75. package/.cursor/skills/session-end/SKILL.md +13 -0
  76. package/.cursor/skills/session-plan/SKILL.md +13 -0
  77. package/.cursor/skills/session-start/SKILL.md +13 -0
  78. package/.cursor/skills/skill-creator/SKILL.md +13 -0
  79. package/.cursor/skills/spinout/SKILL.md +12 -0
  80. package/.cursor/skills/sunset-review/SKILL.md +13 -0
  81. package/.cursor/skills/test-runner/SKILL.md +13 -0
  82. package/.cursor/skills/tmux-layout/SKILL.md +13 -0
  83. package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
  84. package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
  85. package/.cursor/skills/vault-mirror/SKILL.md +13 -0
  86. package/.cursor/skills/vault-sync/SKILL.md +13 -0
  87. package/.cursor/skills/wave-executor/SKILL.md +13 -0
  88. package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
  89. package/.mcp.json +4 -1
  90. package/CHANGELOG.md +446 -0
  91. package/README.md +22 -17
  92. package/agents/AGENTS.md +23 -4
  93. package/agents/code-implementer.md +2 -1
  94. package/agents/db-specialist.md +2 -2
  95. package/agents/docs-writer.md +3 -1
  96. package/agents/eval-judge.md +1 -1
  97. package/agents/session-reviewer.md +7 -1
  98. package/agents/test-writer.md +2 -1
  99. package/agents/ui-developer.md +2 -1
  100. package/commands/bootstrap.md +2 -2
  101. package/commands/close.md +3 -1
  102. package/commands/go.md +1 -1
  103. package/commands/journey-audit.md +43 -0
  104. package/docs/USER-GUIDE.md +2 -2
  105. package/docs/ci-setup.md +194 -25
  106. package/docs/codex-setup.md +64 -0
  107. package/docs/components.md +7 -7
  108. package/docs/cursor-setup.md +26 -47
  109. package/docs/events-schema.md +120 -10
  110. package/docs/github-mirror-protection.md +197 -0
  111. package/docs/pi-setup.md +2 -0
  112. package/docs/rule-authoring.md +3 -1
  113. package/docs/scope-collision-guard.md +49 -2
  114. package/docs/session-config-reference.md +89 -9
  115. package/docs/session-config-template.md +38 -7
  116. package/docs/telemetry/telemetry-claims.md +11 -10
  117. package/docs/telemetry.md +52 -1
  118. package/hooks/_lib/atomic-json.mjs +111 -0
  119. package/hooks/_lib/lock-bootstrap.mjs +8 -4
  120. package/hooks/_lib/subagent-paths.mjs +143 -0
  121. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  122. package/hooks/cwd-change-restore.mjs +9 -29
  123. package/hooks/enforce-scope.mjs +93 -0
  124. package/hooks/hooks-codex.json +1 -1
  125. package/hooks/hooks-cursor.json +201 -20
  126. package/hooks/hooks-pi.json +1 -1
  127. package/hooks/hooks.json +2 -2
  128. package/hooks/on-session-end.mjs +486 -19
  129. package/hooks/on-session-start.mjs +263 -12
  130. package/hooks/on-stop.mjs +392 -24
  131. package/hooks/post-bash-write-verify.mjs +104 -4
  132. package/hooks/post-subagent-discovery-validator.mjs +182 -21
  133. package/hooks/post-tool-batch-wave-signal.mjs +165 -42
  134. package/hooks/post-tool-failure-corrective-context.mjs +9 -32
  135. package/hooks/pre-bash-issue-budget.mjs +117 -4
  136. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  138. package/hooks/pre-bash-staging-fence.mjs +4 -0
  139. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  140. package/hooks/skill-invocation-telemetry.mjs +21 -10
  141. package/hooks/subagent-telemetry.mjs +11 -26
  142. package/monitors/monitors.json +6 -0
  143. package/package.json +1 -1
  144. package/pi/prompts/journey-audit.md +12 -0
  145. package/rules/_index.md +9 -1
  146. package/rules/always-on/ask-via-tool.md +62 -0
  147. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  148. package/rules/always-on/build-value.md +47 -0
  149. package/rules/always-on/cross-session-messaging.md +59 -0
  150. package/rules/always-on/loop-and-monitor.md +221 -0
  151. package/rules/always-on/parallel-sessions.md +142 -12
  152. package/rules/always-on/receiving-review.md +108 -0
  153. package/rules/always-on/test-value.md +40 -0
  154. package/rules/always-on/verification-before-completion.md +77 -0
  155. package/scripts/archive-closed-prds.mjs +258 -18
  156. package/scripts/autopilot.mjs +31 -12
  157. package/scripts/backfill-abandoned-sessions.mjs +80 -11
  158. package/scripts/backfill-evidence-digest.mjs +376 -0
  159. package/scripts/cursor-install.mjs +89 -48
  160. package/scripts/emit-event.mjs +10 -2
  161. package/scripts/export-hw-learnings.mjs +143 -2
  162. package/scripts/express-path.mjs +299 -0
  163. package/scripts/generate-cursor-adapter.mjs +253 -0
  164. package/scripts/github-protection-audit.mjs +358 -0
  165. package/scripts/lib/auq/parse.mjs +5 -29
  166. package/scripts/lib/auto-dialectic.mjs +68 -0
  167. package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
  168. package/scripts/lib/build-live-signals.mjs +49 -27
  169. package/scripts/lib/ci-status-banner.mjs +158 -11
  170. package/scripts/lib/cold-start-detector.mjs +23 -14
  171. package/scripts/lib/command-blocker.mjs +70 -0
  172. package/scripts/lib/config/block-header.mjs +55 -0
  173. package/scripts/lib/config/discovery-validator.mjs +7 -2
  174. package/scripts/lib/config/health-endpoints.mjs +383 -0
  175. package/scripts/lib/config/reconcile.mjs +79 -4
  176. package/scripts/lib/config/remote-hosts.mjs +233 -0
  177. package/scripts/lib/config/section-extractor.mjs +235 -36
  178. package/scripts/lib/config-schema.mjs +9 -1
  179. package/scripts/lib/config.mjs +87 -8
  180. package/scripts/lib/convergence-monitor.mjs +13 -2
  181. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  182. package/scripts/lib/dispatcher/cli.mjs +2 -2
  183. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  184. package/scripts/lib/events-schema.mjs +48 -0
  185. package/scripts/lib/events.mjs +238 -5
  186. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  187. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  188. package/scripts/lib/express-path.mjs +327 -0
  189. package/scripts/lib/file-lock.mjs +22 -4
  190. package/scripts/lib/gates/gate-full.mjs +81 -8
  191. package/scripts/lib/gates/gate-helpers.mjs +76 -15
  192. package/scripts/lib/git-config-drift.mjs +134 -5
  193. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  194. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  195. package/scripts/lib/host-identity.mjs +247 -2
  196. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  197. package/scripts/lib/issue-budget.mjs +229 -30
  198. package/scripts/lib/learnings/io.mjs +55 -10
  199. package/scripts/lib/learnings/schema.mjs +95 -28
  200. package/scripts/lib/lock-reaper.mjs +7 -1
  201. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  202. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  203. package/scripts/lib/memory-banner.mjs +25 -10
  204. package/scripts/lib/memory-paths.mjs +15 -6
  205. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  206. package/scripts/lib/peer-discovery.mjs +20 -2
  207. package/scripts/lib/platform.mjs +72 -9
  208. package/scripts/lib/plugin-root.mjs +143 -19
  209. package/scripts/lib/project-hygiene.mjs +43 -3
  210. package/scripts/lib/quality-gate.mjs +271 -13
  211. package/scripts/lib/reconcile/emitter.mjs +87 -19
  212. package/scripts/lib/reconcile/engine.mjs +517 -18
  213. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  214. package/scripts/lib/reconcile/renderer.mjs +148 -3
  215. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  216. package/scripts/lib/reconcile/writer.mjs +415 -84
  217. package/scripts/lib/rule-loader.mjs +37 -2
  218. package/scripts/lib/rules-sync.mjs +51 -8
  219. package/scripts/lib/scope-gate.mjs +126 -0
  220. package/scripts/lib/session-close-backfill.mjs +427 -37
  221. package/scripts/lib/session-discovery.mjs +69 -5
  222. package/scripts/lib/session-end/phase-skip.mjs +38 -5
  223. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  224. package/scripts/lib/session-id.mjs +30 -14
  225. package/scripts/lib/session-identity/own-session.mjs +220 -0
  226. package/scripts/lib/session-lock.mjs +85 -30
  227. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  228. package/scripts/lib/session-schema/validator.mjs +40 -0
  229. package/scripts/lib/session-start-probes.mjs +608 -0
  230. package/scripts/lib/session-transition.mjs +277 -0
  231. package/scripts/lib/sessions-canonical.mjs +446 -0
  232. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  233. package/scripts/lib/spiral-carryover.mjs +90 -9
  234. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  235. package/scripts/lib/state-md/mission-status.mjs +350 -52
  236. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  237. package/scripts/lib/state-md.mjs +12 -2
  238. package/scripts/lib/telemetry/schema.mjs +74 -8
  239. package/scripts/lib/telemetry/sync.mjs +91 -16
  240. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  241. package/scripts/lib/validate/check-agents.mjs +66 -0
  242. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  243. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  244. package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
  245. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  246. package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
  247. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  248. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  249. package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
  250. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  251. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  252. package/scripts/lib/validate/check-unwired-features.mjs +333 -32
  253. package/scripts/lib/validate/check-validator-registration.mjs +248 -0
  254. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  255. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  256. package/scripts/lib/validate/repo-files.mjs +275 -0
  257. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  258. package/scripts/lib/vault-mirror/process.mjs +99 -43
  259. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  260. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  261. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  262. package/scripts/lib/vault-status/board-writer.mjs +381 -141
  263. package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
  264. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  265. package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
  266. package/scripts/lib/wave-resource-gate.mjs +127 -7
  267. package/scripts/lib/wave-transcript-tail.mjs +889 -0
  268. package/scripts/materialize-wave-scope.mjs +228 -15
  269. package/scripts/mcp-server.sh +11 -2
  270. package/scripts/memory-propose.mjs +132 -8
  271. package/scripts/parse-config.mjs +65 -0
  272. package/scripts/promote-vault-strict.mjs +4 -15
  273. package/scripts/site-numbers.mjs +36 -4
  274. package/scripts/token-audit.sh +9 -2
  275. package/scripts/validate-plugin.mjs +29 -0
  276. package/scripts/validate-wave-scope.mjs +67 -0
  277. package/scripts/vault-consolidate.mjs +3 -11
  278. package/scripts/vault-integration-watcher.mjs +2 -4
  279. package/scripts/vault-mirror.mjs +305 -51
  280. package/skills/_shared/monitor-patterns.md +31 -5
  281. package/skills/_shared/parallel-aware-auq.md +31 -2
  282. package/skills/_shared/parallel-aware-preamble.md +19 -4
  283. package/skills/_shared/platform-tools.md +11 -5
  284. package/skills/_shared/state-ownership.md +29 -2
  285. package/skills/autopilot/SKILL.md +5 -1
  286. package/skills/bootstrap/SKILL.md +3 -3
  287. package/skills/bootstrap/_shared-template.md +18 -10
  288. package/skills/bootstrap/deep-template.md +10 -6
  289. package/skills/bootstrap/fast-template.md +15 -8
  290. package/skills/bootstrap/standard-template.md +10 -6
  291. package/skills/claude-md-drift-check/checker.mjs +39 -11
  292. package/skills/contract-version-bump/SKILL.md +1 -1
  293. package/skills/dispatcher/SKILL.md +1 -1
  294. package/skills/ecosystem-health/SKILL.md +4 -1
  295. package/skills/ecosystem-health/wizard.md +5 -0
  296. package/skills/evolve/SKILL.md +38 -1
  297. package/skills/journey-audit/SKILL.md +270 -0
  298. package/skills/peekaboo-driver/SKILL.md +15 -3
  299. package/skills/persona-panel/SKILL.md +1 -1
  300. package/skills/reconcile/SKILL.md +46 -3
  301. package/skills/remote-offload/SKILL.md +89 -0
  302. package/skills/session-end/SKILL.md +17 -4
  303. package/skills/session-end/metrics-collection.md +7 -4
  304. package/skills/session-end/phase-3-6-tail.md +20 -9
  305. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  306. package/skills/session-plan/SKILL.md +6 -1
  307. package/skills/session-plan/wave-template.md +1 -0
  308. package/skills/session-start/SKILL.md +54 -17
  309. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  310. package/skills/session-start/phase-8-5-express-path.md +77 -12
  311. package/skills/vault-sync/validator.mjs +31 -0
  312. package/skills/wave-executor/SKILL.md +5 -3
  313. package/skills/wave-executor/circuit-breaker.md +34 -9
  314. package/skills/wave-executor/wave-loop.md +143 -22
  315. package/templates/_shared/journey-manifest.md +110 -0
  316. package/templates/_shared/rules/parallel-sessions.md +0 -77
package/docs/pi-setup.md CHANGED
@@ -18,6 +18,8 @@ pi install npm:session-orchestrator
18
18
 
19
19
  This is the primary install path — the package is published to npm (since v3.16.0, 2026-07-19) and carries the `pi-package` keyword for the [Pi packages gallery](https://pi.dev/packages) index; no local checkout required. The dev-fallback options below remain available for working against a local checkout.
20
20
 
21
+ The short git form documented for Pi packages (`pi install git:github.com/user/repo`) is **unverified against this repo as of 2026-08-28** — the `pi` CLI was not installed on the verifying host (`which pi` → not found), so no install attempt could be made. The npm form above remains the only measured, documented install path; do not elevate the `git:` short form to this guide or to README until it has been run against this repo and the result dated.
22
+
21
23
  ### Option 2: Project-local registration (dev fallback)
22
24
 
23
25
  ```bash
@@ -118,7 +118,9 @@ Before `syncRules()` writes a source file into a consumer repo's `.claude/rules/
118
118
  | `zero-match-globs` | warn | A `globs:` pattern matching 0 files in the target repo's tracked file list (`git ls-files`, falling back to a directory walk). Legitimately possible in a freshly-scaffolded repo. |
119
119
  | `foreign-glob` | warn | A glob segment carrying a PascalCase, product-like token (regex `[A-Z][a-z]+[A-Z]`, e.g. `WalkAITalkieTests`) — a likely copy-paste leftover from another project's rule scope. |
120
120
 
121
- Error-severity violations skip the write for that file (recorded in `syncRules()`'s `errors[]`); warn-severity violations do not block the write and are recorded additively in `warnings[]`. The standalone CLI (`node scripts/lib/validate-vendored-rules.mjs --dir <rulesDir> [--target-root <repo>] [--require-provenance] [--json] [--mode hard|warn]`) exits `0` (no errors, or errors under `--mode warn`), `1` (errors present under `--mode hard`), or `2` (invocation error).
121
+ Error-severity violations skip the write for that file (recorded in `syncRules()`'s `errors[]`); warn-severity violations do not block the write and are recorded additively in `warnings[]`. Since #1098 the envelope also carries `sanitizer[]` — `{file, line, kind, text}` records from `scanVendoringLeaks()` for plugin-internal citations that resolve to a real file under the plugin root (`scripts/…`, `hooks/…`, `skills/…`, `docs/…`, `tests/…`) and for `See Also` references that name no entry in `rules/_index.md`. The sanitizer is report-only: it never rewrites content and never contributes to `errors[]`, so a finding changes neither the write decision nor the exit code — a human decides whether the citation is a leak. `rules-sync.mjs`'s CLI additionally prints each finding to **stderr** as `rules-sync: sanitizer <kind> <file>:<line> — <text>`, so the report reaches an operator who never parses the JSON envelope.
122
+
123
+ `scanVendoringLeaks()` itself lives in `scripts/lib/validate-vendored-rules.mjs` (it has the validator's shape — judge one rule file, return findings); `rules-sync.mjs` re-exports it for importers that predate the move. The standalone CLI (`node scripts/lib/validate-vendored-rules.mjs --dir <rulesDir> [--target-root <repo>] [--plugin-root <dir>] [--require-provenance] [--json] [--mode hard|warn]`) exits `0` (no errors, or errors under `--mode warn`), `1` (errors present under `--mode hard`), or `2` (invocation error). Passing `--plugin-root <dir>` turns on the same sanitizer scan there: findings appear under a `sanitizer` key in `--json` mode and as `validate-vendored-rules: sanitizer <kind> <file>:<line> — <text>` stderr lines otherwise. The `unresolvable-see-also` half needs `<pluginRoot>/rules/_index.md` to be readable; when it is not, only the `repo-local-path` check runs. Report-only there too — `sanitizer[]` never moves the exit code.
122
124
 
123
125
  ### Archetype-scoped manifest tags (issue #722 Epic A Wave 3)
124
126
 
@@ -37,6 +37,31 @@ Steps 1, 2, 3, 4 and the `--assert-subset` assertion all read the *same* file, a
37
37
 
38
38
  The coordinator's **own** planned direct edits belong in `coordinator.json` in the identical form. They are not dispatches, so the hook can never see them (§ 6); the CLI check is the only gate that covers them.
39
39
 
40
+ ### 2.3 The manifest names its writer (#1123)
41
+
42
+ `wave-scope.json` is written into the WORKING COPY, not into the session — so until #1123 one session's manifest governed every session sharing that checkout. Measured 2026-08-22 (#1082): a Discovery wave's `allowedPaths: []` — which `wave-loop.md` prescribes for *every* Discovery wave — denied every write of an unrelated parallel session, with a deny reason that could only tell it to fix a wave plan it does not own.
43
+
44
+ Two OPTIONAL manifest fields close that: `session` (the raw `session_id`) and its human-readable twin `semantic_session`. Both come from ONE `sessionAttribution(repoRoot)` call (`scripts/lib/events.mjs`, reads `.orchestrator/session.lock` once) in the same coordinator step that writes the rest of the manifest — `skills/wave-executor/wave-loop.md` § Scope Manifest 1.
45
+
46
+ The reader is `hooks/enforce-scope.mjs` **Gate 3b**, between the manifest parse (G3) and the path-guard gate (G4). It resolves identity via `new Set(readProcessLocalSessionIds({ hookInput: input }))` and classifies via `classifyManifestSession(scope, ownIds)`, both from [`scripts/lib/session-identity/own-session.mjs`](../scripts/lib/session-identity/own-session.mjs):
47
+
48
+ | Manifest state | `classifyManifestSession` verdict | Gate 3b disposition |
49
+ |---|---|---|
50
+ | no `session` / `semantic_session` (legacy, pre-#1123) | `unknown` | ENFORCE — falls through unchanged |
51
+ | an id present and matching one of our own | `own` | ENFORCE — falls through unchanged |
52
+ | ids present, none matching, own identity resolvable | `foreign` | **ALLOW** + one `orchestrator.scope.foreign_session_ignored` event |
53
+ | ids present, own identity unresolvable (empty id set) | `unknown` | ENFORCE |
54
+
55
+ Five properties are choices, not omissions — and every one of them points the fail-**closed** way, the deliberate inverse of § 4.1's posture for the dispatch hook:
56
+
57
+ - **Only what is PROVABLY foreign is foreign.** `readProcessLocalSessionIds()` returns the ids that are PROCESS-LOCAL — hook input (`session_id`/`sessionId`/`parent_session_id`) and `CLAUDE_CODE_SESSION_ID` — and an EMPTY set when neither yields an id, which can only produce `unknown`. The repo-global `session.lock` is deliberately NOT a tier here (#1194): it is ONE file shared by every session in the checkout, so unioning it let a peer's manifest match a peer-written lock id, classify `own`, and have Gate 7 deny the second session's legitimate writes — the exact lockout G3b exists to end. A better signal REPLACES a worse one (`.claude/rules/host-resources.md` § HR-102). A gate that guessed would turn "cannot tell" into a silent enforcement-off on every harness exporting no session id. Every value is trimmed on the way in, so a whitespace-only env var cannot enter as a phantom id that matches nothing (`.claude/rules/development.md` § env-var whitespace trap).
58
+ - **Union across the process-local tiers, not first-tier-wins.** Both process-local tiers are read and merged; only an id in NEITHER is somebody else's. Gating them against each other made the READER's identity a strict subset of the WRITER's — the manifest's `session` comes from `sessionAttribution()` = the same repo-global lock — and two divergences inside these tiers produce the same silent failure, the OWN manifest read `foreign` and the write gate switched itself off for the whole wave, logging an event indistinguishable from correct behaviour: (a) a nested harness where payload `session_id` ≠ `CLAUDE_CODE_SESSION_ID` (measured in `hooks/pre-bash-issue-budget.mjs` `resolveSessionId`); (b) a sub-agent invocation, whose own id is the subagent's while the manifest names the coordinator. A third divergence — a session that lost the lock race and wrote a PEER's id into its own manifest — is NO longer covered here since #1194 dropped the lock tier; it is the accepted cost in limit 11, and its defense is the writer guard. The merge only ADDS ids the process actually carries, so the security direction is unchanged: a manifest whose id appears in neither tier still classifies `foreign`. Its cost is named in limit 11 below.
59
+ - **The writer verifies the binding names itself.** Because the lock is repo-global, `skills/wave-executor/wave-loop.md` § Scope Manifest 1 requires the coordinator to compare `sessionAttribution()`'s ids against its own session (STATE.md `session`) and to OMIT the `session`/`semantic_session` keys when they diverge — unbound = ENFORCE. The reader's union covers the case anyway; the writer guard keeps the manifest readable as an audit record instead of publishing a foreign name.
60
+ - **Gate 3b runs after the parse, never on the raw bytes.** A corrupt manifest yields `{}`, hence no ids, hence `unknown` — and keeps failing closed. A gate that peeked at the bytes first would let a truncated manifest disarm the guard.
61
+ - **The empty string is a validator ERROR, not a third flavour of absent.** `validateSession()` → `validateOptionalSessionId()` in `scripts/validate-wave-scope.mjs` rejects `"session": ""` with *"an empty id attributes to nothing; omit the key entirely to declare the manifest unbound"*. An empty id satisfies a truthiness check while matching nobody, so every reader would classify the manifest FOREIGN where the writer meant UNBOUND — opposite dispositions, not a cosmetic ambiguity. An ABSENT key only WARNS, because the § 3.3 pre-union skeleton is itself an unbound manifest and so is every manifest written before #1123.
62
+
63
+ The event is what keeps the skip countable rather than silent: `orchestrator.scope.foreign_session_ignored` carries `hook`, `manifest`, `manifest_session`, `own_session`, `wave` and `file_path`. It is deliberately an event and not an `emitWarn` — the branch is hit on *every* Edit of the non-owning session, so a stderr line per write would be noise the operator learns to ignore.
64
+
40
65
  ## 3. The collision algorithm
41
66
 
42
67
  `findScopeCollisions(agentScopes, { knownFiles })` compares every cross-agent entry pair through `classifyEntryCollision()`, in three binding stages:
@@ -104,6 +129,24 @@ Two structural rules keep this matrix honest, both recorded in the hook header:
104
129
  - `decide()` is a **pure function returning a verdict**; the module emits exactly once, at the end. `emitWarn`/`emitDeny` call `process.exit(0)` and never return, so a warn emitted from inside the checking flow would terminate the process before a later collision could be denied — and would skip the lock's release `finally`.
105
130
  - Row 9's discriminator is **not** `ok !== true`. `ok` means *disjoint*, so `ok === false` is the normal result of a real collision; "not evaluable" is `ok === false` with BOTH result arrays empty. Reading `ok` as evaluability would turn every genuine collision into a warn, i.e. an allow — the exact fail-open the hook exists to prevent.
106
131
 
132
+ ### 4.2 Observability — one record per dispatch decision (#1092)
133
+
134
+ Every dispatch that reaches a verdict also appends one `orchestrator.wave_dispatch.scope_checked` record to `<projectDir>/.orchestrator/metrics/events.jsonl` (payload contract: [`docs/events-schema.md`](events-schema.md)). It is built inside `decide()` as `verdict.telemetry` and emitted by `main()` — awaited **before** the terminal `emitAllow`/`emitDeny`/`emitWarn`, each of which calls `process.exit()` and would discard a pending append — inside a `try/catch`, so a failed write can never change the verdict or the exit code.
135
+
136
+ Why it exists: the in-ledger `scopeSignals` counter (§ 4.1, rows 5/6) is a **wave tally**. It answers "how many dispatches of this wave carried a scope", never "which agent was dispatched unscoped" — and a wave in which the injection step was skipped entirely still produces a plausible-looking tally. The per-dispatch record makes the individual dispatch falsifiable (`.claude/rules/host-resources.md` § HR-105).
137
+
138
+ **What the event proves:** the hook SAW — or did not see — a `FILE-SCOPE` declaration in the prompt the coordinator handed to the dispatch tool, which declaration shape parsed it (`shape`), whether any path survived (`injected`, `declared_path_count`), and which matrix row decided (`ledger_result`, `collision_count`). That is the **send** side, measured at the PreToolUse boundary.
139
+
140
+ **What it does not prove:** that the block reached the agent's context, or that the model read it. Those are **receive**-side facts, and this hook observes the tool payload, not the assembled prompt. A record reading `injected: true` is therefore evidence about the coordinator's dispatch, never about the agent's obedience — reading it as the latter rebuilds exactly the false confidence § 2.2 warns about. The counterpart signal on the receive side remains the agent's own behaviour (`enforce-scope` at write time) and the W5 verification pass.
141
+
142
+ **Payload discipline.** Counts and closed enums only, plus `agent_id` (the coordinator's own dispatch description, clamped) and the optional session attribution: no prompt body, no declared path, no glob. Issue #1092's acceptance criterion 3 is the rule, and the reason is concrete — this record also travels over the optional Clank Event-Bus webhook with no redaction, and paths under `01-projects/` carry private project slugs.
143
+
144
+ **Revisit-Trigger** (verbatim from issue #1092, for the transport half this section deliberately does NOT close):
145
+
146
+ > Implement when the platform exposes a stable prompt-assembly hook or when a coordinator-owned digest event can be proven against the real dispatched transcript.
147
+
148
+ Until then the issue's remaining acceptance criteria — a scope-file **digest** proven to have reached the prompt assembler, and an omitted/malformed injection turning an end-to-end probe red while materializer, disjointness, union and subset checks stay green — are not satisfiable by any mechanism inside this repo, because no observable boundary carries the final prompt.
149
+
107
150
  ## 5. The liveness probe
108
151
 
109
152
  A ledger with no notion of completion denies the wrong thing. Measured over 38 archived transcripts of this repo (346 `Agent` dispatch blocks; hook header, 2026-08-14): 0 of 4 same-batch overlaps and **2 of 2 cross-dispatch overlaps** would have been denied — and both cross-dispatch pairs were legitimate **sequential repair passes** (a dispatch and its later fix). Because a deny deliberately does not persist the ledger, the re-dispatch would have met the same stale record: a permanent block.
@@ -129,6 +172,9 @@ Complete list of what this guard does **not** see, or sees only approximately:
129
172
  6. **Glob ∩ glob without witnesses.** Stage 3a needs tracked files; with git unavailable (row 8) or for files not yet on disk, only stage 3b's prefix fallback carries the load — and it requires at least one recursive entry, so two non-recursive globs that intersect only in an unborn file are not detected.
130
173
  7. **Only collisions involving the current dispatch are actionable.** A pair among already-dispatched agents was either denied at its own dispatch or predates the guard; re-denying it would block an innocent third agent.
131
174
  8. **Lock loss reopens the race.** On lock timeout the cycle runs unlocked (row 14) — two dispatches starting together can then read the same ledger state and one record is lost. That is the pre-lock behaviour, chosen over denying on a lock-file problem.
175
+ 9. **The session binding is self-declared** (§ 2.3). `session` is a plain field in a file any process in this working copy can write, so writing a foreign id into it turns the write gate off for that manifest. Named rather than hidden: it is the SAME power `enforcement: "off"` already grants in the same file, so Gate 3b adds no new authority — the manifest is the coordinator's own artefact either way.
176
+ 10. **Only the WRITE gate is session-bound.** The dispatch ledger of § 4 takes its session component from the harness's own `input.session_id` (`waveKeyOf(projectDir, sessionId, …)`), and reads only `wave` and `role` out of `wave-scope.json` — the `session` field is not consulted there at all. So a peer session's manifest cannot bind this session's writes since #1123, but the two hooks reach that property by different routes, and a change to one does not carry to the other.
177
+ 11. **A session that published a peer's id reads its OWN manifest as `foreign` (#1194).** Dropping the lock tier (§ 2.3) moved the cost to the other side of the trade, deliberately: a session that lost the `bootstrapLock()` race got the peer's id from `sessionAttribution()`, and if it writes that id into its own manifest, Gate 3b now classifies the manifest `foreign` and its own write guard stands down. The defense is on the WRITER, not the reader — `skills/wave-executor/wave-loop.md` § Scope Manifest, "Verify the binding names YOU before you write it": compare both ids against your own session and OMIT the keys when they diverge, because unbound = ENFORCE. CEILING (BV-004): on a harness that exports no session env var and puts no `session_id` in the hook payload (Codex CLI, Cursor today) both tiers are empty, so G3b is permanently `unknown` = enforce = pre-#1123 behaviour there. Revisit when Codex/Cursor hook payloads carry a session id.
132
178
 
133
179
  ## 7. Debugging
134
180
 
@@ -138,7 +184,7 @@ Complete list of what this guard does **not** see, or sees only approximately:
138
184
  - The other agent has finished, but the ledger still binds it → the transcript carried no evidence (§ 5) and you are inside the TTL window. Delete `.orchestrator/wave-dispatch-scopes.json`; the next dispatch rebuilds it.
139
185
  - The `waveKey` names an older wave → the `<session>|w?|?` fallback (limit 5). Check that `<state-dir>/wave-scope.json` exists and is readable, then delete the ledger.
140
186
 
141
- **A dispatch was NOT denied and should have been.** Work down the allow rows: is `FILE-SCOPE` present in the prompt with a fenced block right after it (rows 5/6)? Is the hook armed at all (`GUARD INACTIVE` on stderr = row 2)? Did a `systemMessage` warning appear (rows 7/9)? Cross-check the same scopes through the CLI, which does not depend on prose:
187
+ **A dispatch was NOT denied and should have been.** Start at the ledger line, which now says which row fired without re-running anything: `jq -c 'select(.event=="orchestrator.wave_dispatch.scope_checked")' .orchestrator/metrics/events.jsonl | tail` — `injected:false` with `signal:"marker-absent"` is row 5 (no declaration in the prompt at all, i.e. the injection step, not the guard), `signal:"unparseable"` is row 6 (a declaration the parser could not use), `ledger_result:"allow-finished"` is row 10a (the collision was real but its partner had finished), and NO record at all for a dispatch you watched happen means the hook never ran or crashed before deciding (§ 4.2). Then work down the allow rows: is `FILE-SCOPE` present in the prompt with a fenced block right after it (rows 5/6)? Is the hook armed at all (`GUARD INACTIVE` on stderr = row 2)? Did a `systemMessage` warning appear (rows 7/9)? Cross-check the same scopes through the CLI, which does not depend on prose:
142
188
 
143
189
  ```bash
144
190
  node scripts/validate-wave-scope.mjs --assert-disjoint "$WAVE_SCOPES_SIDECAR" < <state-dir>/wave-scope.json
@@ -163,5 +209,6 @@ Re-measure before citing any of these downstream. A count re-briefed later is a
163
209
 
164
210
  - `skills/wave-executor/wave-loop.md` § Scope Manifest — the coordinator runbook (steps 3.1–3.3) and § Pre-Dispatch: File-Scope Injection (the prompt block shape).
165
211
  - `.claude/rules/parallel-sessions.md` § Decision Tree (why a file in two declared scopes is never a benign sibling signal), § PSA-006 (measurement discipline).
166
- - `hooks/enforce-scope.mjs` — the write-time gate, fail-**closed**; the deliberate inversion of this hook's posture.
212
+ - `hooks/enforce-scope.mjs` — the write-time gate, fail-**closed**; the deliberate inversion of this hook's posture. Its Gate 3b is the reader of the § 2.3 session binding; `scripts/lib/session-identity/own-session.mjs` holds the identity half.
213
+ - `skills/_shared/state-ownership.md` § `wave-scope.json` Session Binding — the same contract from the ownership side (which shared working-copy artefact belongs to which session).
167
214
  - [`docs/adr/0011-guard-degradation-semantics.md`](adr/0011-guard-degradation-semantics.md) — the exit-0 hook protocol (#906) and why a truncated stdout envelope reads as no-decision, i.e. as ALLOW.
@@ -75,11 +75,32 @@ Some sub-configs live in dedicated policy files under `.orchestrator/policy/`:
75
75
  | Field | Type | Default | Description |
76
76
  |-------|------|---------|-------------|
77
77
  | `agents-per-wave` | integer or integer with overrides | `6` | Maximum parallel subagents per wave. Supports session-type overrides: `6 (deep: 18)` outputs `{"default": 6, "deep": 18}`. Plain integers remain plain. The override key names a session type but does **not** create one: there is no `session-type:` Session Config key — `parseSessionConfig()` emits none, so writing one into a repo's `## Session Config` block is inert prose. The session type comes from the `/session` argument (default `deep`, see `commands/session.md`) and is persisted to STATE.md frontmatter as `session-type:`, which is the only live read (`scripts/print-applicable-rules.mjs` rule mode-gating). |
78
- | `agent-mapping` | object | null | Optional mapping of role keys to agent names for explicit agent binding. Keys: `impl`, `test`, `db`, `ui`, `security`, `compliance`, `docs`, `perf`. Example: `{ impl: code-editor, test: test-specialist }`. Overrides auto-discovery when present. |
78
+ | `agent-mapping` | object | null | Optional mapping of role keys to agent names for explicit agent binding. Keys: `impl`, `test`, `db`, `ui`, `security`, `compliance`, `docs`, `perf`. Example: `{ impl: code-editor, test: test-specialist }`. Overrides auto-discovery when present. Values may carry a channel prefix — see § `agent-mapping` values below. |
79
79
  | `waves` | integer | `5` | Number of execution waves for feature and deep sessions. |
80
80
  | `recent-commits` | integer | `20` | Number of recent commits to display during session start git analysis. |
81
81
  | `special` | string | none | Repo-specific instructions. Freeform text that the orchestrator reads and follows during sessions. |
82
82
 
83
+ ### `agent-mapping` values — channel prefixes (#1150)
84
+
85
+ A mapping value has three forms, distinguished by the colon:
86
+
87
+ | Value form | Meaning | Dispatch |
88
+ |---|---|---|
89
+ | `<project-agent>` (no colon) | An agent file under `<state-dir>/agents/<name>.md` | Agent tool, unchanged |
90
+ | `session-orchestrator:<plugin-agent>` | A plugin agent shipped by this repo | Agent tool, unchanged |
91
+ | `cursor:<model>` | A **foreign model** over the Cursor channel (`cursor-agent`) | Coordinator-direct via `dispatchForeign()` — **not** the Agent tool |
92
+
93
+ ```
94
+ agent-mapping: { impl: cursor:composer-2.5, test: cursor:cursor-grok-4.6-high, security: security-reviewer }
95
+ ```
96
+
97
+ **Unknown channels fail loud.** `scripts/lib/config.mjs` parses this key and throws on any prefix outside `cursor` / `session-orchestrator` (`agent-mapping role '<k>' names unknown channel '<c>' …`), and on a prefix with an empty target. Silently accepting a typo would dispatch to an agent that does not exist and surface only as an empty wave, much later. A value without a colon is a plain agent name and is never channel-parsed.
98
+
99
+ **Where the rest of the contract lives** — deliberately not here, so one place owns it:
100
+
101
+ - **Model selection** (which model for which role, and why): the operator's model-routing SSOT (ADR-002). Working defaults are `composer-2.5` for foreign impl and `cursor-grok-4.6-high` for review / test-writing / judgment roles.
102
+ - **Dispatch contract** (detached worktree, the `never_foreign` role lock, the filesystem-measured verdict, the MANDATORY Claude semantic diff-review before merge-back, wall-clock timeout instead of `maxTurns`, and the `orchestrator.foreign_dispatch.completed` event that replaces the hook-chain telemetry a foreign run cannot emit): `skills/wave-executor/wave-loop.md` § Third branch: foreign-model dispatch.
103
+
83
104
  ## VCS & Infrastructure
84
105
 
85
106
  | Field | Type | Default | Description |
@@ -90,8 +111,34 @@ Some sub-configs live in dedicated policy files under `.orchestrator/policy/`:
90
111
  | `cross-repos` | list | none | Related repositories under `~/Projects/`. The orchestrator checks their git state and critical issues during session start. |
91
112
  | `cross-repo.projects` | list | `[]` | Repos to process when running cross-repo maintenance scripts (`run-migrate-v2-cross-repo.mjs`, `vault-integration-watcher.mjs`, `promote-vault-strict.mjs`). Each entry is a path (absolute, `~`-prefixed, or bare name resolved under `~/Projects/`). When this list is empty or absent, those scripts emit a one-line notice and exit 0 — they never error on an empty list. Example: `[~/Projects/my-app, ~/Projects/another-app]`. |
92
113
  | `pencil` | string | none | Path to a `.pen` design file (relative to project root). Enables design-code alignment reviews after Impl-Core and Impl-Polish waves. |
93
- | `ecosystem-health` | boolean | `false` | Enable service health checks at session start. Requires `health-endpoints` to be configured. |
94
- | `health-endpoints` | list | none | Service URLs to check health. Each entry is an object with `name` and `url` fields. |
114
+ | `ecosystem-health` | boolean | `false` | Enable service health checks at session start. Requires `health-endpoints` to be configured. Accepts the SCALAR form (`ecosystem-health: true`) read off the flat key/value map, or a BLOCK form (`ecosystem-health:` with no value, followed by an indented body — the wizard's output) read via `_parseEcosystemHealthBlockEnabled()`; the scalar wins when both are present (#1174). |
115
+ | `health-endpoints` | list | none | Service URLs to check health. Each entry is an object with `name` and `url` fields. Parsed by `scripts/lib/config/health-endpoints.mjs`, which accepts THREE forms (#1174) — see below. |
116
+
117
+ **`health-endpoints` accepted forms.** Before #1174 the parser read this key off the flat KV map,
118
+ which bails to `null` the instant a value contains `{` and cannot see a nested YAML block at all;
119
+ the wizard's own output (Form B) silently failed to parse. All three forms below are parsed
120
+ content-scoped, independent of the flat KV map:
121
+
122
+ ```yaml
123
+ # Form A — inline object array
124
+ health-endpoints: [{name: "API", url: "https://a/health"}, {name: "W", url: "http://w:8080/z"}]
125
+
126
+ # Form B — nested block (top-level, or one level under `ecosystem-health:`); the wizard's output.
127
+ # Block items may also be inline objects, `- { name: API, url: … }` (the form this file's own
128
+ # example above uses).
129
+ health-endpoints:
130
+ - name: API
131
+ url: https://api.example.com/health
132
+
133
+ # Form C — bare bracket list of URLs; each URL becomes its own name ({ name: <url>, url: <url> })
134
+ health-endpoints: [https://a/health, https://b/health]
135
+ ```
136
+
137
+ The `name=url` shorthand (Form D) is **not** supported — an entry containing `=` is treated
138
+ verbatim as a Form-C URL, never split on `=`. A malformed entry (missing `name` or `url`, an
139
+ unmatched brace) resolves to `null` for the whole key and prints exactly one
140
+ `config: health-endpoints:` WARN to stderr — it never throws, so a broken config key cannot take
141
+ down session-start.
95
142
  | `issue-limit` | integer | `50` | Maximum issues to fetch when querying VCS during session start. |
96
143
  | `stale-branch-days` | integer | `7` | Days of inactivity before a branch is flagged as stale. |
97
144
  | `stale-issue-days` | integer | `30` | Days without progress before an issue is flagged for triage. |
@@ -193,7 +240,7 @@ issue-budget:
193
240
 
194
241
  **Exemptions (load-bearing).** `priority::critical`, the carryover class (`[Carryover]`, `[SPIRAL]`/`[FAILED]`, `type::carryover`, a bare `carryover` label) and `broken-window` closure issues bypass the cap unconditionally. Without those exemptions the cap would break the standing session-end promises in `skills/session-end/SKILL.md` (Phase 1.8 "non-deselectable" SPIRAL/FAILED carryover, and the Critical Rule "ALWAYS create issues for unfinished PLANNED work"). Exempt creations are counted in the state file's `exempt` field for observability but never blocked.
195
242
 
196
- **Counter file:** `.orchestrator/runtime/issue-budget.json` — `{ sessionId, count, exempt, overflow: [...] }`. It resets automatically when a new `sessionId` is seen.
243
+ **Counter file:** `.orchestrator/runtime/issue-budget/<sha256(sessionId)[0..16]>.json` — `{ sessionId, count, exempt, overflow: [...] }`, ONE file per session (#1141: the former single `issue-budget.json` slot was reset by whichever session wrote last, so two sessions in one working copy silently disabled each other's cap). Identity-less callers still use the legacy flat path; `budgetStatePath(repoRoot, sessionId)` in `scripts/lib/issue-budget.mjs` is the resolver.
197
244
 
198
245
  **Used by:** `hooks/pre-bash-issue-budget.mjs` (shell path, PreToolUse/Bash), `scripts/lib/spiral-carryover.mjs` `runCli()` (programmatic path), `scripts/lib/issue-budget.mjs` (shared decision core), `skills/session-end/SKILL.md` Phase 5 Step 3b (overflow drain). Parser: `scripts/lib/config/issue-budget.mjs`.
199
246
 
@@ -364,18 +411,18 @@ state-md-lock:
364
411
 
365
412
  ## Discovery-Validator (PSA-006 Enforcement, #567)
366
413
 
367
- Non-blocking `SubagentStop` hook that mechanically enforces PSA-006: distributional claims ("N of M", "100% of", "all N", "no remaining", "every X", "none of") appearing in a subagent's transcript tail must carry an adjacent fenced grep/rg/find transcript. When a claim lacks one, the hook records a `discovery_validator_violation` event in `.orchestrator/metrics/events.jsonl` and emits a stderr WARN. v1 is log + warn only — exit 0 always, never blocks the agent; a blocking hard-gate is reserved for a future iteration. Default ON (flip risk is near-zero; the hook only ever generates telemetry).
414
+ Non-blocking `SubagentStop` hook that mechanically enforces PSA-006: distributional claims ("N of M", "100% of", "all N", "no remaining", "every X", "none of") appearing in a subagent's transcript tail must carry an adjacent fenced grep/rg/find transcript. When a claim lacks one, the hook records a `discovery_validator_violation` event in `.orchestrator/metrics/events.jsonl` and emits a stderr WARN. v1 is log + warn only — exit 0 always, never blocks the agent; a blocking hard-gate is reserved for a future iteration. Default OFF (opt-in) — the #690 flip to ON was reverted 2026-09-02 (#1191) after fleet measurement showed 6,946 violation events accumulating in 18 repos that never declared the block.
368
415
 
369
416
  All fields live under a top-level `discovery-validator` object in your Session Config host file (`CLAUDE.md` or `AGENTS.md`), for example:
370
417
 
371
418
  ```yaml
372
419
  discovery-validator:
373
- enabled: true # on by default; log+warn-only, exit-0-always — set false to silence
420
+ enabled: true # off by default; opt in per repo — log+warn-only, exit-0-always
374
421
  ```
375
422
 
376
423
  | Field | Type | Default | Description |
377
424
  |-------|------|---------|-------------|
378
- | `discovery-validator.enabled` | boolean | `true` | Master toggle. When `false`, the `SubagentStop` hook is bypassed entirely — no transcript scanning, no `discovery_validator_violation` events. Note: when the `discovery-validator:` block is present but omits the `enabled:` line, the parser conservatively resolves to `false` (only a literal `true` flips it) — the `true` default applies when the block is absent entirely. Always set `enabled` explicitly when adding this block. |
425
+ | `discovery-validator.enabled` | boolean | `false` | Master toggle. When `false` (or when the block is absent), the `SubagentStop` hook is bypassed entirely — no transcript scanning, no `discovery_validator_violation` events. Note: when the `discovery-validator:` block is present but omits the `enabled:` line, the parser conservatively resolves to `false` only a literal `true` enables the hook. Always set `enabled` explicitly when adding this block. |
379
426
 
380
427
  **Used by:** `hooks/post-subagent-discovery-validator.mjs`, `scripts/lib/config/discovery-validator.mjs` (`_parseDiscoveryValidator`). See `.claude/rules/parallel-sessions.md` § PSA-006.
381
428
 
@@ -1169,6 +1216,38 @@ Read by: `scripts/lib/config/custom-phases.mjs` (parser), `skills/session-end/SK
1169
1216
  - `archive-closed-prds` (#782, Epic #774) — `node scripts/archive-closed-prds.mjs --apply` — archives `docs/prd/` PRDs (defaults).
1170
1217
  - `archive-closed-plans` (#786) — `… --apply --prd-dir docs/plans --vault-subdir 01-projects/session-orchestrator/plans` — archives `docs/plans/` executable-plan artefacts of closed features/Epics. The plan's tracking `#NNN` (inline in the plan's `Source:` header, see `skills/write-executable-plan/`) is the anchor this phase reads; a plan with no `#NNN` is never archived (fail-closed `no-epic-ref`).
1171
1218
 
1219
+ ## Remote Hosts (#1160)
1220
+
1221
+ Opt-in declaration of ssh-reachable hosts a heavy wave role may be OFFLOADED to instead of shrinking the wave under local resource pressure. This key only DECLARES: it never probes a host, never dispatches, and never changes a wave by itself. Absent/empty ⇒ `[]` ⇒ every wave stays local, exactly as before.
1222
+
1223
+ **Parser gotcha:** like every other block-shaped Session Config key, the `remote-hosts:` key-line itself MUST NOT carry an inline comment — see § Parser Gotcha: No-Inline-Comment Block Headers (top of this file). A trailing `# comment` on that exact line means the parser never enters the block and `remote-hosts` silently resolves to `[]`.
1224
+
1225
+ ```yaml
1226
+ remote-hosts:
1227
+ - alias: m5 # required, SAFE slug; reaches argv as `-H <alias>`
1228
+ roles-allowed: [test, ui, perf] # subset of test|ui|perf (default: all three)
1229
+ repo-path: ~/Projects/Alice # optional; SAFE path; default null
1230
+ claude-path: ~/.local/bin/claude # optional; SAFE path; default null
1231
+ ```
1232
+
1233
+ | Field | Type | Default | Description |
1234
+ |-------|------|---------|-------------|
1235
+ | `remote-hosts` | list | `[]` | The declared hosts, in preference order — the gate takes the FIRST host that accepts the role and is witnessed ready. |
1236
+ | `alias` | string | — (required) | ssh destination as configured on this host. Must match `^[A-Za-z0-9._-]+$`; it reaches argv as `-H <alias>`. A record missing or failing this is dropped with a stderr WARN. |
1237
+ | `roles-allowed` | string[] | `[test, ui, perf]` | The `agent-mapping` roles this host accepts. Entries outside `test` / `ui` / `perf` are filtered with a WARN; a record whose list is empty after filtering is dropped. `impl`, `db`, `security`, `compliance` and `docs` work never leaves the local host. |
1238
+ | `repo-path` | string \| null | `null` | Checkout location on the remote host. SAFE-path validated (`^[A-Za-z0-9._~/-]+$`); an unsafe value drops the whole record with a WARN. |
1239
+ | `claude-path` | string \| null | `null` | `claude` binary location on the remote host. Same validation as `repo-path`. |
1240
+
1241
+ **Two enums, never conflated.** `roles-allowed` holds `agent-mapping` roles (`test`, `ui`, `perf`) — NOT wave roles (`Impl-Core`, `Quality`, …). The wave→role translation is `OFFLOADABLE_WAVE_ROLES` in `scripts/lib/wave-resource-gate.mjs`; a wave role absent from that map is local-only by default.
1242
+
1243
+ **Placement contract.** The gate applies its offload arm only after the HR-004 heavy-repo cap, and only when the resource verdict was `reduce` or `coordinator-direct`. It does NOT probe the network: the coordinator supplies a readiness witness (`remoteReady: { m5: true }`, or an async `probeFn`). With no witness, no host counts as ready and the decision stays local — the gate fails toward local, never toward an unverified host. A role in `NEVER_FOREIGN_ROLES` (`scripts/lib/wave-executor/foreign-dispatch.mjs`) is never offloaded regardless.
1244
+
1245
+ **agent-mapping interaction.** A declared alias is what an `agent-mapping` value of the form `<role>: ssh:<alias>` validates against; naming an undeclared host throws at parse time, naming the `ssh` channel with no target throws as for any other channel.
1246
+
1247
+ Read by: `scripts/lib/config/remote-hosts.mjs` (parser), `scripts/lib/config.mjs` (`ssh:` channel validation), `scripts/lib/wave-resource-gate.mjs` (placement).
1248
+
1249
+ See `skills/remote-offload/SKILL.md` for the wave-executor-side decision rule, the three offload channels, and how a declared alias here is what an `agent-mapping` `ssh:<alias>` value validates against.
1250
+
1172
1251
  ## Evolve Extra Sources (#638)
1173
1252
 
1174
1253
  Opt-in EXTRA learning sources for `/evolve`. A domain measurement (e.g. an eval-learn regression harness) runs OUT-OF-BAND and writes a sidecar JSON of regression flags; `/evolve` then READS each declared sidecar and emits a `domain-regression` learning candidate per flag that has persisted across ≥2 consecutive sessions. This is a strict **read-only consumption contract**: `/evolve` never runs the domain measurement — it only consumes the sidecar output. Absent/empty ⇒ `[]` ⇒ no extra sources are read; existing `/evolve` runs are unaffected.
@@ -1215,7 +1294,7 @@ reconcile:
1215
1294
  |-------|------|---------|-------------|
1216
1295
  | `reconcile.enabled` | boolean | `false` | Master toggle. When `false`, session-end Phase 3.6.8 is a silent no-op — the reconciliation engine never runs. When `true`, the engine evaluates learnings that meet `confidence-floor` and presents rule proposals to the operator via `AskUserQuestion`. Rules are never written without explicit operator approval. Epic #693 FA3 (#696). |
1217
1296
  | `reconcile.mode` | string (`warn` \| `off`) | `warn` | Advisory posture. `warn`: proposals surface in the session-end AUQ and the operator may accept or reject each one; accepted proposals are written to `targets`. `off`: advisory surface is suppressed entirely (equivalent to `enabled: false` for the AUQ step, but Phase 3.6.8 may still emit metrics). In both modes, rules are **never** auto-applied — every write is operator-AUQ-gated. |
1218
- | `reconcile.targets` | string[] | `["repo-local"]` | Where approved rules are written. `repo-local` (v1) maps to `.claude/rules/` in the current repository. This is the rule-write location — it is NOT issue-state or label sync. Future values may include `baseline` (global baseline rules) or `global` (cross-repo). |
1297
+ | `reconcile.targets` | string[] | `["repo-local"]` | Where approved rules are written — a **CLOSED enum**: `repo-local` → `<repoRoot>/.claude/rules/<slug>.md`; `baseline` (#1099) `<baselineRoot>/proposals/<slug>.md`, where `<baselineRoot>` is `plan-baseline-path` resolved host-locally (`SO_BASELINE_PATH` env > `owner.yaml` `paths.baseline-path` > committed value). Both targets are equally AUQ-gated and advisory — the `baseline` write drops files into `proposals/` in that checkout and commits nothing. This is the rule-write location — it is NOT issue-state or label sync. An **unknown member is DROPPED with a stderr WARN** naming it and the valid set (`scripts/lib/config/reconcile.mjs` `VALID_TARGETS`), never a throw: the parser is tolerant by contract and runs at session-start, where a throw would fail the session on a config typo. `global` (cross-repo) is documented-but-unimplemented and is deliberately NOT a member — admitting a value nothing implements is the same defect class as the unvalidated pass-through this enum replaced. When `baseline` is declared but its root is unresolvable on all three tiers, is still the committed `OVERRIDE-IN-…` placeholder, or is not absolute, `resolveEffectiveTargets()` (`scripts/lib/reconcile/engine.mjs`) drops it with ONE WARN **before** the approval AUQ, so the operator is never asked to approve a write to a destination that cannot exist; a root that does not exist on disk is refused at the writer and is never created. |
1219
1298
  | `reconcile.rule-expiry-days` | integer \| null | `null` | Optional override for the TTL stamped into each generated rule's `expires-at` frontmatter. **Default is `null`** — when null or absent, the engine uses per-type TTL (`deriveExpiresAt`, default 60 days). Setting this to a positive integer N forces a flat N-day expiry for all proposals in this repo, overriding per-type TTL. CRITICAL: the default must remain `null` to preserve per-type TTL behaviour; a non-null committed default would silently force flat expiry. |
1220
1299
  | `reconcile.confidence-floor` | float | `0.5` | Minimum learning confidence (0.0..1.0) required before a learning is eligible for a rule proposal. Learnings with `confidence < confidence-floor` are skipped by the engine. Bounds: `0.0 ≤ value ≤ 1.0`; out-of-range values silently fall back to `0.5`. Set to `0.0` to surface proposals for all learnings regardless of confidence. |
1221
1300
  | `reconcile.min-rule-days` | integer | `7` | Floor (in days) applied to the emitted rule's `expires-at` — issue #741.1. A learning close to its natural per-type TTL expiry could otherwise generate a rule that expires almost immediately ("born-dead"); `computeExpiresAt()` (`scripts/lib/reconcile/emitter.mjs`) floors the result at `now + min-rule-days` so an approved rule always has at least this many days of active life. Mirrors the hardcoded `MIN_RULE_DAYS_DEFAULT` constant in the emitter. Bounds: positive integer; non-finite or ≤0 values fall back to the default. |
@@ -1539,7 +1618,7 @@ When any condition is false, the full 5-wave flow runs as before — the check i
1539
1618
 
1540
1619
  ### What changes when express path is active
1541
1620
 
1542
- - **session-start:** After Phase 8 Q&A, emits `"Express path activated — N tasks, coordinator-direct, no inter-wave checks."` banner and executes tasks directly as the coordinator. session-plan is called but receives the express-path signal.
1621
+ - **session-start:** After Phase 8 Q&A, Phase 8.5 runs `node scripts/express-path.mjs` (the canonical caller — it makes the decision and records `orchestrator.express_path.evaluated` on refusal as well as activation, #1146), which emits the `"Express path activated — N tasks, coordinator-direct, no inter-wave checks."` banner. Tasks are then executed directly as the coordinator. session-plan is called but receives the express-path signal.
1543
1622
  - **session-plan:** Detects the banner in conversation context and emits a minimal 1-wave `coordinator-direct` plan (0 agents dispatched). Skips all role decomposition, complexity scoring, and wave splitting.
1544
1623
  - **STATE.md:** Activation is logged in the `## Deviations` section for traceability.
1545
1624
  - **Inter-wave checkpoints:** Skipped entirely — no Discovery → Impl-Core → Quality pipeline.
@@ -1563,6 +1642,7 @@ Set `express-path.enabled: false` when:
1563
1642
  | `housekeeping` | 1–3 | `true` | Yes | No — parallel agents required |
1564
1643
 
1565
1644
  **Related skills and files:**
1645
+ - `scripts/express-path.mjs` — the CLI Phase 8.5 runs; `scripts/lib/express-path.mjs` holds the decision + its `orchestrator.express_path.evaluated` record
1566
1646
  - `skills/session-start/SKILL.md` — Phase 8.5: Express Path Evaluation (activation logic + banner)
1567
1647
  - `skills/session-plan/SKILL.md` — Express Path Short-Circuit section (1-wave plan emission)
1568
1648
  - GitLab issue `#214` (foundation and codification)
@@ -66,7 +66,7 @@ mirror: github # auto-push to mirror after every commit
66
66
  cross-repos: [related-repo-1] # repos under ~/Projects/ to snapshot at session-start
67
67
  pencil: path/to/design.pen # design-code alignment input
68
68
  ecosystem-health: true # toggle health-endpoint probes
69
- health-endpoints:
69
+ health-endpoints: # nested block form — content-scoped parse, see reference doc (#1174)
70
70
  - { name: API, url: https://api.example.com/health }
71
71
  issue-limit: 50 # max issues fetched at session-start
72
72
  stale-branch-days: 7 # branch-age threshold for stale flag
@@ -381,6 +381,29 @@ Security: `command` and `review` reject shell metacharacters; records failing va
381
381
 
382
382
  Read by: `scripts/lib/config/custom-phases.mjs`, `skills/session-end/SKILL.md` Phase 2.5.
383
383
 
384
+ ## Remote Hosts (#1160)
385
+
386
+ Opt-in declaration of ssh-reachable hosts that heavy wave roles may be OFFLOADED to instead of shrinking the wave under local resource pressure. Declaring a host does not enable anything by itself: the wave resource gate only routes a wave to a host when the wave role is offloadable AND the coordinator supplies a readiness witness. Absent/empty ⇒ `[]` ⇒ everything runs locally, exactly as before.
387
+
388
+ ```yaml
389
+ remote-hosts:
390
+ - alias: m5 # required, SAFE slug ([A-Za-z0-9._-]); reaches argv as `-H <alias>`
391
+ roles-allowed: [test, ui, perf] # subset of test|ui|perf (default: all three)
392
+ repo-path: ~/Projects/Alice # optional SAFE path (default: null)
393
+ claude-path: ~/.local/bin/claude # optional SAFE path (default: null)
394
+ ```
395
+
396
+ Field semantics:
397
+ - **`alias`** — the ssh destination as configured on this host. Required; a record without it is dropped with a stderr WARN.
398
+ - **`roles-allowed`** — the `agent-mapping` roles this host accepts. `test`, `ui` and `perf` only — impl, db, security, compliance and docs work never leaves the local host. Unknown entries are filtered with a WARN; a record left with an empty list is dropped.
399
+ - **`repo-path` / `claude-path`** — where the checkout and the `claude` binary live on the remote host, when they differ from the defaults the dispatch adapter assumes.
400
+
401
+ A declared alias is also what an `agent-mapping` value of the form `<role>: ssh:<alias>` is validated against — naming an undeclared host is a parse error, not a runtime surprise.
402
+
403
+ The gate never probes the network. Without a readiness witness no host counts as ready and the decision stays local (fail toward local).
404
+
405
+ Read by: `scripts/lib/config/remote-hosts.mjs`, `scripts/lib/wave-resource-gate.mjs`.
406
+
384
407
  ## Evolve Extra Sources (#638)
385
408
 
386
409
  Opt-in EXTRA learning sources for `/evolve`. A `domain-regression` measurement (e.g. an eval-learn harness) runs OUT-OF-BAND and writes a sidecar JSON; `/evolve` then READS each declared sidecar and emits a `domain-regression` learning candidate per persistent regression flag. `/evolve` NEVER runs the measurement itself — this is a strict read-only consumption contract. Absent/empty ⇒ `[]` ⇒ no extra sources are read.
@@ -410,7 +433,7 @@ Opt-in config foundation for the FA3 (#696) advisory rule-proposal delivery at s
410
433
  reconcile:
411
434
  enabled: false # opt-in; FA3 reads this to gate session-end Phase 3.6.8
412
435
  mode: warn # off | warn — advisory only; rules NEVER auto-applied (#696)
413
- targets: [repo-local] # where approved rules are written; repo-local = .claude/rules/ in v1
436
+ targets: [repo-local] # closed enum: repo-local = .claude/rules/ · baseline = <plan-baseline-path>/proposals/ (#1099)
414
437
  rule-expiry-days: null # null = per-type TTL (default 60d); set positive integer for flat override (#697)
415
438
  confidence-floor: 0.5 # float 0.0..1.0 — min learning confidence for rule proposal eligibility
416
439
  min-rule-days: 7 # #741.1 — floor for emitted rule expires-at: max(derived, now + N days); prevents born-dead rules
@@ -425,16 +448,17 @@ Field semantics:
425
448
  - **`confidence-floor`** — learnings below this confidence level are not eligible for rule proposals. Default 0.5 matches `memory.proposals.confidence-floor`.
426
449
  - **`min-rule-days`** — floor window (days) applied to a proposed rule's `expires-at` so a near-dead or already-elapsed natural expiry never produces a born-dead rule. Positive integer; malformed or ≤0 falls back to 7 (issue #741.1).
427
450
  - **`min-insight-chars`** — opt-in minimum insight length gating the eligibility placeholder-insight check; rejects a learning whose trimmed insight is shorter than N characters before rule conversion. Integer ≥ 0; `0` disables the check (issue #741.2).
451
+ - **`targets`** — CLOSED enum, `repo-local` | `baseline` (issue #1099). `repo-local` writes `<repoRoot>/.claude/rules/<slug>.md`; `baseline` writes `<baselineRoot>/proposals/<slug>.md`, where `<baselineRoot>` is `plan-baseline-path` resolved host-locally (`SO_BASELINE_PATH` env > `owner.yaml` `paths.baseline-path` > committed value). Both are AUQ-gated and advisory — the baseline write commits nothing in that repo. An unknown member is dropped with a stderr WARN naming it and the valid set; `global` is documented-but-unimplemented and is NOT a member. A `baseline` target whose root is unresolvable, is still an `OVERRIDE-IN-…` placeholder, is not absolute, or does not exist on disk degrades to a no-op with a WARN — the root is never created.
428
452
 
429
453
  Read by: `scripts/lib/config/reconcile.mjs` (parser), `skills/session-end/SKILL.md` Phase 3.6.8 (FA3 delivery). FA2 engine: `scripts/lib/reconcile/`.
430
454
 
431
455
  ## Discovery-Validator (PSA-006 Enforcement)
432
456
 
433
- Non-blocking `SubagentStop` hook that mechanically enforces PSA-006: distributional claims ("N of M", "100% of", "all N", "no remaining", "every X", "none of") in a subagent's transcript tail must carry an adjacent fenced grep/rg/find transcript. When a claim lacks one, the hook records a `discovery_validator_violation` event in `.orchestrator/metrics/events.jsonl` and emits a stderr WARN. v1 is log + warn only (exit 0 always — never blocks an agent) — a blocking hard-gate is reserved for a future iteration. ON by default (flip risk is near-zero; generates real telemetry). Issue #567.
457
+ Non-blocking `SubagentStop` hook that mechanically enforces PSA-006: distributional claims ("N of M", "100% of", "all N", "no remaining", "every X", "none of") in a subagent's transcript tail must carry an adjacent fenced grep/rg/find transcript. When a claim lacks one, the hook records a `discovery_validator_violation` event in `.orchestrator/metrics/events.jsonl` and emits a stderr WARN. v1 is log + warn only (exit 0 always — never blocks an agent) — a blocking hard-gate is reserved for a future iteration. OFF by default (opt-in). Issue #567; #690 flip reverted 2026-09-02 after fleet measurement.
434
458
 
435
459
  ```yaml
436
460
  discovery-validator:
437
- enabled: true # on by default; log+warn-only, exit-0-always set false to silence
461
+ enabled: true # opt-in; hook is off unless this block sets it
438
462
  ```
439
463
 
440
464
  Read by: `scripts/lib/config/discovery-validator.mjs`, `hooks/post-subagent-discovery-validator.mjs`.
@@ -604,9 +628,9 @@ webhooks:
604
628
  url: https://gitlab.example.com/hooks/pipeline
605
629
  ```
606
630
 
607
- Read by: `scripts/lib/webhook-url.mjs`, hooks that emit events (`hooks/on-stop.mjs`, `hooks/on-subagent-stop.mjs`).
631
+ Measured: `scripts/lib/webhook-url.mjs` (`resolveWebhookUrl`) is the only reader of this `webhooks:` block, and it currently has **zero callers repo-wide** (`grep -rn "webhook-url" scripts/ hooks/` outside itself and one exemption comment in `check-unwired-features.mjs`) — the block is unreachable at HEAD; follow-up issue pending.
608
632
 
609
- The internal Clank Event Bus uses two separate env vars (`CLANK_EVENT_SECRET`, `CLANK_EVENT_URL`) both required for the fire-and-forget POST.
633
+ What actually fires a webhook today is a **separate** mechanism: `scripts/lib/events.mjs`'s `emitEvent()` reads `CLANK_EVENT_SECRET` + `CLANK_EVENT_URL` directly from the environment (never from this Session Config block) and, when both are set, fire-and-forget POSTs every emitted event to the internal Clank Event Bus. Every hook that calls `emitEvent()` — which is most of `hooks/` — participates in that path; none of them reads `webhooks:` here.
610
634
 
611
635
  ## Hook Runtime Profile (env-only, not config)
612
636
 
@@ -816,6 +840,13 @@ custom-phases:
816
840
  mode: hard # warn | hard | off (default: warn)
817
841
  review: docs/eval/last-run.md # optional SAFE path read after the command (default: null)
818
842
 
843
+ # Remote hosts — ssh-reachable offload targets for heavy roles (#1160)
844
+ remote-hosts:
845
+ - alias: m5 # required, SAFE slug; reaches argv as `-H <alias>`
846
+ roles-allowed: [test, ui, perf] # subset of test|ui|perf (default: all three)
847
+ repo-path: ~/Projects/Alice # optional SAFE path (default: null)
848
+ claude-path: ~/.local/bin/claude # optional SAFE path (default: null)
849
+
819
850
  # Evolve extra-sources — opt-in EXTRA /evolve learning sources (#638)
820
851
  evolve:
821
852
  extra-sources:
@@ -835,7 +866,7 @@ reconcile:
835
866
 
836
867
  # Discovery-validator — PSA-006 enforcement (#567)
837
868
  discovery-validator:
838
- enabled: true
869
+ enabled: true # opt-in; hook is off unless this block sets it
839
870
 
840
871
  # Dialectic-Deriver (#506)
841
872
  dialectic:
@@ -58,29 +58,30 @@ the measurement recipe — see [Reproduce it yourself](#reproduce-it-yourself).
58
58
 
59
59
  ---
60
60
 
61
- ## In-repo test suite — the "10,000+ tests" badge
61
+ ## In-repo test suite — the "15,000+ tests" badge
62
62
 
63
63
  Distinct from the private-corpus figures above, the README **Tests** badge and the
64
- "10,000+ vitest tests run on every commit" line count **this repository's own test
64
+ "15,000+ vitest tests run on every commit" line count **this repository's own test
65
65
  suite** — a **public, CI-verifiable** number, not a maintainer-reported aggregate.
66
66
 
67
67
  | Claim | What it counts | How measured | As-of |
68
68
  | --- | --- | --- | --- |
69
- | 10,000+ vitest tests | executed test cases across `tests/**/*.test.mjs` | `npm test` prints the exact runtime total; the static floor is countable without running the suite (below) | 2026-06 |
69
+ | 15,000+ vitest tests | executed test cases across `tests/**/*.test.mjs` | `npm test` prints the exact runtime total; the static floor is countable without running the suite (below) | 2026-09 |
70
70
 
71
71
  Both numbers reproduce in a fresh checkout:
72
72
 
73
73
  ```bash
74
- find tests -name '*.test.mjs' | wc -l # test files -> 475
75
- grep -rohE '\b(it|test)\(' tests | wc -l # static test defs -> ~9,871
76
- grep -rohE '\b(it|test|describe)\.each\b' tests | wc -l # parameterized blocks -> 93
74
+ find tests -name '*.test.mjs' | wc -l # test files -> 637
75
+ grep -rohE '\b(it|test)\(' tests | wc -l # static test defs -> ~13,361
76
+ grep -rohE '\b(it|test|describe)\.each\b' tests | wc -l # parameterized blocks -> 440
77
77
  ```
78
78
 
79
- The static `it(` / `test(` count (~9,871 across 475 files) is a **floor**: the 93
79
+ The static `it(` / `test(` count (~13,361 across 637 files) is a **floor**: the 440
80
80
  `it.each` / `test.each` parameterized blocks each expand to multiple executed cases
81
- at runtime, so the **case count vitest reports on `npm test` is 10,000+**. Unlike the
82
- private-corpus figures above, this one is fully auditable run `npm test` in this
83
- checkout and read vitest's summary line.
81
+ at runtime, so the **case count vitest reports on `npm test` is 15,000+** (measured
82
+ 15,869/0 on the local Full Gate at HEAD, 2026-09-02). Unlike the private-corpus
83
+ figures above, this one is fully auditable — run `npm test` in this checkout and
84
+ read vitest's summary line.
84
85
 
85
86
  ## Methodology
86
87
 
package/docs/telemetry.md CHANGED
@@ -43,7 +43,36 @@ projection unit test enforces the drop of any non-whitelisted input field.
43
43
  | `session_type` | One of `housekeeping`, `feature`, `deep`, `other`. |
44
44
  | `duration_bucket` | One of `<15m`, `15-60m`, `1-3h`, `>3h` — a coarse bucket, never an exact duration. |
45
45
  | `skills[]` | Names of invoked skills, filtered against the shipped plugin roster — any name not in that roster becomes `"other"`. |
46
- | `commands[]` | Same filtering rule as `skills[]`. |
46
+ | `commands[]` | Names of invoked slash-commands, same filtering rule as `skills[]`. |
47
+
48
+ ### How a name lands in `skills[]` or `commands[]`
49
+
50
+ Both buckets are fed from one local ledger of invocations
51
+ (`.orchestrator/metrics/skill-invocations.jsonl`), so a single classification
52
+ rule decides which bucket a name reaches — and it is deliberately biased
53
+ towards anonymizing rather than towards attributing:
54
+
55
+ - Shipped **skills** are recorded plugin-prefixed
56
+ (`session-orchestrator:session-end`); shipped **commands** are recorded bare
57
+ (`session`). The Skill tool surfaces a slash-command that has no backing
58
+ `skills/` directory under the *prefixed* form too, so a prefixed name whose
59
+ bare form is a shipped command is reported in `commands[]` under that bare
60
+ name.
61
+ - A name is only ever reported as one of our commands when it carries the
62
+ plugin prefix. A **bare** name is never credited to a command, even when it
63
+ collides with one of our command names — a third-party or personal skill
64
+ invoked bare as `test` would otherwise be reported as our `/test` command.
65
+ Bare unknown names take the skills path and are reduced to `"other"`. The one
66
+ exception is a name arriving in the ledger's `.command` **field**: that field
67
+ is itself the "this is one of ours" provenance signal a bare `.skill` arrival
68
+ lacks, so `buildUsagePing` prefixes every `.command` value before
69
+ classification (`scripts/lib/telemetry/schema.mjs`). Without that step the
70
+ `.command` producer would be wired but dead — every record it writes would
71
+ silently become `"other"`.
72
+ - On a spelling collision (`memory-cleanup` exists as both a skill and a
73
+ command) the skill roster wins, so exactly one bucket is credited. Counting
74
+ distinct surfaces across `skills[]` and `commands[]` therefore never
75
+ double-counts a single one.
47
76
 
48
77
  ## What we never collect
49
78
 
@@ -84,6 +113,16 @@ Precedence, highest wins:
84
113
  headless or CI invocation never shows this prompt and never sends
85
114
  telemetry, regardless of any saved state.
86
115
 
116
+ The prompt is triggered **mechanically**: `hooks/on-session-start.mjs` calls
117
+ `resolveConsent()` on every session start and, only when no decision is on
118
+ record and the run is not CI, injects a one-line instruction into the session
119
+ via `hookSpecificOutput.additionalContext` — riding the same single stdout
120
+ envelope as the host banner. The skill phase that describes the question
121
+ (`skills/session-start/SKILL.md` § Phase 6.8) is the wording, not the trigger.
122
+ The CI check is `isCiEnv()`, deliberately **not** `isHeadless()`: a hook's
123
+ stdout is always a pipe, so `isHeadless()` would answer "headless" every time
124
+ and the prompt could never appear (#1138).
125
+
87
126
  If the consent file is corrupt or unreadable, the client fails **closed**:
88
127
  telemetry state degrades to "no consent" (nothing sent) rather than
89
128
  guessing, with a one-line stderr hint pointing at the CLI below.
@@ -129,6 +168,18 @@ unreachable, the batch queues locally (bounded size, oldest entries dropped
129
168
  first) and retries later. Telemetry never blocks or slows down a session
130
169
  beyond that short timeout budget.
131
170
 
171
+ **When a send is attempted.** `hooks/on-session-end.mjs` calls `flush()` at the
172
+ end of every session teardown — including sessions that never run `/close`. The
173
+ `skills/session-end/SKILL.md` § Phase 3.45 description documents the behaviour;
174
+ the hook is what fires it. Each attempt writes one
175
+ `orchestrator.telemetry.flush` breadcrumb (`{outcome, reason}` only — no
176
+ payload, no `anon_id`) to the repo's local `events.jsonl`, so the send rate is
177
+ measurable rather than assumed. A second, `Skill`-triggered daily fallback
178
+ sends when more than 24h have passed since the last successful flush AND either
179
+ the offline queue is non-empty or a session has completed since — the latter
180
+ clause is what lets the fallback originate a ping instead of only retrying a
181
+ failed one (#1138).
182
+
132
183
  ## Retention
133
184
 
134
185
  - **Raw records:** kept 24 months, then pruned. The retention window exists