session-orchestrator 3.16.0 → 3.19.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 (220) 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/rules/030-wave-execution.mdc +17 -1
  5. package/CHANGELOG.md +130 -412
  6. package/README.md +17 -12
  7. package/SECURITY.md +190 -27
  8. package/agents/AGENTS.md +20 -3
  9. package/agents/code-implementer.md +6 -6
  10. package/agents/db-specialist.md +1 -1
  11. package/agents/qa-strategist.md +31 -6
  12. package/agents/schemas/qa-strategist.schema.json +27 -0
  13. package/agents/schemas/test-writer.schema.json +60 -2
  14. package/agents/security-reviewer.md +1 -1
  15. package/agents/session-reviewer.md +1 -1
  16. package/agents/test-writer.md +29 -10
  17. package/agents/ui-developer.md +1 -1
  18. package/commands/contract-version-bump.md +28 -0
  19. package/commands/portfolio.md +1 -1
  20. package/docs/README.md +2 -1
  21. package/docs/USER-GUIDE.md +8 -3
  22. package/docs/ci-setup.md +121 -7
  23. package/docs/codex-setup.md +1 -1
  24. package/docs/components.md +7 -7
  25. package/docs/cursor-setup.md +22 -9
  26. package/docs/events-schema.md +5 -1
  27. package/docs/instruction-delivery.md +444 -0
  28. package/docs/pi-setup.md +1 -1
  29. package/docs/rule-authoring.md +58 -9
  30. package/docs/session-config-reference.md +306 -6
  31. package/docs/session-config-template.md +66 -3
  32. package/docs/telemetry/telemetry-claims.md +204 -0
  33. package/docs/telemetry.md +158 -0
  34. package/hooks/_lib/guard-source-loader.mjs +467 -0
  35. package/hooks/_lib/lock-bootstrap.mjs +21 -0
  36. package/hooks/_lib/vcs-create-matcher.mjs +119 -0
  37. package/hooks/config-protection.mjs +0 -0
  38. package/hooks/enforce-commands.mjs +10 -2
  39. package/hooks/hooks-codex.json +1 -1
  40. package/hooks/hooks-cursor.json +11 -2
  41. package/hooks/hooks-pi.json +10 -0
  42. package/hooks/hooks.json +21 -1
  43. package/hooks/on-session-end.mjs +178 -18
  44. package/hooks/on-session-start.mjs +23 -0
  45. package/hooks/post-bash-write-verify.mjs +977 -0
  46. package/hooks/post-subagent-discovery-validator.mjs +256 -41
  47. package/hooks/pre-bash-destructive-guard.mjs +525 -160
  48. package/hooks/pre-bash-issue-budget.mjs +167 -0
  49. package/hooks/pre-bash-sessions-ledger-guard.mjs +627 -0
  50. package/hooks/pre-bash-templates-first.mjs +96 -63
  51. package/hooks/skill-invocation-telemetry.mjs +109 -10
  52. package/hooks/subagent-telemetry.mjs +527 -37
  53. package/package.json +16 -3
  54. package/pi/prompts/contract-version-bump.md +12 -0
  55. package/rules/README.md +32 -0
  56. package/scripts/archive-closed-prds.mjs +12 -22
  57. package/scripts/autopilot-multi.mjs +103 -20
  58. package/scripts/backfill-abandoned-sessions.mjs +160 -4
  59. package/scripts/check-doc-consistency.sh +17 -1
  60. package/scripts/compute-grounding-injection.sh +18 -3
  61. package/scripts/dialectic-deriver.mjs +7 -2
  62. package/scripts/eval-session.mjs +50 -9
  63. package/scripts/fleet-instruction-scan.mjs +141 -0
  64. package/scripts/lib/auto-dialectic.mjs +11 -2
  65. package/scripts/lib/auto-dream.mjs +16 -5
  66. package/scripts/lib/autopilot/mr-draft.mjs +31 -1
  67. package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
  68. package/scripts/lib/backlog-scan.mjs +39 -6
  69. package/scripts/lib/blocked-commands-policy.mjs +340 -0
  70. package/scripts/lib/build-live-signals.mjs +7 -4
  71. package/scripts/lib/ci-status-banner.mjs +75 -12
  72. package/scripts/lib/claude-md-budget-lint.mjs +283 -34
  73. package/scripts/lib/command-blocker.mjs +1013 -58
  74. package/scripts/lib/config/config-protection.mjs +2 -1
  75. package/scripts/lib/config/context-coverage.mjs +82 -0
  76. package/scripts/lib/config/drift-check.mjs +9 -1
  77. package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
  78. package/scripts/lib/config/issue-budget.mjs +123 -0
  79. package/scripts/lib/config/moc-staleness.mjs +98 -0
  80. package/scripts/lib/config/reconcile.mjs +21 -0
  81. package/scripts/lib/config/section-extractor.mjs +121 -1
  82. package/scripts/lib/config/worktree-orphans.mjs +138 -0
  83. package/scripts/lib/config-schema.mjs +23 -3
  84. package/scripts/lib/config.mjs +32 -0
  85. package/scripts/lib/context-coverage-banner.mjs +223 -0
  86. package/scripts/lib/convergence-monitor.mjs +49 -3
  87. package/scripts/lib/description-surface.mjs +535 -0
  88. package/scripts/lib/dispatcher/enumerate.mjs +136 -30
  89. package/scripts/lib/dispatcher/rank.mjs +22 -8
  90. package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
  91. package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
  92. package/scripts/lib/eval/engine.mjs +47 -5
  93. package/scripts/lib/events.mjs +59 -7
  94. package/scripts/lib/evolve/autonomy-verdict.mjs +5 -0
  95. package/scripts/lib/evolve/autopilot-effectiveness.mjs +54 -7
  96. package/scripts/lib/gates/gate-full.mjs +15 -3
  97. package/scripts/lib/gates/gate-helpers.mjs +132 -6
  98. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
  99. package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
  100. package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
  101. package/scripts/lib/handover-gate.mjs +7 -3
  102. package/scripts/lib/harness-audit/categories/category4.mjs +22 -5
  103. package/scripts/lib/instruction-budget-guard.mjs +402 -51
  104. package/scripts/lib/io.mjs +345 -10
  105. package/scripts/lib/issue-budget.mjs +269 -0
  106. package/scripts/lib/issue-close-strip-labels.mjs +39 -9
  107. package/scripts/lib/label-scope.mjs +47 -0
  108. package/scripts/lib/learnings/schema.mjs +43 -3
  109. package/scripts/lib/lock-reaper.mjs +1 -2
  110. package/scripts/lib/memory-proposals/schema.mjs +36 -1
  111. package/scripts/lib/moc-staleness-banner.mjs +267 -0
  112. package/scripts/lib/peer-discovery.mjs +645 -0
  113. package/scripts/lib/pi-hook-bridge.mjs +146 -17
  114. package/scripts/lib/product-repo-detect.mjs +9 -8
  115. package/scripts/lib/project-hygiene.mjs +432 -0
  116. package/scripts/lib/quality-gate.mjs +167 -0
  117. package/scripts/lib/recommendations-v0.mjs +1 -1
  118. package/scripts/lib/reconcile/eligibility.mjs +1 -1
  119. package/scripts/lib/reconcile/emitter.mjs +23 -4
  120. package/scripts/lib/reconcile/engine.mjs +147 -39
  121. package/scripts/lib/reconcile/idempotency.mjs +114 -14
  122. package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
  123. package/scripts/lib/resource-probe/evaluate.mjs +70 -4
  124. package/scripts/lib/resource-probe.mjs +19 -0
  125. package/scripts/lib/rule-loader.mjs +6 -0
  126. package/scripts/lib/scope-baseline.mjs +564 -0
  127. package/scripts/lib/scope-gate.mjs +399 -98
  128. package/scripts/lib/session-close-backfill.mjs +61 -6
  129. package/scripts/lib/session-end/phase-skip.mjs +1 -0
  130. package/scripts/lib/session-end/worktree-orphan-sweep.mjs +252 -0
  131. package/scripts/lib/session-id.mjs +221 -41
  132. package/scripts/lib/session-lock.mjs +304 -6
  133. package/scripts/lib/session-schema/constants.mjs +22 -3
  134. package/scripts/lib/session-schema/filters.mjs +88 -0
  135. package/scripts/lib/session-schema/validator.mjs +16 -0
  136. package/scripts/lib/session-schema.mjs +1 -0
  137. package/scripts/lib/sessions-integrity-banner.mjs +294 -0
  138. package/scripts/lib/sessions-staleness-banner.mjs +121 -12
  139. package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
  140. package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
  141. package/scripts/lib/skill-health/join.mjs +35 -9
  142. package/scripts/lib/spiral-carryover.mjs +142 -30
  143. package/scripts/lib/state-md/mission-status.mjs +53 -3
  144. package/scripts/lib/subagents-schema.mjs +43 -9
  145. package/scripts/lib/telemetry/anon-id.mjs +141 -0
  146. package/scripts/lib/telemetry/consent.mjs +299 -0
  147. package/scripts/lib/telemetry/paths.mjs +27 -0
  148. package/scripts/lib/telemetry/queue.mjs +287 -0
  149. package/scripts/lib/telemetry/schema.mjs +384 -0
  150. package/scripts/lib/telemetry/sync.mjs +312 -0
  151. package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
  152. package/scripts/lib/tests-src-ratio.mjs +484 -0
  153. package/scripts/lib/validate/check-agents.mjs +56 -0
  154. package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
  155. package/scripts/lib/validate/check-rules.mjs +217 -35
  156. package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
  157. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  158. package/scripts/lib/validate-vendored-rules.mjs +10 -2
  159. package/scripts/lib/vault-archive.mjs +17 -2
  160. package/scripts/lib/vault-backfill/glab.mjs +8 -0
  161. package/scripts/lib/vault-mirror/process.mjs +30 -0
  162. package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
  163. package/scripts/lib/vault-status/board-writer.mjs +63 -5
  164. package/scripts/lib/vault-status/narrative-mirror.mjs +13 -7
  165. package/scripts/lib/vcs-repo-spec.mjs +362 -0
  166. package/scripts/lib/wave-resource-gate.mjs +115 -11
  167. package/scripts/lib/worktree/listing.mjs +44 -7
  168. package/scripts/mcp-server.sh +32 -6
  169. package/scripts/measure-context-overhead.sh +151 -0
  170. package/scripts/memory-propose.mjs +72 -9
  171. package/scripts/print-applicable-rules.mjs +51 -12
  172. package/scripts/release.mjs +534 -0
  173. package/scripts/run-quality-gate.mjs +123 -5
  174. package/scripts/telemetry.mjs +250 -0
  175. package/scripts/validate-wave-scope.mjs +182 -17
  176. package/scripts/vault-integration-watcher.mjs +32 -10
  177. package/skills/_shared/config-reading.md +2 -2
  178. package/skills/bootstrap/fast-template.md +1 -1
  179. package/skills/claude-md-drift-check/checker.mjs +145 -28
  180. package/skills/contract-version-bump/SKILL.md +219 -0
  181. package/skills/discovery/SKILL.md +4 -4
  182. package/skills/discovery/issue-templates.md +11 -11
  183. package/skills/discovery/probes-audit.md +1 -1
  184. package/skills/discovery/probes-feature.md +1 -1
  185. package/skills/discovery/probes-session.md +26 -5
  186. package/skills/ecosystem-health/SKILL.md +1 -1
  187. package/skills/ecosystem-health/wizard.md +4 -4
  188. package/skills/evolve/SKILL.md +1 -0
  189. package/skills/gitlab-ops/SKILL.md +20 -12
  190. package/skills/gitlab-portfolio/SKILL.md +2 -2
  191. package/skills/hook-development/SKILL.md +1 -1
  192. package/skills/mode-selector/SKILL.md +1 -1
  193. package/skills/npm-publish/SKILL.md +97 -0
  194. package/skills/plan/SKILL.md +5 -5
  195. package/skills/plan/mode-feature.md +4 -4
  196. package/skills/plan/mode-new.md +10 -10
  197. package/skills/plan/mode-retro.md +1 -1
  198. package/skills/quality-gates/SKILL.md +1 -1
  199. package/skills/reconcile/SKILL.md +21 -4
  200. package/skills/session-end/SKILL.md +108 -14
  201. package/skills/session-end/discovery-scan.md +4 -2
  202. package/skills/session-end/drift-operations.md +4 -4
  203. package/skills/session-end/metrics-collection.md +13 -0
  204. package/skills/session-end/phase-3-2-docs-verification.md +1 -1
  205. package/skills/session-end/phase-3-6-tail.md +2 -1
  206. package/skills/session-end/plan-verification.md +5 -2
  207. package/skills/session-end/vault-operations.md +1 -1
  208. package/skills/session-end/verification-checklist.md +1 -1
  209. package/skills/session-plan/SKILL.md +6 -2
  210. package/skills/session-plan/wave-template.md +2 -0
  211. package/skills/session-start/SKILL.md +149 -7
  212. package/skills/session-start/phase-4-5-resource-health.md +15 -2
  213. package/skills/test-runner/SKILL.md +2 -2
  214. package/skills/vault-sync/SKILL.md +1 -1
  215. package/skills/vault-sync/package-lock.json +3 -3
  216. package/skills/vault-sync/validator.mjs +228 -40
  217. package/skills/wave-executor/SKILL.md +5 -2
  218. package/skills/wave-executor/circuit-breaker.md +2 -0
  219. package/skills/wave-executor/wave-loop.md +163 -10
  220. package/templates/_shared/loop.md +4 -4
@@ -32,6 +32,35 @@ Validation runs automatically via `scripts/parse-config.mjs` → `scripts/valida
32
32
 
33
33
  Bypass via `SO_SKIP_CONFIG_VALIDATION=1`. Missing fields can be patched into an existing config file via `/bootstrap --retroactive`.
34
34
 
35
+ ## Parser Gotcha: No-Inline-Comment Block Headers
36
+
37
+ **General contract, applies to every nested (block-shaped) Session Config key, not just the handful annotated below.** A top-level block key like `eval:`, `custom-phases:`, `moc-staleness:`, `context-coverage:`, or `worktree-orphans:` is opened by the shared bold-tolerant matcher `matchBlockHeader(line, key)` (`scripts/lib/config/block-header.mjs`) rather than by a per-key regex. As of the #830 generalisation, **38 files** under `scripts/lib/config/` import it — 37 block-shaped parsers (`auto-dream`, `broken-window`, `cold-start`, `config-protection`, `context-coverage`, `cross-repo`, `custom-phases`, `dialectic`, `discovery-validator`, `dispatcher-autonomy`, `dispatcher-autonomy-capture`, `docs-orchestrator`, `docs-staleness`, `drift-check`, `eval`, `events-rotation`, `evolve`, `frontend-slop-hook`, `gitlab-portfolio`, `handover-gate`, `loop-guard`, `memory`, `moc-staleness`, `persona-gate-wave`, `reconcile`, `skill-evolution`, `slopcheck`, `state-md-lock`, `templates-first`, `test`, `vault-integration`, `vault-mirror-quality`, `vault-staleness`, `vault-sync`, `verification-auto-fix`, `wave-reviewers`, `worktree-orphans`) plus `block-header.mjs` itself — confirmed via `grep -rln "matchBlockHeader" scripts/lib/config/ | wc -l` → `38`.
38
+
39
+ **The matcher's accept/reject contract** (`matchBlockHeader(line, key)`, equivalent to the regex `^(?:-\s+)?(?:\*\*)?<key>:(?:\*\*)?\s*$`):
40
+
41
+ | Form | Matches? |
42
+ |---|---|
43
+ | `key:` | ✅ plain header |
44
+ | `- key:` | ✅ dash-bullet header |
45
+ | `**key:**` | ✅ bold header |
46
+ | `- **key:**` | ✅ dash-bullet + bold header |
47
+ | `key: value` | ❌ a header carrying a value is not a block-opener |
48
+ | `key: # comment` | ❌ **the load-bearing gotcha** — a trailing inline comment on the header line itself |
49
+ | ` key:` (indented) | ❌ this is a sub-key of some other block, not a top-level header |
50
+ | `other-key:` | ❌ different key |
51
+
52
+ **Why the inline-comment case matters more than it looks.** When a block-header line carries a trailing `# comment`, the matcher returns `false` for that line — the block-open scan never flips into "in-block" mode, so **every field under that key silently falls back to its default**. There is no error, no warning, no stderr output anywhere in the pipeline. The only symptom is a repo's opt-in feature quietly behaving as if it were never configured — exactly the class of bug this gotcha exists to prevent. Sub-key lines (`enabled:`, `mode:`, …) are unaffected — inline comments on THOSE lines parse fine; only the block-opener line itself is fragile.
53
+
54
+ **The five keys carrying an explicit source-level warning comment today** (a strict subset of the 37 — every other block-shaped key has the identical failure mode, just without an inline reminder):
55
+
56
+ - `eval:` — see § Eval (#803) below for the full parser-gotcha paragraph (learning confidence 0.9).
57
+ - `custom-phases:` — see § Custom Phases (#637) below.
58
+ - `moc-staleness:` — see § MOC Staleness (#831/B2) below.
59
+ - `context-coverage:` — see § Context Coverage (#831/B4) below.
60
+ - `worktree-orphans:` — see § Worktree Orphans (#831/B5) below.
61
+
62
+ **Stale-citation note:** an older code comment on the `custom-phases:` key in this repo's own `CLAUDE.md` cites a per-key regex (`/^custom-phases:\s*$/`) as the mechanism. That citation predates the #830 generalisation — `custom-phases.mjs` (like all 37 consumers) now delegates to the shared `matchBlockHeader(line, 'custom-phases')`, which is strictly MORE tolerant than the old per-key regex (it additionally accepts the dash-bullet and bold-bullet renderings). The no-inline-comment failure mode is unchanged; only the underlying mechanism moved from a bespoke regex to the shared helper. Treat any remaining per-key regex citation in prose (including in this file, prior to this section's introduction) as documentation of the OLD mechanism — the general contract above is current.
63
+
35
64
  ## Policy Files
36
65
 
37
66
  Some sub-configs live in dedicated policy files under `.orchestrator/policy/`:
@@ -67,6 +96,24 @@ Some sub-configs live in dedicated policy files under `.orchestrator/policy/`:
67
96
  | `stale-branch-days` | integer | `7` | Days of inactivity before a branch is flagged as stale. |
68
97
  | `stale-issue-days` | integer | `30` | Days without progress before an issue is flagged for triage. |
69
98
 
99
+ ## Auto-Skill Dispatch (#337)
100
+
101
+ Opt-in phrase-match meta-skill (`skills/using-orchestrator/SKILL.md`) that inspects the user's first message for implicit slash-command intent (e.g. "plane neues Projekt", "run discovery on the backlog") and dispatches to the highest-confidence matching entry-point skill via the `Skill` tool — before that skill's own Phase 1 — once the bootstrap gate is open. Off by default; when `false` (or absent) the meta-skill returns immediately with zero reads, zero logging, zero side effects, and every calling skill behaves exactly as if it was never invoked.
102
+
103
+ This is a top-level SCALAR field — not a nested object — matching `vcs`, unlike most of the other opt-in features in this reference which live under a nested block.
104
+
105
+ ```yaml
106
+ auto-skill-dispatch: false # opt-in; default false preserves existing behavior
107
+ ```
108
+
109
+ | Field | Type | Default | Description |
110
+ |-------|------|---------|-------------|
111
+ | `auto-skill-dispatch` | boolean | `false` | Master toggle. When `false`, `skills/using-orchestrator/SKILL.md` is a no-op — no message inspection, no phrase-map scoring, no dispatch. When `true`, every entry-point skill (per `skills/_shared/bootstrap-gate.md`) invokes the meta-skill once before its own Phase 1. |
112
+
113
+ **Dispatch algorithm summary:** the meta-skill scores the user's first message against a bilingual (EN/DE) phrase map covering `/plan {new,feature,retro}`, `/session {housekeeping,feature,deep}`, `/discovery`, `/evolve`, `/close`, `/bootstrap`. Confidence tiers: exact slash-command match `0.95`, exact natural-language match `0.90`, substring match `0.60`, semantic near-miss `0.40`. Only scores ≥ `0.85` trigger a dispatch. When the top two candidates both score ≥ `0.85` with a delta < `0.15`, the meta-skill disambiguates via `AskUserQuestion` (per `.claude/rules/ask-via-tool.md` AUQ-003) rather than silently picking one. Below `0.85`, or with no candidates, the meta-skill returns silently and the calling skill's own routing logic takes over — the original user message is never rewritten before being passed to the dispatch target.
114
+
115
+ **Used by:** `skills/using-orchestrator/SKILL.md` (the dispatch algorithm, full phrase map, and confidence-scoring table), `skills/_shared/bootstrap-gate.md` (the opt-in call site — invoked once per entry-point skill when the gate is open). Note: unlike most Session Config booleans documented in this file, `auto-skill-dispatch` has no dedicated parser module under `scripts/lib/config/` — it is read directly from the raw Session Config text by the calling skill's own Phase logic, the same pattern used for prose-level opt-in flags that never reach `scripts/parse-config.mjs`'s structured JSON output.
116
+
70
117
  ## Templates-First Hook (#519)
71
118
 
72
119
  Opt-out configuration for the PreToolUse `Bash` hook that blocks `gh|glab pr|mr|issue create` calls unless the matching repo template (`.github/PULL_REQUEST_TEMPLATE*`, `.github/ISSUE_TEMPLATE*`, `.gitlab/merge_request_templates/*`, `.gitlab/issue_templates/*`) was Read in the current session. Per-session acknowledgement is tracked in `.orchestrator/runtime/templates-acknowledged.json` — once a template is Read (or `/templates-ack` is invoked), the hook stops blocking for the remainder of the session. Mechanical replacement for gitlab-ops template advice. PRD gsd Pattern 3 / issue #519.
@@ -120,13 +167,36 @@ verification-auto-fix:
120
167
 
121
168
  | Field | Type | Default | Description |
122
169
  |-------|------|---------|-------------|
123
- | `discovery-on-close` | boolean | session-type aware: `false` for `housekeeping`, `true` for `feature`/`deep` (#264) | Run discovery probes automatically during `/close`. Default is `false` for housekeeping sessions and `true` for feature and deep sessions when not explicitly configured. An explicit value always overrides the session-type default. |
170
+ | `discovery-on-close` | boolean | `true` for every session type | Run discovery probes automatically during `/close`. An explicit value always wins. Was session-type aware until 2026-07-29 (`false` for `housekeeping`, #264); measurement showed the probes are the system's main project-hygiene surface, so defaulting them off for the cleanup session type left it as the only one closing without hygiene diagnostics. Set `false` explicitly for a faster close. |
124
171
  | `discovery-probes` | list | `[all]` | Probe categories to run: `all`, `code`, `infra`, `ui`, `arch`, `session`, `audit`, `vault`, `feature`. |
125
172
  | `discovery-exclude-paths` | list | `[]` | Glob patterns to exclude from discovery scanning (e.g., `vendor/**`, `dist/**`). |
126
173
  | `discovery-severity-threshold` | string | `low` | Minimum severity for reported findings: `critical`, `high`, `medium`, `low`. |
127
174
  | `discovery-confidence-threshold` | integer | `60` | Minimum confidence score (0-100) for discovery findings to be reported. Findings below this threshold are auto-deferred. |
128
175
  | `discovery-parallelism` | integer | `5` | Maximum probe agents dispatched in parallel per category during Phase 3. Bounds: `1..16`; out-of-range values silently fall back to the default. Raise for large stacks to reduce wall-clock, lower to relieve a busy host. |
129
176
 
177
+ ## Issue Budget (per-session creation cap)
178
+
179
+ Bounds how many issues ONE session may create. This is a **quantity** gate and is deliberately separate from `discovery-severity-threshold` / `discovery-confidence-threshold` above, which are per-finding **quality** filters: those two cannot bound volume (their `low` / `60` defaults filter almost nothing), they are only consulted in skill prose, and the largest producers — session-end carryover filing, `/plan` issue creation, `scripts/lib/spiral-carryover.mjs` — never read them at all.
180
+
181
+ ```yaml
182
+ issue-budget:
183
+ max-per-session: 12 # integer >= 0; 0 blocks every non-exempt creation
184
+ mode: strict # strict | warn | off
185
+ overflow: collect-issue # collect-issue | vault-note
186
+ ```
187
+
188
+ | Field | Type | Default | Description |
189
+ |-------|------|---------|-------------|
190
+ | `issue-budget.max-per-session` | integer | `12` | Non-exempt issues one session may create before the cap bites. `0` is valid (blocks everything non-exempt). Malformed or negative values fall back to `12`. |
191
+ | `issue-budget.mode` | string | `strict` | `strict` blocks over-cap creations (exit 2 from the hook) and parks them as overflow; `warn` allows them with a stderr notice; `off` disables the gate entirely (no counter is written). |
192
+ | `issue-budget.overflow` | string | `collect-issue` | Where session-end drains parked creations. `collect-issue` files exactly ONE `[Backlog-Sammel] <session-id>, N zurückgestellte Punkte` issue (`type::backlog`, `priority::low`) whose body is a checklist of the parked items; `vault-note` writes a single Markdown file under `vault/00-inbox/` instead. |
193
+
194
+ **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
+
196
+ **Counter file:** `.orchestrator/runtime/issue-budget.json` — `{ sessionId, count, exempt, overflow: [...] }`. It resets automatically when a new `sessionId` is seen.
197
+
198
+ **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
+
130
200
  ## Slopcheck (Package Legitimacy Gate) (#520)
131
201
 
132
202
  Opt-in defense against LLM-hallucinated package names ("slopsquatting"). When enabled, `classifyPackages(pkgs)` consults the registry and classifies each package as `LEGITIMATE` (exists, download count above threshold), `ASSUMED` (exists but very new / low downloads — warning, not block), `SUS` (audit warning hit — operator confirmation required), or `SLOP` (package not found in the registry — a possible LLM hallucination; hard block in plan-flow). Hooked into `/plan` PRD generation (Phase 3.5 Package-Audit) and `/discovery` supply-chain probes. Complementary to the always-on SEC-020 supply-chain baseline (`ignore-scripts=true`, `block-exotic-subdeps=true`, `minimum-release-age=1440`): SEC-020 prevents post-install execution of malicious packages; Slopcheck prevents adopting non-existent (typosquat-target) packages in the first place. PRD gsd Pattern 2 / issue #520.
@@ -157,7 +227,7 @@ slopcheck:
157
227
  | `learnings-surface-top-n` | integer | `15` | Cap on how many learnings the session-start Phase 5.6 and session-plan Step 0.5 sections surface, ranked by confidence descending. `0` = do not surface any learnings. Applies to Project Intelligence output. |
158
228
  | `learning-decay-rate` | float (0.0 ≤ x < 1.0) | `0.05` | Confidence decay applied to every untouched learning at session-end (after touched-set update, before prune). `0.0` = disable decay. A learning starting at `0.5` confidence survives ~10 untouched sessions with default decay. |
159
229
  | `enforcement` | string | `warn` | Hook enforcement level for scope and command restrictions: `strict`, `warn`, or `off`. |
160
- | `enforcement-gates` | object | null | Per-gate toggles for enforcement hooks. Keys: `path-guard`, `command-guard`, `post-edit-validate`. Values are booleans. Missing keys default to enabled. Example: `{ path-guard: true, command-guard: true, post-edit-validate: false }`. Combined with `enforcement` (which controls strict/warn/off globally). |
230
+ | `enforcement-gates` | object | null | Per-gate toggles for enforcement hooks. Keys: `path-guard`, `command-guard`, `post-edit-validate`, `bash-write-guard`, `bash-write-verify`. Values are booleans. **Missing keys default to enabled — with one deliberate exception: `bash-write-guard` defaults to DISABLED** (see below). Example: `{ path-guard: true, command-guard: true, post-edit-validate: false }`. Combined with `enforcement` (which controls strict/warn/off globally). |
161
231
  | `allow-destructive-ops` | boolean | `false` | When `true`, disables the main-session destructive-command guard (`hooks/pre-bash-destructive-guard.mjs`). Set to `true` for intentional maintenance sessions that need `git reset --hard`, `rm -rf`, etc. Defaults to `false` (safe). See issue #155 and `.claude/rules/parallel-sessions.md` (PSA-003). Example: `allow-destructive-ops: true` |
162
232
  | `reasoning-output` | boolean | `false` | Enable STATE:/PLAN: structured reasoning markers in agent prompts. When true, agents emit short transparency lines before tool calls. Opt-in — adds prompt overhead. |
163
233
  | `grounding-check` | boolean | `true` | Enable file-level grounding verification in session-end Phase 1.1a (planned vs touched files). When true, session-end compares each agent's declared file scope against `git diff --name-only $SESSION_START_REF..HEAD` and reports scope creep + incomplete coverage. Informational — does not block session close. |
@@ -166,6 +236,113 @@ slopcheck:
166
236
  | `max-turns` | integer or string | `auto` | Maximum agent turns before PARTIAL. Auto: housekeeping=8, feature=15, deep=25. |
167
237
  | `auto-commit-per-wave` | boolean | `false` | Automatically commit each wave's work after the Quality-Lite gate passes. Checkpoint commits per wave reduce the risk of data loss from `git stash` collisions in parallel sessions (V3.3 RESCUE incident — see GitLab #214). When `false`, all work is committed at session-end via `/close`. Requires `persistence: true`; the flag is silently ignored when `persistence: false`. Trade-off: each wave produces an additional commit; git log shows N+1 commits instead of 1. Use `/simplify` or `git rebase -i --autosquash` before final close to squash if a clean history is desired. **Implementation note:** the procedural commit sequence (`scripts/lib/auto-commit.mjs`) is deferred to V3.6. Until then, setting this flag to `true` triggers a session-start warning that auto-commits are not yet active — the flag is a no-op but is validated so projects can opt in early. |
168
238
 
239
+ ### enforcement-gates: the five gate keys (#800/#915)
240
+
241
+ `enforcement-gates` is surfaced to the hook layer as `gates` inside the wave's
242
+ `wave-scope.json`. Five keys are read today:
243
+
244
+ | Gate key | Hook | Event | Default when the key is ABSENT | Effect |
245
+ |----------|------|-------|-------------------------------|--------|
246
+ | `path-guard` | `hooks/enforce-scope.mjs` | PreToolUse `Edit\|Write\|MultiEdit` | **enabled** | Denies (strict) / warns (warn) on a file path outside `allowedPaths`. |
247
+ | `command-guard` | `hooks/enforce-commands.mjs` | PreToolUse `Bash` | **enabled** | Denies (strict) / warns (warn) on a blocked command pattern. |
248
+ | `post-edit-validate` | `hooks/post-edit-validate.mjs` | PostToolUse `Edit\|Write` | **enabled** | Per-file validation after a successful edit. |
249
+ | `bash-write-guard` | `hooks/enforce-commands.mjs` | PreToolUse `Bash` | **DISABLED — inverted default** | Warn-only; parses likely shell write targets out of the command and warns for each one outside `allowedPaths`. |
250
+ | `bash-write-verify` | `hooks/post-bash-write-verify.mjs` | PostToolUse `Bash` | **enabled** | Warn-only; observes the actual working-tree delta after a Bash call and reports files changed outside `allowedPaths`. |
251
+
252
+ **`bash-write-guard` is the one gate whose missing key means OFF.** Every other
253
+ key follows "absent → enabled", so a reader who has internalised that convention
254
+ will assume a repo without an explicit entry is covered. It is not. The hook
255
+ requires `gates['bash-write-guard'] === true` **literally** —
256
+ `enforce-commands.mjs` tests for the boolean `true`, not for "not false".
257
+
258
+ That silent assumption is why this row exists at all (#915): the gate shipped in
259
+ #800, was documented nowhere for two releases, and consequently ran in zero
260
+ sessions while the enforcement layer was believed to cover Bash writes. It does
261
+ not by default — and `enforce-scope.mjs` gates only `Edit`/`Write`/`MultiEdit`,
262
+ so a plain `echo x > out-of-scope.mjs` passes every PreToolUse path check.
263
+
264
+ The inverted default is deliberate, not an oversight: parsing write targets out
265
+ of an arbitrary shell command is heuristic (quoting, `>$VAR`, process
266
+ substitution, heredocs), so a false positive is cheap to produce. Measured over
267
+ 2 528 real Bash calls from 41 archived sessions of this repo, the parser flagged
268
+ 56 calls (2.22 %) — of which the majority were parse artefacts (`EOF`, `{`,
269
+ `0.3`) that never touched the filesystem. Keeping it opt-in until that rate is
270
+ driven down is correct; leaving it undocumented was not.
271
+
272
+ `bash-write-verify` (#915) is the complementary, non-heuristic half: it reports
273
+ what the filesystem actually shows rather than what the command appeared to say,
274
+ which is why it can default to enabled. It is warn-only and cannot block — a
275
+ PostToolUse hook fires after the command already ran. Its purpose is to produce
276
+ the evidence needed to flip `bash-write-guard` to `true` with confidence. See
277
+ § Bash-Write Verify below.
278
+
279
+ ## Bash-Write Verify (PostToolUse Bash diff, #915)
280
+
281
+ `hooks/post-bash-write-verify.mjs` closes the observability half of the Bash
282
+ bypass class. After every `Bash` tool call it runs
283
+ `git --no-optional-locks status --porcelain -z`, subtracts a baseline snapshot
284
+ taken on the previous invocation, and reports paths that appeared or changed
285
+ **outside** the wave's `allowedPaths`.
286
+
287
+ Properties that matter:
288
+
289
+ - **Warn-only, always.** stderr line plus a PostToolUse `additionalContext`
290
+ string. Never a deny; PostToolUse cannot block a command that already ran.
291
+ - **Delta, not absolute.** Reporting the whole dirty tree on every call would
292
+ fire on every Bash call for the rest of the session once a single file is
293
+ edited. Only paths that are new *relative to the previous Bash call* are
294
+ reported, and each path is reported **once**.
295
+ - **Silent re-baseline.** First call of a session, and any call after the wave's
296
+ `allowedPaths` change, records the current dirty set without warning — that
297
+ dirt was not caused by the call being observed.
298
+ - **Snapshot lives outside the repo** (`$TMPDIR/so-bash-write-verify/<hash>.json`),
299
+ so the guard can never report its own bookkeeping.
300
+ - **Ignore list is part of the contract**, not an implementation detail — see
301
+ the table in the hook's header comment. It covers sibling-hook writes under
302
+ `.orchestrator/`, coordinator status files under `.claude/`/`.codex/`/`.cursor/`/`.pi/`,
303
+ package-manager artefacts (`node_modules/`, lockfiles), build/coverage output,
304
+ and `tmp+rename` residue (`*.tmp*`). These are mostly `.gitignore`d in this
305
+ repo, but the guard must not depend on a consumer repo's `.gitignore` being
306
+ complete.
307
+
308
+ **Measured cost and noise.** All figures below were measured on 2026-07-30
309
+ against this repo at 1 507 tracked files (`git ls-files | wc -l`); the corpus is
310
+ this repo's 51 archived Claude Code transcripts (the per-project `*.jsonl`
311
+ files under the host-local Claude Code projects directory), of which 41 contain
312
+ at least one `Bash` tool call.
313
+
314
+ | Metric | Value |
315
+ |--------|-------|
316
+ | Hook end-to-end, per Bash call | **94.8 ms** (10 runs, wall clock) |
317
+ | …of which node cold start (paid by every hook process anyway) | ~67 ms — an existing PreToolUse Bash hook, `enforce-commands.mjs`, measures 66.9 ms on the same loop |
318
+ | …marginal cost of the `git status` this hook adds | **~28 ms** |
319
+ | `git … status --porcelain -z --untracked-files=all` | 25.0 ms/call (`--untracked-files=normal` measured 28.7 ms — `all` is not the slower option here, because the expensive subtrees are `.gitignore`d) |
320
+ | Bash calls in the corpus | 2 528 |
321
+ | Calls containing any write construct at all | 732 (29.0 %) |
322
+ | Calls writing a real in-repo, non-ignored path | **23 (0.91 %)** |
323
+ | Distinct such paths per session | **median 1, max 5** |
324
+
325
+ So a typical session sees ~1 warning and a worst-case session sees 5. Two
326
+ design choices produce that number rather than the naive one: the ignore list,
327
+ and — more importantly — report-once. Without report-once the guard emits a
328
+ line on *every* Bash call from the first out-of-scope write to the end of the
329
+ session, because `git status` reports the cumulative dirty tree, not a delta.
330
+
331
+ Live cross-check on the session that added this hook: with a 29-entry
332
+ `allowedPaths` union, the snapshot recorded **0** out-of-scope paths — i.e. zero
333
+ warnings across the session's Bash calls.
334
+
335
+ **Turning it off:** `enforcement-gates: { bash-write-verify: false }`, or
336
+ `enforcement: off`, or the standard `SO_DISABLED_HOOKS=post-bash-write-verify`
337
+ profile-gate env var.
338
+
339
+ **Making it bite.** This hook can never become blocking — the escalation path is
340
+ to flip `bash-write-guard` (PreToolUse, *can* deny) to `true` once its
341
+ false-positive rate has been measured down. Prerequisite before that flip: a
342
+ session's worth of `bash-write-verify` warnings compared against the same
343
+ session's `bash-write-guard` warnings, showing the parser produces no warning
344
+ that the filesystem diff does not confirm.
345
+
169
346
  ## STATE.md Lock (#518)
170
347
 
171
348
  Mechanical write-lock around STATE.md that prevents race conditions between parallel worker sessions (or parallel wave-executor checkpoints within one session) writing the same file. When enabled, `withStateMdLock(repoRoot, fn)` acquires `.orchestrator/state.lock` via atomic tmp-file + rename before invoking `fn`, and releases on completion or throw. A stale lock (holder PID no longer alive, or heartbeat expired) is overridden atomically with a WARN on stderr; genuine contention past `timeout-ms` returns `{ ok: false, reason: 'timeout' }` to the caller. This mechanically enforces PSA-003/PSA-004 (Destructive Action Safeguards / Commit Discipline) for STATE.md specifically — the race condition becomes structurally impossible rather than merely discouraged. PRD gsd Pattern 1 / issue #518.
@@ -303,6 +480,19 @@ Independent of isolation choice, wave-executor now persists a per-worktree meta
303
480
 
304
481
  This guard converts the manual post-copy `git diff` check (used to rescue the 07:30 and 09:00 regressions) into a coded pre-copy gate. The check is non-blocking on `pass` / `warn` / `no-meta`; only `block` interrupts the wave.
305
482
 
483
+ ### Heavy-Repo Preflight (HR-003/HR-004, baseline #60)
484
+
485
+ `templates/shared/.claude/rules/heavy-repo.md` documents two Session Config fields for repos large enough that default parallelism risks resource pressure (HR-001 indicators: checkout > 50 MB, DB surface > 100 tables, prior parallel agent count > 15, build time > 90s, generated artifacts > 200 MB). Both fields are now wired end-to-end (previously documented but silently dropped by the parser):
486
+
487
+ | Field | Type | Default | Description |
488
+ |-------|------|---------|-------------|
489
+ | `heavy-repo` | boolean | `false` | Marks the repo as heavy per HR-001. When `true`, `scripts/lib/resource-probe/evaluate.mjs` and `scripts/lib/wave-resource-gate.mjs` apply a STATIC preflight ceiling: `recommended_agents_per_wave_cap` / dispatched `agents` are clamped to at most `agents-per-wave`, REGARDLESS of the live resource-probe verdict. When `agents-per-wave` uses the parenthetical override syntax (e.g. `4 (deep: 18)`), the parsed value is an object `{default, <mode>: N}`, not a plain number — both modules resolve that shape to `.default` (no session-mode input is in scope at the gate), so the cap is never silently skipped for overridden repos. More-restrictive-wins: a resource-driven `reduce`/`coordinator-direct` that already computed a tighter number is never loosened by this cap — it only ever lowers, never raises, the dispatched agent count. `validateSessionConfig()` (`scripts/lib/config-schema.mjs`) also emits a warn-level cross-field finding when `heavy-repo: true` and `isolation` is `auto` or `none` — heavy repos should pin `isolation: worktree` (HR-003 anti-pattern). |
490
+ | `worktree-cleanup` | string | `default` | One of `default` \| `aggressive`. HR-003 recommends `aggressive` for heavy repos (clean up worktrees immediately after each wave, no cross-wave retention). **Honesty note:** `aggressive` currently behaves identically to `default` at runtime — the parser accepts and returns the value, but the per-wave aggressive sweep into `worktree-cleanup.mjs`/`worktree-sweep.mjs` is a tracked follow-up, not yet implemented. Setting this field today documents intent; it does not yet change wave-executor's cleanup cadence. |
491
+
492
+ Session-start Phase 4.5 (`skills/session-start/phase-4-5-resource-health.md`) passes `heavy-repo` and `agents-per-wave` through to `evaluate()` and renders `⚠ Heavy-repo mode active — agents-per-wave capped to N (Session Config heavy-repo: true)` when the ceiling actually reduces the recommendation, per HR-004.
493
+
494
+ The heavy-repo cap applies only on the resource-aware path: it runs inside `applyDecisionRules()`/`evaluate()`, both gated behind `resource-awareness` being enabled (the default). Setting `resource-awareness: false` is a FULL opt-out — it skips the live probe AND bypasses the HR-004 static cap, even when `heavy-repo: true` is also set.
495
+
306
496
  ## Planning
307
497
 
308
498
  | Field | Type | Default | Description |
@@ -451,7 +641,7 @@ vault-integration:
451
641
  | `vault-integration.enabled` | boolean | `false` | If true, session-end and evolve skills invoke `vault-mirror.mjs` to sync learnings and sessions into the vault. When false (or missing), mirroring is skipped silently. |
452
642
  | `vault-integration.vault-dir` | string or null | `null` | Absolute path to the vault repository. Falls back to `$VAULT_DIR` env variable if not set. Required when `enabled` is true. |
453
643
  | `vault-integration.mode` | string | `warn` | Mirror error handling. `strict` blocks session close if the mirror exits non-zero. `warn` reports errors but does not block. `off` bypasses mirror invocation entirely (useful when transitioning). |
454
- | `vault-integration.vault-name` | string or null | `null` | Optional override for the per-project vault namespace segment (#660). When set (or via CLI `--vault-name`), vault writes go to `40-learnings/<vault-name>/` and `50-sessions/<vault-name>/`, sanitised to a kebab slug. When null/absent, the namespace is derived from the git origin via `deriveRepo()`. Owner-privacy leaks (personal home path / private slug / personal name) are redacted to `redacted-repo`. NOT a filesystem path → NOT host-path-resolved. Resolver: `scripts/lib/vault-mirror/namespace.mjs` (`resolveRepoNamespace`). |
644
+ | `vault-integration.vault-name` | string or null | `null` | Optional override for the per-project vault namespace segment (#660). When set (or via CLI `--vault-name`), vault writes go to `40-learnings/<vault-name>/` and `50-sessions/<vault-name>/`, sanitised to a kebab slug. When null/absent, the namespace is derived from the git origin via `deriveRepo()`. Owner-privacy leaks (personal home path / private slug / personal name) are redacted to `redacted-repo`. NOT a filesystem path → NOT host-path-resolved. **Coverage gap:** portfolio board rows are NOT yet namespace-aware — the override only reaches `40-learnings/`, `50-sessions/`, and the per-repo narrative mirror (`narrative-mirror.mjs`, #832 item 2); tracked separately in #832. Resolver: `scripts/lib/vault-mirror/namespace.mjs` (`resolveRepoNamespace`). |
455
645
  | `vault-integration.gitlab-groups` | string[] or null | `null` | List of GitLab group paths to scan for repos missing `.vault.yaml`. Consumed by `scripts/vault-backfill.mjs` (via `readVaultIntegrationConfig()`) and the `/plan retro` vault-backfill sub-mode (`skills/plan/mode-retro.md` Phase 1.6 Step 1). When null/unset, the backfill CLI exits with a "no groups configured" notice. |
456
646
 
457
647
  ### Environment override: `VAULT_MIRROR_CANONICAL_SUFFIX`
@@ -651,7 +841,7 @@ handover-gate:
651
841
 
652
842
  ## Broken-Window Budget (#730/H5)
653
843
 
654
- Opt-in configuration for the Broken-Window Budget in `/close`. When enabled, session-end Phase 2.6 aggregates THIS session's "knowingly-broken shipments" — echo-stub findings that shipped under `enforcement: warn`, "Override and close" choices in Phase 2.3 / 2.5, MED/LOW review findings routed to "Unresolved Review Findings" (#617), and wave-level reviewer findings overridden without a fix task — and files ONE hard-terminated closure issue per item (labels `broken-window` + `priority:high`, with a hard due-date). It also emits `orchestrator.finding.overridden` events feeding the `effectiveness.override_ratio` metric. Non-blocking and idempotent: a filing failure is a WARN and re-running a close never duplicates issues.
844
+ Opt-in configuration for the Broken-Window Budget in `/close`. When enabled, session-end Phase 2.6 aggregates THIS session's "knowingly-broken shipments" — echo-stub findings that shipped under `enforcement: warn`, "Override and close" choices in Phase 2.3 / 2.5, MED/LOW review findings routed to "Unresolved Review Findings" (#617), and wave-level reviewer findings overridden without a fix task — and files ONE hard-terminated closure issue per item (labels `broken-window` + `priority::high`, with a hard due-date). It also emits `orchestrator.finding.overridden` events feeding the `effectiveness.override_ratio` metric. Non-blocking and idempotent: a filing failure is a WARN and re-running a close never duplicates issues.
655
845
 
656
846
  All fields live under a top-level `broken-window-budget` object in your Session Config host file (`CLAUDE.md` or `AGENTS.md`), for example:
657
847
 
@@ -695,6 +885,8 @@ dialectic:
695
885
 
696
886
  **Token cost:** With defaults (cadence: 5, budget-tokens: 8000, output 4000, model haiku), ~12k tokens every 5 sessions. At haiku pricing this is ~$0.02/run. Surfaced in Final Report.
697
887
 
888
+ **Empirical note (2026-07-04 session-3) — the parser default of `8000` proved structurally unreachable for this repo's own peer-card/steering corpus.** Fixed overhead (Peer-Cards + Steering + scaffold) alone runs ≈13k tokens, and a full input set (top-50 learnings + last-10 sessions) runs ≈28.4k tokens — both already exceed the `8000` default before any call is made. This repo's own committed Session Config therefore sets `budget-tokens: 32000`, not the documented default. If your repo accumulates a similarly large peer-card/steering corpus over time, raise `budget-tokens` accordingly rather than leaving the low default in place — a too-low budget silently truncates the dialectic critique input rather than erroring.
889
+
698
890
  ## Eval (#803)
699
891
 
700
892
  Opt-in configuration for the Standard v1 evaluation harness (aiat-llm-eval PRD, `docs/prd/2026-07-16-aiat-llm-eval.md` §S6) and the forthcoming `/eval` skill (Session-Prozess-Eval — lands in a later wave of Epic #803). This section documents the config surface only; the skill that reads it is not yet shipped as of this parser's introduction.
@@ -718,7 +910,7 @@ eval:
718
910
  | `eval.report` | string | `html` | Report artifact format. Must be one of `html`, `none`. **Fail-fast** on an unknown value, same as `eval.mode`. |
719
911
  | `eval.handle` | string \| `null` | `null` | Optional free-text handle/label for the eval run. Absent, empty, or whitespace-only values all collapse to `null` (never an empty string). |
720
912
 
721
- **Parser gotcha (learning confidence 0.9 — mirrors `custom-phases:` and `dialectic:`):** the `eval:` key-line itself MUST NOT carry an inline comment. The block-open scan uses the strict regex `/^eval:\s*$/`; a trailing `# comment` on that exact line fails the match, so the parser never enters the block and ALL fields silently fall back to their defaults — no error, no warning surfaces anywhere. Sub-key lines (`enabled:`, `mode:`, …) tolerate inline comments without issue.
913
+ **Parser gotcha (learning confidence 0.9 — mirrors `custom-phases:` and every other block-shaped key):** the `eval:` key-line itself MUST NOT carry an inline comment. The block-open scan uses the shared `matchBlockHeader(line, 'eval')` (`scripts/lib/config/block-header.mjs`) — it tolerates the bold-bullet `- **eval:**` rendering (#830) but a trailing `# comment` on the header line still fails the match, so the parser never enters the block and ALL fields silently fall back to their defaults — no error, no warning surfaces anywhere. Sub-key lines (`enabled:`, `mode:`, …) tolerate inline comments without issue. See § Parser Gotcha: No-Inline-Comment Block Headers (top of this file) for the general contract this key shares with 36 other block-shaped keys.
722
914
 
723
915
  **Used by:** `scripts/lib/config/eval.mjs` (`_parseEval`), `scripts/lib/config.mjs`. Skill consumer (`skills/eval/SKILL.md`) is a follow-up wave of Epic #803 — not yet implemented as of this parser.
724
916
 
@@ -785,6 +977,71 @@ docs-staleness:
785
977
 
786
978
  **Used by:** `skills/discovery/probes/docs-staleness.mjs` (`runProbe`), `scripts/lib/config/docs-staleness.mjs` (`_parseDocsStaleness`). Writes one JSONL summary record per run to `.orchestrator/metrics/docs-staleness.jsonl`. See `docs/README.md` for the living-vs-archived docs classification this probe enforces.
787
979
 
980
+ ## MOC Staleness (#831/B2)
981
+
982
+ Opt-in session-start banner probe for Obsidian "map of content" index notes — `<vault>/08-topics/*-moc.md` whose frontmatter `updated:` is older than the threshold. Complements `vault-staleness` (which covers project narratives) by covering the topic index layer. Rendered at session-start Phase 4 alongside the other banners; never blocks a session.
983
+
984
+ ```yaml
985
+ moc-staleness:
986
+ # Parser gotcha: this key line must carry NO inline comment (§ Parser Gotcha: No-Inline-Comment Block Headers, top of this file).
987
+ enabled: false # opt-in
988
+ thresholds:
989
+ moc: 90 # days — frontmatter `updated:` staleness threshold
990
+ mode: warn # warn | off
991
+ ```
992
+
993
+ | Field | Type | Default | Description |
994
+ |-------|------|---------|-------------|
995
+ | `moc-staleness.enabled` | boolean | `false` | Must be explicitly `true` to activate. The gate fails CLOSED — a missing block, a missing `enabled` key, or an omitted `config` argument all return `null` before any filesystem I/O. |
996
+ | `moc-staleness.thresholds.moc` | integer (days) | `90` | Age threshold measured against frontmatter `updated:`. Non-numeric or non-positive values fall back to the default. |
997
+ | `moc-staleness.mode` | string | `warn` | `warn` \| `off`. A malformed value falls back to `warn`. |
998
+
999
+ A MOC whose `updated:` is **missing or unparseable is deliberately EXCLUDED**, not reported as stale — the corrective action there is "fix the frontmatter", not the banner's own hint. Same rule as `scripts/lib/peer-cards/staleness-banner.mjs`.
1000
+
1001
+ **Used by:** `scripts/lib/moc-staleness-banner.mjs` (`checkMocStaleness`), `scripts/lib/config/moc-staleness.mjs` (`_parseMocStaleness`). Wired at `skills/session-start/SKILL.md` Phase 4.
1002
+
1003
+ ## Context Coverage (#831/B4)
1004
+
1005
+ Opt-in session-start coverage banner: registered `<vault>/01-projects/<slug>/` folders that carry **neither** `context.md` nor `_passive.md`. A project counts as *registered* iff its folder contains `_overview.md` — the same convention `discoverVaultRepos()` uses (`scripts/lib/gitlab-portfolio/vcs-detect.mjs`). Folders lacking `_overview.md` are never counted and never reported as gaps.
1006
+
1007
+ ```yaml
1008
+ context-coverage:
1009
+ # Parser gotcha: this key line must carry NO inline comment (§ Parser Gotcha: No-Inline-Comment Block Headers, top of this file).
1010
+ enabled: false # opt-in
1011
+ mode: warn # warn | off
1012
+ ```
1013
+
1014
+ | Field | Type | Default | Description |
1015
+ |-------|------|---------|-------------|
1016
+ | `context-coverage.enabled` | boolean | `false` | Must be explicitly `true` to activate. Fails CLOSED, as above. |
1017
+ | `context-coverage.mode` | string | `warn` | `warn` \| `off`. A malformed value falls back to `warn`. |
1018
+
1019
+ **Used by:** `scripts/lib/context-coverage-banner.mjs` (`checkContextCoverage`), `scripts/lib/config/context-coverage.mjs` (`_parseContextCoverage`). Wired at `skills/session-start/SKILL.md` Phase 4.
1020
+
1021
+ ## Worktree Orphans (#831/B5)
1022
+
1023
+ Opt-in session-end sweep (Phase 4b) identifying git worktree branches with **0 commits ahead of the base branch** — leftovers from finished sessions. The module **proposes; it never disposes**: it returns `candidates` and the coordinator renders the removal AUQ. Nothing is removed without explicit operator confirmation (PSA-003).
1024
+
1025
+ ```yaml
1026
+ worktree-orphans:
1027
+ # Parser gotcha: this key line must carry NO inline comment (§ Parser Gotcha: No-Inline-Comment Block Headers, top of this file).
1028
+ enabled: false # opt-in
1029
+ base-branch: main # ref the ahead-count is measured against
1030
+ mode: warn # warn | off
1031
+ ```
1032
+
1033
+ | Field | Type | Default | Description |
1034
+ |-------|------|---------|-------------|
1035
+ | `worktree-orphans.enabled` | boolean | `false` | Must be explicitly `true` to activate. Fails CLOSED — a repo that never opted in pays no git invocation. |
1036
+ | `worktree-orphans.base-branch` | string | `main` | Ref the ahead-count is measured against. **Validated:** a value beginning with `-`, or containing whitespace or shell metacharacters, is rejected and falls back to `main`. |
1037
+ | `worktree-orphans.mode` | string | `warn` | `warn` \| `off`. A malformed value falls back to `warn`. |
1038
+
1039
+ **Why `base-branch` is validated rather than passed through.** It reaches an argv position in `git rev-list --count <base>..<branch>`. A value shaped like a git flag (e.g. `--glob=refs/heads/*`) is parsed as an OPTION rather than a revision range, exits 0, and prints `0` — silently marking **every** worktree as a 0-ahead orphan and offering the operator a deletion prompt for worktrees full of live work. The conservative default does not catch it, because `0` parses fine. Defence is two-layer: the parser rejects leading-dash values, and the sink passes `--end-of-options` so any surviving payload becomes a hard git error. This makes `base-branch` a **fifth command-influencing Session Config surface** beyond the four listed in `.claude/rules/security.md` § "Session Config Command Trust" — and unlike those, no attacker is required: a typo reaches the same outcome.
1040
+
1041
+ A worktree holding uncommitted, staged or untracked work is **never** a candidate — `isWorktreeClean()` (the Phase 4a helper) is consulted first, and any git error while checking excludes the worktree conservatively.
1042
+
1043
+ **Used by:** `scripts/lib/session-end/worktree-orphan-sweep.mjs` (`checkWorktreeOrphans`), `scripts/lib/config/worktree-orphans.mjs` (`_parseWorktreeOrphans`, `_isSafeBaseBranch`). Wired at `skills/session-end/SKILL.md` Phase 4b.
1044
+
788
1045
  ## Docs Orchestrator
789
1046
 
790
1047
  Opt-in configuration for the `docs-orchestrator` skill, which generates audience-split documentation (User / Dev / Vault) within sessions (see `skills/docs-orchestrator/SKILL.md`). When enabled, session-start runs a Phase 2.5 docs-context step, session-plan assigns a Docs role, and session-end runs a Phase 3.2 gap-reporting step. The `docs-writer` agent is made available automatically when `enabled: true`.
@@ -859,6 +1116,8 @@ test:
859
1116
 
860
1117
  Opt-in, repo-declared deterministic phases that run as their own phase during session close (and/or housekeeping). Where the freeform `special:` key gives no execution guarantee, `custom-phases` is a **contract**: each phase runs a deterministic `command` via Bash with exit-code gating and summary reporting, so a repo can run a domain command (e.g. an eval-learn aggregate) as a first-class close step. Absent/empty ⇒ `[]` ⇒ no custom phases run; existing sessions are unaffected.
861
1118
 
1119
+ **Parser gotcha:** like every other block-shaped Session Config key, the `custom-phases:` key-line itself MUST NOT carry an inline comment — see § Parser Gotcha: No-Inline-Comment Block Headers (top of this file) for the general `matchBlockHeader` contract. A trailing `# comment` on that exact line means the parser never enters the block and `custom-phases` silently resolves to `[]` — no error, no warning.
1120
+
862
1121
  The block is a YAML list under a top-level `custom-phases` key:
863
1122
 
864
1123
  ```yaml
@@ -926,6 +1185,7 @@ reconcile:
926
1185
  confidence-floor: 0.5 # min learning confidence before a learning is eligible
927
1186
  min-rule-days: 7 # floor on emitted expires-at so a rule is never born-dead
928
1187
  min-insight-chars: 24 # reject placeholder/minimal insights before rule conversion
1188
+ max-proposals-per-run: 10 # volume brake — cap on proposals minted per engine run
929
1189
  ```
930
1190
 
931
1191
  | Field | Type | Default | Description |
@@ -937,6 +1197,7 @@ reconcile:
937
1197
  | `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. |
938
1198
  | `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. |
939
1199
  | `reconcile.min-insight-chars` | integer | `24` | Minimum `insight` length (characters) required before a learning is eligible for rule conversion — issue #741.2. Opt-in and additive to the always-on placeholder/empty-insight rejection in `classifyLearning()` (`scripts/lib/reconcile/eligibility.mjs`): a non-empty but too-short insight (e.g. a stub or a recovery placeholder) is rejected with reason `placeholder-insight` before it reaches proposal generation. Set to `0` to disable the length check (only the always-on empty/placeholder-regex check applies). |
1200
+ | `reconcile.max-proposals-per-run` | integer | `10` | Volume brake — issue #900 D. After the eligibility filter runs, `runReconcile()` (`scripts/lib/reconcile/engine.mjs`) sorts eligible learnings by confidence DESC and proposes at most this many per run; the rest are recorded as `capped` rejections (visible in `summary.capped` and each carrying a `capped — ...` reason) rather than silently dropped. Both production call sites — the session-end Phase 3.6.8 dispatcher (`decideReconcile()` in `scripts/lib/session-end/phase-skip.mjs`) and the on-demand `/reconcile` command (`skills/reconcile/SKILL.md`) — forward this Session Config value to `runReconcile()` as `maxProposalsPerRun`. The brake is **always active** even beyond that: a caller that omits the parameter entirely (e.g. a direct programmatic `runReconcile()` call) still gets the engine's own internal default, which mirrors the Session Config parser's default of `10`. Bounds: positive integer (≥ 1); malformed, absent, or non-positive values fall back to `10`. |
940
1201
 
941
1202
  ### Never-always-on invariant
942
1203
 
@@ -966,7 +1227,7 @@ Session-end Phase 3.7a is the **only writer** of these fields. Session-start Pha
966
1227
  | Field | Type | Value range | Description |
967
1228
  |-------|------|-------------|-------------|
968
1229
  | `recommended-mode` | string | `housekeeping` \| `feature` \| `deep` \| `discovery` \| `evolve` \| `plan-retro` | v0 heuristic output: suggested mode for the next session. |
969
- | `top-priorities` | integer[] | 0–5 entries | Carried-over issue IIDs, pre-sorted (priority:critical/high first, FIFO tiebreak). |
1230
+ | `top-priorities` | integer[] | 0–5 entries | Carried-over issue IIDs, pre-sorted (priority::critical/high first, FIFO tiebreak). |
970
1231
  | `carryover-ratio` | float | `0.00`–`1.00` | `carryover_count / planned_issues` (0 when planned=0). Rounded to 2 decimals. |
971
1232
  | `completion-rate` | float | `0.00`–`1.00` | `completed_issues / planned_issues`. Rounded to 2 decimals. |
972
1233
  | `rationale` | string | ≤ 120 chars, single line | Which v0 rule branch fired (e.g. `"v0: completion <50% → retro"`). |
@@ -1094,6 +1355,15 @@ allow-config-weakening: false # per-session bypass (mirrors allow-destr
1094
1355
 
1095
1356
  Always-on directive-budget banner. At session-start Phase 4 the probe (`scripts/lib/instruction-budget-guard.mjs`, `checkInstructionBudget`) sums the structural directives (bullets, ordered items, headings ≥ depth 2 — fenced code and YAML frontmatter excluded) across the always-on `.claude/rules/*.md` files (membership delegated to `rule-loader.mjs`; glob-scoped rules excluded) and renders a **warn-only / non-blocking** banner when the total **exceeds** `ceiling`. It is a *growth-ratchet*: the current baseline (~457 structural directives across 11 always-on rules) sits under the default ceiling of `480`, so the banner is silent today and only fires when NEW always-on directives push the count over the ceiling — "mechanism over discipline". Default ON (this is a guard, not an opt-in feature) — set `enabled: false` or `mode: off` to silence it.
1096
1357
 
1358
+ **Two axes, not one (#931a).** Since the byte dimension shipped in #877 it was measured but never judged: `overBudget` followed from the directive count alone, and the banner text never named a byte. It now reads `overDirectiveBudget || overByteBudget`, and the banner names only the axis that actually broke (re-sorting the top-files list by bytes when bytes were the trigger, because the count ordering points at the wrong file).
1359
+
1360
+ | Key | Default | Meaning |
1361
+ |---|---|---|
1362
+ | `instruction-budget.ceiling` | `480` | Structural-directive ceiling. Baseline ~457, i.e. +5% headroom. |
1363
+ | `instruction-budget.byte-ceiling` | `114000` | Byte ceiling over the same always-on corpus. Baseline 108,589 measured 2026-07-30, i.e. the same +5% headroom — the two axes are calibrated alike so neither is accidentally the stricter one. |
1364
+
1365
+ Why both: a 9 KB prose rule carrying three bullets is nearly invisible to the directive count while consuming real prompt payload. Why the headroom rather than the measured value: a ceiling that reddens the current state is switched off within a session and then measures nothing.
1366
+
1097
1367
  All fields live under a top-level `instruction-budget` object inside the `## Session Config` block of your host file (`CLAUDE.md` or `AGENTS.md`):
1098
1368
 
1099
1369
  ```yaml
@@ -1163,6 +1433,7 @@ SO_DISABLED_HOOKS=enforce-scope,enforce-commands claude ...
1163
1433
  | `enforce-commands` | PreToolUse/Bash |
1164
1434
  | `enforce-scope` | PreToolUse/Edit\|Write |
1165
1435
  | `post-edit-validate` | PostToolUse/Edit\|Write |
1436
+ | `post-bash-write-verify` | PostToolUse/Bash |
1166
1437
  | `on-stop` | Stop + SubagentStop |
1167
1438
 
1168
1439
  ### Implementation
@@ -1292,6 +1563,31 @@ autopilot:
1292
1563
 
1293
1564
  **Feature introduced by:** GitLab issue #431 (CC 2.1.143 `worktree.bgIsolation` changelog adoption). Implementation: `scripts/autopilot-multi.mjs` reads `config?.autopilot?.['bg-isolation']` via `scripts/parse-config.mjs`. Documentation: `skills/autopilot/SKILL.md` § Configuration.
1294
1565
 
1566
+ ## Wave Reviewers
1567
+
1568
+ Opt-in inter-wave architecture/QA/PRD audit dispatch. When configured, wave-executor's `### 5a. Persona-reviewer dispatch` step (Impl-Core and Impl-Polish waves only — Discovery, Quality, and Finalization are skipped) fans out the named code-oriented reviewer agents in parallel with read-only scope after the wave's own work completes. Findings are **advisory only** — a `WARN` or `FAIL` never blocks the wave; it is surfaced in the wave progress summary and fed into the next wave's agent assignments, or logged as an overridden Deviation (#730/H5) when not actioned.
1569
+
1570
+ All fields live under a top-level `wave-reviewers` object in your Session Config host file (`CLAUDE.md` or `AGENTS.md`), for example:
1571
+
1572
+ ```yaml
1573
+ wave-reviewers:
1574
+ enabled: false # opt-in inter-wave architecture/QA/PRD audits
1575
+ reviewers: [] # ["architect-reviewer", "qa-strategist", "analyst"]
1576
+ mode: warn # warn | strict | off
1577
+ ```
1578
+
1579
+ | Field | Type | Default | Description |
1580
+ |-------|------|---------|-------------|
1581
+ | `wave-reviewers.enabled` | boolean | `false` | Master toggle. Absent, `false`, or an empty `reviewers` array all resolve to the same no-op — the dispatch step is skipped entirely and the wave loop proceeds exactly as before. |
1582
+ | `wave-reviewers.reviewers` | string[] | `[]` | Reviewer agent names to dispatch in parallel. Plugin-provided values: `architect-reviewer`, `qa-strategist`, `analyst`. Custom reviewer agents under `agents/` are also valid if their `name` frontmatter matches. Each name is dispatched via `Agent({ subagent_type: "session-orchestrator:<reviewer-name>", ... })` with read-only scope. |
1583
+ | `wave-reviewers.mode` | string (`warn` \| `strict` \| `off`) | `warn` | Parsed but currently only `enabled` + a non-empty `reviewers` array gate the dispatch step in `skills/wave-executor/wave-loop.md`; `mode` is reserved for future escalation behaviour (e.g. blocking on `FAIL`). |
1584
+
1585
+ **Deprecated alias:** `persona-reviewers` is accepted as a backward-compatible alias. When only `persona-reviewers` is present, its values are used and a deprecation WARN is emitted to stderr; when both keys are present, `wave-reviewers` wins and the WARN still fires. Issue #461/#478.
1586
+
1587
+ **Distinct from `persona-gate-wave`** (below): `wave-reviewers` dispatches code-oriented reviewer agents (`architect-reviewer`, `qa-strategist`, `analyst`) that judge implementation correctness; `persona-gate-wave` dispatches catalog domain/buyer/audit personas from `.claude/personas/` that judge audience fit. The two keys are independent and may both be configured on the same project without conflict.
1588
+
1589
+ **Used by:** `scripts/lib/config/wave-reviewers.mjs` (`_parseWaveReviewers`), `scripts/lib/config.mjs`, `skills/wave-executor/wave-loop.md` § 5a.
1590
+
1295
1591
  ## Persona-Gate Wave (#458)
1296
1592
 
1297
1593
  Opt-in mid-wave hook that dispatches a `/persona-panel`-style review after a configured wave completes (Quality or Impl-Polish). Distinct from `wave-reviewers` (which targets code-oriented reviewer agents like `architect-reviewer` and `qa-strategist`): `persona-gate-wave` dispatches catalog personas from `.claude/personas/` — domain-experts, buyer-personas, and auditors. The two keys are independent; a project may configure both on the same wave without conflict.
@@ -1409,6 +1705,8 @@ skill-evolution:
1409
1705
 
1410
1706
  **Parity note.** The `skill-evolution:` key is documented in `docs/session-config-template.md` as a **standalone `## Skill Evolution` section** outside the `## Session Config` block — intentionally parity-exempt from `claude-md-drift-check` Check-6. Adding it as a column-0 key inside `## Session Config` would hard-fail every repo with `drift-check.mode: hard` that has not yet adopted the feature.
1411
1707
 
1708
+ **Root instruction-file activation capture (#652).** This repo's own `## Skill Evolution` block records why the feature was armed (`autonomy: autonomous-gated`) here specifically: the engine was activated for this repo (#652) after the C2 engine (#647/#651) plus the H1 `evidence_kind` guard (session-3) made autonomous-apply safe. Under that gate, the engine may auto-apply ONLY the `command-count` drift shape on the root instruction file, behind the quadruple condition (autonomy ∧ safe-posture ∧ gate-green ∧ evidence ≥ evidence-floor) AND only for `filesystem-fact`-sourced candidates. Plugin-level, local-skill, and remote-skill repair targets are ALWAYS MR-only regardless of this setting.
1709
+
1412
1710
  ## Dispatcher Autonomy (#679)
1413
1711
 
1414
1712
  Opt-in configuration for the cross-repo free-repo dispatcher autonomy gate (Epic #673, Sub-issue #679). Controls whether the `/dispatcher` flow runs in advisory mode (surfaces ranked candidates for operator review only) or applies dispatch decisions behind a confidence gate. The default is `off` — fail-closed, no behavior change for repos that omit this block.
@@ -1434,6 +1732,8 @@ dispatcher-autonomy:
1434
1732
 
1435
1733
  **Parity note.** The `dispatcher-autonomy:` key is documented in `docs/session-config-template.md` as a **standalone `## Dispatcher Autonomy` section** outside the `## Session Config` block — intentionally parity-exempt from `claude-md-drift-check` Check-6 (session-config-parity). Adding it as a column-0 key inside `## Session Config` would hard-fail every repo with `drift-check.mode: hard` that has not yet adopted the feature.
1436
1734
 
1735
+ **Root instruction-file adoption capture (#681).** This repo's own `## Dispatcher Autonomy` block is the one-time capture of #681 (dogfooded here), committed with `autonomy: off` — fail-closed, identical to this repo's de-facto state before adoption. Its committed PRESENCE is itself the never-re-ask marker: session-start Phase 1.1's migration trigger checks for the block's existence and will not re-prompt, regardless of the value inside. See the Host-local override paragraph above (#653 pattern) for the full env > `owner.yaml` > committed > `off` precedence chain that lets a machine opt this repo into `advisory`/`autonomous-gated` without editing the committed block.
1736
+
1437
1737
  ## Defaults
1438
1738
 
1439
1739
  If no `## Session Config` section exists in the platform config host file (`CLAUDE.md` or `AGENTS.md`), skills use: `feature` type, 6 agents, 5 waves, and field-specific defaults listed above.