session-orchestrator 3.17.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 (187) 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 +105 -412
  6. package/README.md +12 -9
  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/USER-GUIDE.md +8 -3
  21. package/docs/ci-setup.md +121 -7
  22. package/docs/codex-setup.md +1 -1
  23. package/docs/components.md +6 -6
  24. package/docs/cursor-setup.md +22 -9
  25. package/docs/events-schema.md +5 -1
  26. package/docs/instruction-delivery.md +444 -0
  27. package/docs/rule-authoring.md +58 -9
  28. package/docs/session-config-reference.md +244 -9
  29. package/docs/session-config-template.md +39 -3
  30. package/hooks/_lib/guard-source-loader.mjs +467 -0
  31. package/hooks/_lib/lock-bootstrap.mjs +21 -0
  32. package/hooks/_lib/vcs-create-matcher.mjs +119 -0
  33. package/hooks/config-protection.mjs +0 -0
  34. package/hooks/enforce-commands.mjs +10 -2
  35. package/hooks/hooks-codex.json +1 -1
  36. package/hooks/hooks-cursor.json +11 -2
  37. package/hooks/hooks-pi.json +10 -0
  38. package/hooks/hooks.json +21 -1
  39. package/hooks/on-session-end.mjs +178 -18
  40. package/hooks/on-session-start.mjs +23 -0
  41. package/hooks/post-bash-write-verify.mjs +977 -0
  42. package/hooks/post-subagent-discovery-validator.mjs +256 -41
  43. package/hooks/pre-bash-destructive-guard.mjs +525 -160
  44. package/hooks/pre-bash-issue-budget.mjs +167 -0
  45. package/hooks/pre-bash-sessions-ledger-guard.mjs +627 -0
  46. package/hooks/pre-bash-templates-first.mjs +96 -63
  47. package/hooks/subagent-telemetry.mjs +527 -37
  48. package/package.json +5 -2
  49. package/pi/prompts/contract-version-bump.md +12 -0
  50. package/rules/README.md +32 -0
  51. package/scripts/archive-closed-prds.mjs +12 -22
  52. package/scripts/autopilot-multi.mjs +103 -20
  53. package/scripts/backfill-abandoned-sessions.mjs +160 -4
  54. package/scripts/check-doc-consistency.sh +17 -1
  55. package/scripts/eval-session.mjs +50 -9
  56. package/scripts/fleet-instruction-scan.mjs +141 -0
  57. package/scripts/lib/autopilot/mr-draft.mjs +31 -1
  58. package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
  59. package/scripts/lib/backlog-scan.mjs +39 -6
  60. package/scripts/lib/blocked-commands-policy.mjs +340 -0
  61. package/scripts/lib/ci-status-banner.mjs +75 -12
  62. package/scripts/lib/claude-md-budget-lint.mjs +283 -34
  63. package/scripts/lib/command-blocker.mjs +1013 -58
  64. package/scripts/lib/config/config-protection.mjs +2 -1
  65. package/scripts/lib/config/drift-check.mjs +9 -1
  66. package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
  67. package/scripts/lib/config/issue-budget.mjs +123 -0
  68. package/scripts/lib/config/reconcile.mjs +21 -0
  69. package/scripts/lib/config/section-extractor.mjs +121 -1
  70. package/scripts/lib/config-schema.mjs +23 -3
  71. package/scripts/lib/config.mjs +17 -0
  72. package/scripts/lib/convergence-monitor.mjs +49 -3
  73. package/scripts/lib/description-surface.mjs +535 -0
  74. package/scripts/lib/dispatcher/enumerate.mjs +26 -40
  75. package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
  76. package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
  77. package/scripts/lib/eval/engine.mjs +47 -5
  78. package/scripts/lib/events.mjs +59 -7
  79. package/scripts/lib/gates/gate-full.mjs +15 -3
  80. package/scripts/lib/gates/gate-helpers.mjs +132 -6
  81. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
  82. package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
  83. package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
  84. package/scripts/lib/handover-gate.mjs +7 -3
  85. package/scripts/lib/harness-audit/categories/category4.mjs +9 -3
  86. package/scripts/lib/instruction-budget-guard.mjs +402 -51
  87. package/scripts/lib/io.mjs +345 -10
  88. package/scripts/lib/issue-budget.mjs +269 -0
  89. package/scripts/lib/issue-close-strip-labels.mjs +39 -9
  90. package/scripts/lib/label-scope.mjs +47 -0
  91. package/scripts/lib/learnings/schema.mjs +43 -3
  92. package/scripts/lib/lock-reaper.mjs +1 -2
  93. package/scripts/lib/memory-proposals/schema.mjs +36 -1
  94. package/scripts/lib/peer-discovery.mjs +645 -0
  95. package/scripts/lib/pi-hook-bridge.mjs +146 -17
  96. package/scripts/lib/product-repo-detect.mjs +9 -8
  97. package/scripts/lib/project-hygiene.mjs +432 -0
  98. package/scripts/lib/quality-gate.mjs +167 -0
  99. package/scripts/lib/recommendations-v0.mjs +1 -1
  100. package/scripts/lib/reconcile/eligibility.mjs +1 -1
  101. package/scripts/lib/reconcile/emitter.mjs +23 -4
  102. package/scripts/lib/reconcile/engine.mjs +147 -39
  103. package/scripts/lib/reconcile/idempotency.mjs +114 -14
  104. package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
  105. package/scripts/lib/resource-probe/evaluate.mjs +70 -4
  106. package/scripts/lib/resource-probe.mjs +19 -0
  107. package/scripts/lib/rule-loader.mjs +6 -0
  108. package/scripts/lib/scope-baseline.mjs +564 -0
  109. package/scripts/lib/scope-gate.mjs +399 -98
  110. package/scripts/lib/session-close-backfill.mjs +61 -6
  111. package/scripts/lib/session-end/phase-skip.mjs +1 -0
  112. package/scripts/lib/session-id.mjs +221 -41
  113. package/scripts/lib/session-lock.mjs +304 -6
  114. package/scripts/lib/session-schema/constants.mjs +22 -3
  115. package/scripts/lib/session-schema/validator.mjs +16 -0
  116. package/scripts/lib/sessions-integrity-banner.mjs +294 -0
  117. package/scripts/lib/sessions-staleness-banner.mjs +121 -12
  118. package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
  119. package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
  120. package/scripts/lib/spiral-carryover.mjs +142 -30
  121. package/scripts/lib/state-md/mission-status.mjs +53 -3
  122. package/scripts/lib/subagents-schema.mjs +43 -9
  123. package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
  124. package/scripts/lib/tests-src-ratio.mjs +484 -0
  125. package/scripts/lib/validate/check-agents.mjs +56 -0
  126. package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
  127. package/scripts/lib/validate/check-rules.mjs +217 -35
  128. package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
  129. package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
  130. package/scripts/lib/validate-vendored-rules.mjs +10 -2
  131. package/scripts/lib/vault-archive.mjs +17 -2
  132. package/scripts/lib/vault-backfill/glab.mjs +8 -0
  133. package/scripts/lib/vault-mirror/process.mjs +30 -0
  134. package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
  135. package/scripts/lib/vcs-repo-spec.mjs +362 -0
  136. package/scripts/lib/wave-resource-gate.mjs +115 -11
  137. package/scripts/lib/worktree/listing.mjs +44 -7
  138. package/scripts/mcp-server.sh +17 -3
  139. package/scripts/measure-context-overhead.sh +151 -0
  140. package/scripts/memory-propose.mjs +72 -9
  141. package/scripts/print-applicable-rules.mjs +51 -12
  142. package/scripts/release.mjs +534 -0
  143. package/scripts/run-quality-gate.mjs +123 -5
  144. package/scripts/validate-wave-scope.mjs +182 -17
  145. package/scripts/vault-integration-watcher.mjs +32 -10
  146. package/skills/_shared/config-reading.md +2 -2
  147. package/skills/bootstrap/fast-template.md +1 -1
  148. package/skills/claude-md-drift-check/checker.mjs +145 -28
  149. package/skills/contract-version-bump/SKILL.md +219 -0
  150. package/skills/discovery/SKILL.md +4 -4
  151. package/skills/discovery/issue-templates.md +11 -11
  152. package/skills/discovery/probes-audit.md +1 -1
  153. package/skills/discovery/probes-feature.md +1 -1
  154. package/skills/discovery/probes-session.md +26 -5
  155. package/skills/ecosystem-health/SKILL.md +1 -1
  156. package/skills/ecosystem-health/wizard.md +4 -4
  157. package/skills/evolve/SKILL.md +1 -0
  158. package/skills/gitlab-ops/SKILL.md +20 -12
  159. package/skills/gitlab-portfolio/SKILL.md +2 -2
  160. package/skills/hook-development/SKILL.md +1 -1
  161. package/skills/mode-selector/SKILL.md +1 -1
  162. package/skills/npm-publish/SKILL.md +17 -1
  163. package/skills/plan/SKILL.md +5 -5
  164. package/skills/plan/mode-feature.md +4 -4
  165. package/skills/plan/mode-new.md +10 -10
  166. package/skills/plan/mode-retro.md +1 -1
  167. package/skills/quality-gates/SKILL.md +1 -1
  168. package/skills/reconcile/SKILL.md +21 -4
  169. package/skills/session-end/SKILL.md +34 -13
  170. package/skills/session-end/discovery-scan.md +4 -2
  171. package/skills/session-end/drift-operations.md +4 -4
  172. package/skills/session-end/metrics-collection.md +13 -0
  173. package/skills/session-end/phase-3-2-docs-verification.md +1 -1
  174. package/skills/session-end/phase-3-6-tail.md +2 -1
  175. package/skills/session-end/plan-verification.md +5 -2
  176. package/skills/session-end/vault-operations.md +1 -1
  177. package/skills/session-end/verification-checklist.md +1 -1
  178. package/skills/session-plan/SKILL.md +6 -2
  179. package/skills/session-plan/wave-template.md +2 -0
  180. package/skills/session-start/SKILL.md +73 -7
  181. package/skills/session-start/phase-4-5-resource-health.md +15 -2
  182. package/skills/test-runner/SKILL.md +2 -2
  183. package/skills/vault-sync/validator.mjs +108 -7
  184. package/skills/wave-executor/SKILL.md +5 -2
  185. package/skills/wave-executor/circuit-breaker.md +2 -0
  186. package/skills/wave-executor/wave-loop.md +163 -10
  187. package/templates/_shared/loop.md +4 -4
@@ -0,0 +1,444 @@
1
+ # Instruction Delivery — How `.claude/rules/` Actually Reaches an Agent
2
+
3
+ > Measurement + decision record for GitLab issue **#931 (Teil b)**, Epic **#929**.
4
+ > Measured 2026-07-30 on branch `feat/929-instruments-enforcement` at the working-tree
5
+ > state of commit `a2a8397`. Every number below is reproducible with the command
6
+ > printed next to it.
7
+
8
+ **This document answers a question with "no".** The issue asks whether the dormant
9
+ rule-loader should be wired into a second delivery path. The measurement says it
10
+ should not. The reasoning is in [§5](#5-recommendation).
11
+
12
+ ---
13
+
14
+ ## 1. The measured status quo
15
+
16
+ `.claude/rules/` holds **26 files, 169,961 bytes**:
17
+
18
+ ```console
19
+ $ find .claude/rules -name '*.md' | wc -l
20
+ 26
21
+ $ find .claude/rules -name '*.md' -exec cat {} + | wc -c
22
+ 169961
23
+ ```
24
+
25
+ All 26 reach the agent through Claude Code's **native project-instruction loading**.
26
+ Three independent checks establish that no `*.mjs` code path in this repo performs
27
+ the injection:
28
+
29
+ ```console
30
+ $ grep -c "^@" CLAUDE.md
31
+ 0
32
+ $ node -e 'const s=require("./.claude/settings.json");console.log(JSON.stringify(Object.keys(s)))'
33
+ ["permissions","hooks"]
34
+ $ git grep -n "loadApplicableRules" -- '*.mjs' | grep -v '^tests/' | grep -v '\* '
35
+ scripts/lib/instruction-budget-guard.mjs:81:import { loadApplicableRules } from './rule-loader.mjs';
36
+ scripts/lib/instruction-budget-guard.mjs:524: allEntries = loadApplicableRules({ rulesDir, scopePaths: [] });
37
+ scripts/lib/instruction-budget-guard.mjs:525: waveEntries = loadApplicableRules({ rulesDir, scopePaths: [], context: 'wave' });
38
+ scripts/lib/instruction-budget-guard.mjs:526: coordinatorEntries = loadApplicableRules({ rulesDir, scopePaths: [], context: 'coordinator' });
39
+ scripts/print-applicable-rules.mjs:253: rules = loadApplicableRules({ rulesDir, scopePaths, mode, hostClass, context });
40
+ ```
41
+
42
+ `CLAUDE.md` has no imports; `.claude/settings.json` carries only `permissions` and
43
+ `hooks`; and the only two `loadApplicableRules` consumers are a **measurement probe**
44
+ (`instruction-budget-guard.mjs`) and a **CLI nobody calls at delivery time**
45
+ (`print-applicable-rules.mjs`). The single `SessionStart` hook is a git-behind check,
46
+ not an injector:
47
+
48
+ ```console
49
+ $ node -e 'const s=require("./.claude/settings.json");console.log(s.hooks.SessionStart[0].hooks[0].command.slice(0,40))'
50
+ git fetch --quiet 2>/dev/null; BRANCH=$(git b
51
+ ```
52
+
53
+ **Consequence:** the `globs:` / `paths:` / `tier:` frontmatter is understood by
54
+ `rule-loader.mjs` alone, and `rule-loader.mjs` does not run on the delivery path.
55
+ The scoping metadata is inert at the moment it would have to matter.
56
+
57
+ ### 1.1 First-person confirmation (self-report, flagged as such)
58
+
59
+ The agent that produced this document ran as a **wave subagent** and received all
60
+ 26 rule files in its context — including the four `tier: coordinator-only` files
61
+ (`loop-and-monitor.md`, `owner-persona.md`, `mvp-scope.md`, `lsp.md`, together
62
+ 40,254 bytes) that a `context: 'wave'` load would have excluded, and including
63
+ glob-scoped rules whose globs do **not** intersect this wave's `allowedPaths`
64
+ (e.g. `anti-pattern-vi-restoreallmocks-…` scoped to `tests/lib/autopilot/**`,
65
+ `anti-pattern-agents-md-description-…` scoped to `agents/**` +
66
+ `scripts/lib/validate/**`).
67
+
68
+ This is introspective evidence about one context window, not a command transcript;
69
+ it is **corroborating**, not load-bearing. The load-bearing evidence is §1's three
70
+ grep/`node` checks, which show no `*.mjs` code path capable of applying the scoping.
71
+
72
+ ### 1.2 There IS a second call site — and the census that missed it is this repo's own anti-pattern
73
+
74
+ §1's third check greps `-- '*.mjs'`. The wave-executor of this repo is **not a
75
+ module**; it is a skill body, executed as prose by the coordinator. The filter
76
+ therefore excludes the one consumer class that matters:
77
+
78
+ ```console
79
+ $ git grep -ln "print-applicable-rules" -- 'skills'
80
+ skills/_shared/config-reading.md
81
+ skills/wave-executor/SKILL.md
82
+ skills/wave-executor/wave-loop.md
83
+ ```
84
+
85
+ `skills/wave-executor/wave-loop.md` § "Pre-Dispatch: Glob-Scoped Rule Injection
86
+ (#336/#694)" does not describe the injection as optional. It instructs the
87
+ coordinator to run `print-applicable-rules.mjs --context wave` once per wave and
88
+ **prepend the result to EACH agent's prompt**. That is the same execution mechanism
89
+ as every other wave-executor step.
90
+
91
+ So `docs/rule-authoring.md:8` — "The wave-executor calls it at each wave boundary
92
+ […] so a wave that touches only frontend files does not pay the token cost" — is
93
+ **not** documentation-vs-reality drift about a missing call site. The call site
94
+ exists as prose. What is wrong there is the *saving*: §2 measures it at 4.0% on a
95
+ real wave, not at the "does not pay" the sentence implies.
96
+
97
+ **This matters beyond bookkeeping.** §5 concludes that injecting alongside
98
+ undiminished native delivery costs +72% and is harmful — and a coordinator who
99
+ follows `wave-loop.md` literally does exactly that. The coordinator of the session
100
+ that produced this document noticed the size at dispatch time and declined to inject,
101
+ recording the deviation; the instruction itself was left standing. Naming the
102
+ diagnosis while leaving the instruction in place is the failure mode Epic #929 exists
103
+ to remove.
104
+
105
+ Note the shape of the mistake, because this repo already has a rule for it: a census
106
+ keyed on the payload (`loadApplicableRules` in `*.mjs`) misses every consumer that
107
+ pins only the channel (`print-applicable-rules.mjs` invoked from prose). See
108
+ `.claude/rules/anti-pattern-a-protocol-migration-census-keyed-on-the-payload-misses-every-consumer-that-pins-only-the-channel-18f3d0a.md`.
109
+
110
+ ### 1.3 A second delivery source outside this repo's control
111
+
112
+ The parent workspace contributes an additional file that `print-applicable-rules.mjs`
113
+ structurally cannot see — it only ever reads `<repoRoot>/.claude/rules`
114
+ (`scripts/print-applicable-rules.mjs:157`):
115
+
116
+ ```console
117
+ $ wc -c "$(dirname "$PWD")"/.claude/rules/parallel-sessions.md
118
+ 5623
119
+ ```
120
+
121
+ So the real delivered corpus is **175,584 bytes**, not 169,961. Any injected
122
+ replacement would reproduce only the 169,961-byte subset.
123
+
124
+ ---
125
+
126
+ ## 2. Rule-corpus breakdown
127
+
128
+ Produced by parsing each file's frontmatter through `parseGlobsFrontmatter()` from
129
+ `scripts/lib/rule-loader.mjs` (script in the session scratchpad; it only reads).
130
+
131
+ | Inclusion axis | Files | Bytes | Share |
132
+ |---|---:|---:|---:|
133
+ | **Always-on** (no `globs:` / `paths:`) | 12 | 109,169 | 64.2% |
134
+ | **Glob-scoped** (`globs:` present) | 14 | 60,792 | 35.8% |
135
+ | *of which carry `tier:`* | 15 | — | — |
136
+ | *of which carry `expires-at:`* | 11 | — | — |
137
+ | Frontmatter parse errors | 0 | — | — |
138
+
139
+ The five largest files carry **65.9%** of the whole corpus:
140
+
141
+ | Bytes | Share | File | Axis |
142
+ |---:|---:|---|---|
143
+ | 33,820 | 19.9% | `testing.md` | `globs: [tests/**, **/*.test.*, …]`, `tier: wave-only` |
144
+ | 33,377 | 19.6% | `loop-and-monitor.md` | always-on, `tier: coordinator-only` |
145
+ | 19,898 | 11.7% | `parallel-sessions.md` | always-on, `tier: always` |
146
+ | 16,713 | 9.8% | `security.md` | always-on, `tier: always` |
147
+ | 8,263 | 4.9% | `receiving-review.md` | always-on, `tier: always` |
148
+ | **112,071** | **65.9%** | **top 5 of 26** | |
149
+
150
+ The top **two** alone are 67,197 bytes = **39.5%**.
151
+
152
+ ---
153
+
154
+ ## 3. How much can scoping actually save?
155
+
156
+ Baseline for every row: **169,961 bytes** (what native delivery hands over today).
157
+ Each measurement used a throwaway scope file so the live `.claude/wave-scope.json`
158
+ was never touched:
159
+
160
+ ```console
161
+ $ node scripts/print-applicable-rules.mjs --wave-scope <tmp>.json --context wave | wc -c
162
+ ```
163
+
164
+ | `allowedPaths` set | Rules | Scoped bytes | Saved | Saved % |
165
+ |---|---:|---:|---:|---:|
166
+ | empty — matches nothing (the **floor**) | 8 | 69,024 | 100,937 | 59.4% |
167
+ | narrow — `scripts/lib/instruction-budget-guard.mjs` | 13 | 82,106 | 87,855 | 51.7% |
168
+ | medium — impl + test + doc (3 paths) | 14 | 115,932 | 54,029 | 31.8% |
169
+ | **broad — the live 20-path wave scope** | 18 | **122,875** | 47,086 | **27.7%** |
170
+ | match-all — a path hitting every glob | 25 | 168,156 | 1,805 | 1.1% |
171
+
172
+ ### 3.1 The decomposition that decides the issue
173
+
174
+ Running the same scopes **without** `--context` isolates the glob axis from the tier
175
+ axis. The result is the sharpest finding in this document:
176
+
177
+ | `allowedPaths` set | Glob axis alone | Saved % | + tier axis | Tier's contribution |
178
+ |---|---:|---:|---:|---:|
179
+ | narrow | 122,360 | 28.0% | 82,106 | 40,254 |
180
+ | medium | 156,186 | 8.1% | 115,932 | 40,254 |
181
+ | **broad (live wave)** | **163,129** | **4.0%** | 122,875 | **40,254** |
182
+ | match-all | 168,156 | 1.1% | 168,156 | 0 |
183
+
184
+ Two things fall out:
185
+
186
+ 1. **Glob scoping saves 4.0% on the real wave.** Not 40%, not 27% — 4.0%. The
187
+ headline 27.7% in §3 is almost entirely the *tier* axis wearing the glob axis's
188
+ clothes.
189
+ 2. **The tier axis's contribution is a constant 40,254 bytes**, identical across every
190
+ scope. It is not scope-dependent at all: it is exactly the four
191
+ `tier: coordinator-only` files. Nothing about `allowedPaths` influences it.
192
+
193
+ The reason glob scoping under-delivers is structural: the largest glob-scoped file,
194
+ `testing.md` (33,820 bytes = 19.9% of the corpus), is scoped on `tests/**`. Under this
195
+ repo's test-first discipline essentially every wave has a test path in scope, so the
196
+ single biggest "scoped" rule is matched almost always. Compare the narrow row (no test
197
+ file → 28.0%) with the medium row (one test file added → 8.1%): adding one test path
198
+ costs 33,820 bytes and consumes three quarters of the glob axis's benefit.
199
+
200
+ ### 3.2 Correction to the briefed figures
201
+
202
+ The task brief cited **117,732 bytes / 47 rules** for a 9-path scope. The byte figure
203
+ is plausible and close to my 20-path measurement of 122,875. **The rule count is not
204
+ reproducible**: the loader reads a single flat directory
205
+ (`readdirSync(rulesDir)`, non-recursive, `.md` only — `rule-loader.mjs:463-476`)
206
+ containing 26 files, so 47 cannot be produced by this CLI against this repo. My
207
+ reproduction of the live scope yields **18** rules. I could not re-measure the exact
208
+ 9-path scope because `.claude/wave-scope.json` had already been overwritten with the
209
+ current wave's 20 paths. Treat the 47 as an artifact of a different measurement.
210
+
211
+ ---
212
+
213
+ ## 4. Is the native delivery path disableable?
214
+
215
+ **Partly answerable from the repo; the decisive part is not.**
216
+
217
+ ### 4.1 What is established
218
+
219
+ - No repo-local mechanism performs the injection (§1), so the loading is Claude Code
220
+ native behaviour keyed on the file location.
221
+ - The repo **already maintains a rules directory that is not auto-loaded**: `rules/`
222
+ (10 files, 96,788 bytes) is the vendoring *library*, copied **into**
223
+ `.claude/rules/` by `/bootstrap --sync-rules` to become active
224
+ (`docs/rule-authoring.md:88-90`). Its existence demonstrates that "rules that live
225
+ outside `.claude/rules/` are not delivered" holds in this codebase — the
226
+ architectural option is real.
227
+ - The blast radius of relocating rules is measurable and large:
228
+
229
+ ```console
230
+ $ git grep -l "\.claude/rules/" | wc -l
231
+ 199
232
+ $ git grep -c "\.claude/rules/" | awk -F: '{s+=$2} END {print s}'
233
+ 584
234
+ ```
235
+
236
+ 199 tracked files carry 584 citations of the `.claude/rules/` path — agent
237
+ definitions, skills, docs, validators (`check-rules.mjs`,
238
+ `claude-md-drift-check`), the budget guard, and the vendoring contract that
239
+ consumer repos depend on.
240
+ - One delivery source is **outside this repo entirely** (§1.3): the parent
241
+ workspace's `.claude/rules/parallel-sessions.md`. Relocating this repo's rules
242
+ would not suppress it.
243
+
244
+ ### 4.2 What is NOT established — explicitly unbelegt
245
+
246
+ I found **no** artifact in this repo documenting Claude Code's auto-load rule for
247
+ `.claude/rules/*.md`: no settings schema, no upstream-doc excerpt, no ADR. Therefore
248
+ the following are **unverified inferences**, not findings:
249
+
250
+ - *that* `.claude/rules/` is the directory the native loader keys on (inferred from
251
+ the "project instructions, checked into the codebase" labelling plus the absence of
252
+ any other wiring);
253
+ - *whether* a settings key, env var, or `.claudeignore`-style mechanism can suppress
254
+ native loading without moving files;
255
+ - *whether* an injected block and a natively-loaded file would be deduplicated.
256
+
257
+ Per the task's instruction, these are marked unbelegt rather than asserted. Any plan
258
+ that depends on them must verify them against upstream documentation first.
259
+
260
+ ---
261
+
262
+ ## 5. Recommendation
263
+
264
+ > ## RECOMMENDATION: Do **not** build a second delivery path. Shrink the corpus instead.
265
+ >
266
+ > **Because glob scoping saves 4.0% on the real wave (163,129 vs 169,961 bytes), while
267
+ > the five largest of 26 files carry 65.9% of the load — the payoff is in the editor,
268
+ > not in a delivery mechanism.**
269
+
270
+ Rationale, in the order the measurements produced it:
271
+
272
+ 1. **Adding injection without disabling native delivery is strictly negative.** The
273
+ scoped block for the live wave is 122,875 bytes. Injected alongside an
274
+ undiminished 169,961-byte native load, the wave agent pays **292,836 bytes** —
275
+ a 72% *increase*. This option is not merely unhelpful; it is harmful, and it is
276
+ the option the issue's framing leads to by default.
277
+
278
+ 2. **Disabling native delivery buys 27.7% at a cost of 199 files and an unverified
279
+ premise.** It requires relocating rules out of `.claude/rules/`, rewriting 584
280
+ citations, breaking the vendoring contract consumer repos rely on, and re-pointing
281
+ `check-rules.mjs` / `claude-md-drift-check` / the budget guard — all resting on
282
+ §4.2's unverified inference about what triggers native loading. It would also
283
+ still leave the parent-workspace file (§1.3) in place.
284
+
285
+ 3. **The 27.7% is mostly not the glob axis anyway.** 40,254 of the 47,086 saved bytes
286
+ (85.5%) are the four `tier: coordinator-only` files. That is a *fixed* set, known
287
+ without any scope computation — no `wave-scope.json`, no glob engine, no injection
288
+ machinery is needed to identify it.
289
+
290
+ 4. **The concentration makes editing the cheaper instrument.** `loop-and-monitor.md`
291
+ is 33,377 bytes — 19.6% of the corpus, in every agent's context, and declared
292
+ `tier: coordinator-only`, i.e. its own frontmatter says wave agents do not need it.
293
+ `testing.md` is another 19.9%. Reducing those two files is a pure content decision
294
+ with zero delivery-mechanism risk, zero blast radius, and it pays off under the
295
+ delivery path that exists **today**.
296
+
297
+ 5. **The enforcement instrument already exists.** `instruction-budget-guard.mjs`
298
+ already measures the always-on byte load against a ceiling
299
+ (`byte-ceiling: 114000`, live 108,589 — consistent with the 109,169 always-on bytes
300
+ measured in §2, which include frontmatter the guard skips). Epic #929's enforcement
301
+ goal is served by tightening that ratchet, not by adding a delivery path.
302
+
303
+ ### 5.1 What to do instead (each independently landable)
304
+
305
+ | # | Action | Expected effect | Risk |
306
+ |---|---|---|---|
307
+ | 1 | Reduce `loop-and-monitor.md` (33,377 B). It is `tier: coordinator-only` yet reaches every agent; it is largely an upstream-feature changelog with 8 re-verify footers. | up to 19.6% of the corpus | none — content only |
308
+ | 2 | Reduce `testing.md` (33,820 B), the largest file and the one that defeats glob scoping (§3.1). | up to 19.9% | none — content only |
309
+ | 3 | Fix the false claim at `docs/rule-authoring.md:8` (§1.2). | correctness | none |
310
+ | 4 | Keep ratcheting `instruction-budget-guard`'s `byte-ceiling` downward as 1+2 land. | locks in the gains | none |
311
+
312
+ ### 5.2 If a second delivery path is ever revisited
313
+
314
+ It becomes worth reconsidering only when **all** of these hold — none do today:
315
+
316
+ - upstream documentation confirms §4.2's suppression question affirmatively;
317
+ - the corpus has been shrunk first, so the remaining glob-axis saving is measured on a
318
+ lean corpus rather than on `testing.md`'s bulk;
319
+ - `testing.md` has been split so its `tests/**` glob stops matching every wave;
320
+ - the 199-file / 584-citation migration is budgeted as its own epic with the consumer-
321
+ repo vendoring contract versioned.
322
+
323
+ Wiring that changes every session-start of this repo belongs behind its own operator
324
+ decision, not inside a measurement task.
325
+
326
+ ---
327
+
328
+ ## 6. The projects-baseline ablation axis (GitLab #936, T3)
329
+
330
+ > This section aligns the instruction-ablation eval to the **fleet-wide** lever and
331
+ > pre-registers the decision rule. It builds the alignment, not a run — the run
332
+ > stays the operator's call because it burns real API budget.
333
+
334
+ ### 6.1 Why the baseline is the lever, not this repo
335
+
336
+ §5 concludes: shrink the corpus, do not add a delivery path. §6 names *where* the
337
+ corpus lives. The `.claude/rules/` files reaching an agent are **not authored here** —
338
+ they are rolled out from `projects-baseline`. Measured 2026-07-30 (#936): **104,997
339
+ bytes are byte-identical across 14 repos**, all seeded from that baseline. A cut in
340
+ this repo's `.claude/rules/` is a single-repo fix that the next `/bootstrap
341
+ --sync-rules` can overwrite; a cut in the baseline propagates to every repo rolled
342
+ out from it.
343
+
344
+ This is consistent with §5's mechanism claim, not a new one: on Claude Code the
345
+ delivery path is native project-instruction loading (§1), so a baseline cut acts
346
+ purely through **corpus size, fleet-wide** — never through glob-scoping (which §3.1
347
+ measured at 4.0% on a real wave). The baseline axis is the same "editor, not delivery
348
+ mechanism" lever from §5, applied at the source that feeds 14 editors instead of one.
349
+
350
+ ### 6.2 The one command (host-local, never hardcoded)
351
+
352
+ `evals/instruction-ablation/run.mjs` now accepts `--rules-source repo|baseline`
353
+ (default `repo`, byte-identical to before). With `baseline` it sources the rule
354
+ corpus from the host-local `projects-baseline` instead of this repo. The path is
355
+ resolved through the same precedence as `plan-baseline-path` in
356
+ `scripts/lib/config.mjs` — **`SO_BASELINE_PATH` env → owner.yaml `baselines:` match →
357
+ owner.yaml `paths.baseline-path`** — so no machine-specific path is committed.
358
+
359
+ ```bash
360
+ # free — resolves the baseline, prints the corpus size + cost estimate, runs nothing
361
+ node evals/instruction-ablation/run.mjs --rules-source baseline --list
362
+
363
+ # the real run (operator's decision — ~USD 25 for these 14 cells)
364
+ node evals/instruction-ablation/run.mjs --rules-source baseline \
365
+ --cases vbc-001-verify-before-claiming --variants v0-full,v2-no-rules --runs 7
366
+ ```
367
+
368
+ `--list` reports which tier resolved the baseline, the corpus byte total (the
369
+ baseline corpus is larger than this repo's, so v0-full cells run a bigger context and
370
+ cost more than the repo axis), and refuses nothing. A real `--runs` against an
371
+ unresolved baseline exits `2` with a message pointing at `SO_BASELINE_PATH` /
372
+ `owner.yaml`, rather than silently falling back to the repo corpus. `CLAUDE.md` is
373
+ held constant (sourced from this repo) across both axes, so the rule corpus is the
374
+ only variable.
375
+
376
+ ### 6.3 The decision rule — pre-registered BEFORE any run
377
+
378
+ The asymmetry that governs this axis: a wrong **cut** ships a behavioural regression to
379
+ 14 repos at once; a wrong **keep** costs bytes. So the axis is fail-safe toward KEEP —
380
+ cutting requires a *positive* demonstration of no-effect, not merely the absence of a
381
+ significant one (n=7 cannot prove equivalence, only fail to reject it).
382
+
383
+ Run the candidate rule's case at `--runs 7`, `v0-full` (full baseline corpus) vs
384
+ `v2-no-rules` (empty corpus). Read the two pass rates against this table, fixed here
385
+ before the numbers exist so no result is rationalised after the fact:
386
+
387
+ | Outcome (v0-full vs v2-no-rules, n=7) | Reading | Baseline action |
388
+ |---|---|---|
389
+ | **Flat-identical** — 7/7 vs 7/7, or 0/7 vs 0/7 | the whole corpus's presence changed nothing this case can see | **CUT candidate, fleet-wide** — but per-section, never whole-file (see below) |
390
+ | **Clear separation** — v0 ≥ 5/7 **and** v2 ≤ 1/7 (Fisher p < 0.05) | the rule carries a real, fleet-wide behavioural effect | **KEEP in baseline** — it earns its 14-repo byte cost |
391
+ | **In-between** — e.g. 2/7 vs 0/7 (Fisher p ≈ 0.2), or any split not meeting the two rows above | signal, not significance (the #936-T2 zone) | **KEEP + raise n**, do not cut — under 14-repo blast radius, inconclusive defaults to keep |
392
+
393
+ Two guards carry over from the #936 Vorsession and bind here unchanged:
394
+
395
+ - **A case tests one claim, not a whole file.** `sec-007` passing proves only SQL
396
+ parameterisation, not that `security.md` (SSRF, XXE, secrets, supply-chain) is
397
+ cuttable. Even a flat-identical result makes a rule a **per-section cut candidate**,
398
+ never a whole-file delete candidate.
399
+ - **`--runs 1` is a smoke test, never a result.** The pilot's non-monotonic n=1
400
+ pattern (v1 FAIL while v2 PASS) inverted at n=3. Compare variants at n≥3, n=7 where a
401
+ result is close.
402
+
403
+ ### 6.4 The pre-registered expectation for `vbc-001`
404
+
405
+ `verification-before-completion.md` is the one rule that showed an effect (README:
406
+ 2/3 vs 0/3 at the repo axis, Fisher p ≈ 0.2 — signal, not significance). It ships from
407
+ the baseline like the rest. **Stated before the baseline run:** we expect
408
+ `v0-full` ≥ 5/7 and `v2-no-rules` ≤ 1/7 — i.e. the effect is real and
409
+ `verification-before-completion.md` should be **KEPT** in the baseline. If instead the
410
+ two converge (both ≥ 6/7 or both ≤ 1/7), the repo-axis signal was an artifact and the
411
+ rule becomes a per-section cut candidate fleet-wide. Anything in between raises n; it
412
+ does not cut. Writing this down before the run is the point: it stops a
413
+ 14-repo-affecting decision from being reverse-engineered out of whichever number
414
+ appears.
415
+
416
+ ---
417
+
418
+ ## Reproducing this document
419
+
420
+ ```bash
421
+ # corpus size
422
+ find .claude/rules -name '*.md' | wc -l
423
+ find .claude/rules -name '*.md' -exec cat {} + | wc -c
424
+
425
+ # no repo-local injector
426
+ grep -c "^@" CLAUDE.md
427
+ node -e 'const s=require("./.claude/settings.json");console.log(Object.keys(s))'
428
+ git grep -n "loadApplicableRules" -- '*.mjs' | grep -v '^tests/'
429
+
430
+ # scoped block for an arbitrary scope (never overwrite .claude/wave-scope.json)
431
+ echo '{"allowedPaths":["scripts/lib/x.mjs"]}' > /tmp/scope.json
432
+ node scripts/print-applicable-rules.mjs --wave-scope /tmp/scope.json --context wave | wc -c
433
+ node scripts/print-applicable-rules.mjs --wave-scope /tmp/scope.json | wc -c # glob axis only
434
+
435
+ # blast radius
436
+ git grep -l "\.claude/rules/" | wc -l
437
+ ```
438
+
439
+ ## See also
440
+
441
+ - `scripts/lib/rule-loader.mjs` — `loadApplicableRules()`, the loader that does not run at delivery time
442
+ - `scripts/print-applicable-rules.mjs` — the CLI bridge; reads only `<repoRoot>/.claude/rules`
443
+ - `scripts/lib/instruction-budget-guard.mjs` — the existing measurement + ceiling instrument
444
+ - `docs/rule-authoring.md` — frontmatter contract (and the stale claim at line 8)
@@ -5,7 +5,7 @@
5
5
 
6
6
  ## Purpose & Overview
7
7
 
8
- Files under `.claude/rules/*.md` are engineering rules injected into agent prompts. The loader — `loadApplicableRules()` in [`scripts/lib/rule-loader.mjs`](../scripts/lib/rule-loader.mjs) — reads every `*.md` file in the rules directory, parses its optional YAML frontmatter, and returns the subset applicable to a given wave. The wave-executor calls it at each wave boundary with the wave's `allowedPaths` (from `wave-scope.json`) as `scopePaths`, so a wave that touches only frontend files does not pay the token cost of backend or Swift rules.
8
+ Files under `.claude/rules/*.md` are engineering rules injected into agent prompts. The loader — `loadApplicableRules()` in [`scripts/lib/rule-loader.mjs`](../scripts/lib/rule-loader.mjs) — reads every `*.md` file in the rules directory, parses its optional YAML frontmatter, and returns the subset applicable to a given wave. The wave-executor calls it at each wave boundary with the wave's `allowedPaths` (from `wave-scope.json`) as `scopePaths` see `skills/wave-executor/wave-loop.md` § "Pre-Dispatch: Glob-Scoped Rule Injection". **The saving that scoping buys is smaller than it looks, and on Claude Code it can be negative:** measured 2026-07-30 on a real wave, the glob axis saved 4.0% of a 169,961-byte corpus, and because Claude Code already delivers every `.claude/rules/*.md` through native project-instruction loading, prepending the block on top costs +72% rather than saving anything. Scoping pays where the harness does NOT auto-load the directory (Codex CLI, Pi, Cursor). Full measurement: [`docs/instruction-delivery.md`](instruction-delivery.md).
9
9
 
10
10
  Two rule categories existed before FA1:
11
11
 
@@ -32,6 +32,7 @@ All keys are optional. Unknown keys are **ignored without error** — adding a n
32
32
  | `auto-generated` | `boolean` | no | Marks a rule produced by the FA2 reconciliation engine (not hand-authored). Triggers the never-always-on invariant (see below). | `auto-generated: true` |
33
33
  | `confidence` | `number` (0..1) | no | Confidence of the source learning that generated the rule. Mirrors the `learnings.jsonl` confidence field. | `confidence: 0.85` |
34
34
  | `tier` | `string` (`always` \| `coordinator-only` \| `wave-only`) | no | Load-context tier (issue #692). Gates which contexts the rule loads in, via the `context` param to `loadApplicableRules`. Absent = no tier gating (backward-compatible). See [Tier gating](#tier-gating-issue-692) below. | `tier: coordinator-only` |
35
+ | `review-date` | `string` (ISO 8601 date) | no | **check-rules.mjs-only, advisory (#880 FA5).** Periodic-review marker for handwritten rules. NOT read by `rule-loader.mjs` (not in `SCALAR_META_KEYS`) — zero effect on loading. Deliberately distinct from `expires-at`, which IS a live gate. See [Handwritten Rule Review Date](#handwritten-rule-review-date-880-fa5) below. | `review-date: 2026-10-23` |
35
36
 
36
37
  **Surfaced names.** The loader normalises kebab-case YAML keys to camelCase on the rule entry: `host-class` → `hostClass`, `expires-at` → `expiresAt`, `learning-key` → `learningKey`, `auto-generated` → `autoGenerated`. (`tier` is already a single lowercase token, so it is surfaced unchanged as `tier`.)
37
38
 
@@ -131,17 +132,59 @@ Absent tag = universal (vendored to every consumer repo, the default and fully b
131
132
 
132
133
  ## The Never-Always-On Invariant (Auto-Generated Rules)
133
134
 
134
- > Forward-reference to FA4 (issue #697). The CI gate described here lands with FA4 in `scripts/lib/validate/check-rules.mjs`; FA1 documents the contract that gate will enforce.
135
+ > Delivered by FA4 (issue #697) in `scripts/lib/validate/check-rules.mjs`, wired into CI via `scripts/validate-plugin.mjs`. FA1 (this doc) specifies the contract that gate enforces.
135
136
 
136
137
  Hand-authored rules are the curated, cross-cutting baseline. **Auto-generated rules are *extra* rules** — narrow, learning-derived, and time-boxed. They must never inflate the always-on instruction budget (cross-ref #668 instruction-budget). The brandmauer (firewall) is:
137
138
 
138
139
  Any rule with `auto-generated: true` **MUST**:
139
140
 
140
- 1. Carry **at least one activation axis** — `globs`, `mode`, or `host-class`. It must **NOT** be always-on.
141
+ 1. Carry **at least one activation axis** — `globs` or `host-class`. It must **NOT** be always-on.
141
142
  2. Carry a `learning-key` (provenance — which learning produced it).
142
143
  3. Carry an `expires-at` (time-box — auto-generated rules are not permanent).
143
144
 
144
- A rule that sets `auto-generated: true` but lacks an activation axis, or omits `learning-key` / `expires-at`, is a violation. The FA4 CI gate (`scripts/lib/validate/check-rules.mjs`) will fail the build on such a rule. This keeps every machine-authored rule conditional and self-expiring — the always-on surface stays the hand-curated baseline.
145
+ A rule that sets `auto-generated: true` but lacks an activation axis, or omits `learning-key` / `expires-at`, is a violation. The FA4 CI gate (`scripts/lib/validate/check-rules.mjs`) fails the build on such a rule (exit 1). This keeps every machine-authored rule conditional and self-expiring — the always-on surface stays the hand-curated baseline.
146
+
147
+ > Note: `scripts/lib/reconcile/emitter.mjs`'s own module doc additionally lists `mode` as a third accepted axis for the *emitter's* internal throw-guard (the pure function that produces a rule's activation metadata before it is ever written to disk). `check-rules.mjs`'s CI gate — which audits `.claude/rules/*.md` files already on disk — checks only `globs`/`host-class`, not `mode`. This is a pre-existing discrepancy between the emitter's internal guard and the CI gate's scope, not something #880 (below) introduced or resolved; every currently-emitted rule uses `globs` as its axis in practice, so the discrepancy has not yet produced a false pass.
148
+
149
+ ## Handwritten Rule Review Date (#880 FA5)
150
+
151
+ The invariant above only binds the **machine author** (the FA2 reconciliation engine, via the emitter's throw-guard). A human authoring a `.claude/rules/*.md` file by hand bypasses it entirely — there was previously no check at all on handwritten rules' activation scoping or on when they were last reviewed. Issue #880 found that most of this repo's handwritten rules carry no `globs`/`paths`/`host-class` frontmatter and no periodic-review marker, and nothing in the system ever prompted a re-review.
152
+
153
+ **Correction to a common misreading:** "no `globs`/`paths`/`host-class`" does **not** mean "no activation axis at all." Since issue #692, `tier:` (`always` | `coordinator-only` | `wave-only`) is a real load-context gating axis — `rule-loader.mjs`'s `applyGates()` excludes `coordinator-only` rules from wave context and `wave-only` rules from coordinator context (see [Tier gating](#tier-gating-issue-692) above). Every handwritten rule in this repo already carries a `tier:` key. Treating `tier:` as *not* an axis and flagging all of them as "no activation axis" would be both factually wrong and operationally dangerous: the obvious-looking fix — adding a `globs:`/`paths:` filter to a rule that is intentionally always-on (e.g. `security.md`, `verification-before-completion.md`) — would silently stop that rule from loading in most waves, a live behaviour change to a safety-critical directive disguised as a metadata fix. **`check-rules.mjs` therefore counts `tier:` as a valid activation axis for handwritten rules, on equal footing with `globs`/`paths`/`host-class`.**
154
+
155
+ ### The symmetric check (WARN-only, `check-rules.mjs`)
156
+
157
+ For every `.claude/rules/*.md` file **without** `auto-generated: true`, the gate checks:
158
+
159
+ 1. **Activation axis** — a non-empty `globs` array, its `paths` alias (#795), a `host-class` key, **or** a `tier` key. Missing all four → `WARN: ... no activation axis ...`.
160
+ 2. **`review-date`** (ISO 8601, e.g. `2026-10-23`) — a periodic-review marker. Missing → `WARN: ... missing a review-date ...`.
161
+
162
+ > **`globs: []` is not a fourth way to pass — and no other axis can rescue it.** An *empty* `globs` array is not the same as an *absent* one. `rule-loader.mjs` excludes such a rule unconditionally (`if (globs.length === 0) continue;`), and that check runs **after** `applyGates()` — so a co-present `tier:`, `host-class:` or `mode:` cannot bring it back. The rule matches nothing and never loads, in any context. `check-rules.mjs` therefore emits its own distinct WARN for `globs: []` that wins over the axis check, rather than reporting the (opposite) "loads always-on". Read rule 1 as a flat OR only for a *non-empty* or *absent* `globs`.
163
+
164
+ Both checks are **advisory (WARN), not a build failure** — `check-rules.mjs`'s exit code is driven **solely** by the pre-existing auto-generated hard-fail invariants above. A handwritten-rule WARN never turns CI red. Promoting this to a hard gate is a later, deliberate step (not part of #880), once every handwritten rule in the fleet has a `review-date`.
165
+
166
+ ### Why `review-date`, not `expires-at`
167
+
168
+ `expires-at` was deliberately **not** reused as the handwritten review marker. `rule-loader.mjs`'s `applyGates()` treats `expires-at` as a **live expiry gate**: once the date passes, the rule is silently **excluded** from every load (fail-open only on a malformed date — see [Fail-open on parse error](#fail-open-on-parse-error) above). Stamping `expires-at` on an always-on safety rule (`security.md`, `parallel-sessions.md`, …) as a "please review this periodically" reminder would mean that rule **actually stops loading** the day the reminder date passes — the opposite of what a review marker should do.
169
+
170
+ `review-date` is a **new, inert** frontmatter key instead:
171
+
172
+ - It is **not** in `rule-loader.mjs`'s `SCALAR_META_KEYS` allowlist (that module is contract-locked as of #880 — its allowlist was not extended). An unrecognised key is ignored without error per the existing parser contract, so `review-date` has **zero effect** on `loadApplicableRules()` / `applyGates()` — it is parsed only by `check-rules.mjs`, via a small local regex helper (`hasFrontmatterKey()`) that scans the raw frontmatter block directly.
173
+ - Format: a bare ISO 8601 date (`YYYY-MM-DD`), same shape as `expires-at`, but purely advisory.
174
+
175
+ ```markdown
176
+ ---
177
+ tier: always
178
+ review-date: 2026-10-23
179
+ ---
180
+ # Security Rules (Always-on)
181
+ ```
182
+
183
+ ### Current fleet state (as migrated by #880)
184
+
185
+ **Every** handwritten rule in this repo's own `.claude/rules/` carries a `review-date`: the #880 migration added one to the then-current set (2026-07-25, +90 days from the session date), and every rule added since ships with one from the start. None gained a new `globs:`/`paths:` axis — every file's pre-existing activation-axis state (`tier:`, with or without `globs:`) is unchanged; only the `review-date:` line was added. The auto-generated rules are untouched by this section (they already satisfy the FA4 invariant in full).
186
+
187
+ Four of them (`loop-and-monitor.md`, `lsp.md`, `mvp-scope.md`, `owner-persona.md`) carry `tier: coordinator-only` — genuinely excluded from wave context, not part of the wave-time always-on budget despite lacking `globs:`. The rest carry either `tier: always` or `tier: wave-only` + `globs:` (`bash-harness-pitfalls.md`, `cli-design.md`, `testing.md`) — the `tier: always` cohort is the genuinely unconditional, every-context, every-wave one.
145
188
 
146
189
  ## Learning Type-Taxonomy, TTL & Provenance Standard (issue #723 B6 / #733)
147
190
 
@@ -160,8 +203,8 @@ Transcribed verbatim from `LEARNING_TYPE_REGISTRY` (16 types):
160
203
  | `fragile-file` | 45 | true | true |
161
204
  | `effective-sizing` | 45 | true | false |
162
205
  | `recurring-issue` | 45 | true | true |
163
- | `workflow-pattern` | 90 | true | false |
164
- | `proven-pattern` | 90 | true | false |
206
+ | `workflow-pattern` | 90 | true | true |
207
+ | `proven-pattern` | 90 | true | true |
165
208
  | `anti-pattern` | 90 | true | true |
166
209
  | `autopilot-effectiveness` | 90 | true | false |
167
210
  | `autonomy-verdict` | 90 | false | false |
@@ -174,7 +217,11 @@ Transcribed verbatim from `LEARNING_TYPE_REGISTRY` (16 types):
174
217
 
175
218
  Capability axes:
176
219
  - **`agentProposable`** — the type may appear in `PROPOSAL_TYPES` (a wave-agent may `memory.propose()` this type). `autonomy-verdict`, `fragile-pattern`, and `stagnation-class-frequency` are `false` — these are analyzer-synthesized classes, not agent-observed, so they are never agent-proposable.
177
- - **`ruleConvertible`** — the type may appear in `CONVERT_TYPES` (the FA2 reconciliation engine may convert a learning of this type into a conditional `.claude/rules/*.md` proposal). `fragile-file`, `recurring-issue`, `anti-pattern`, `convention`, `architecture-pattern`, `design-pattern`, `fragile-pattern`, and `stagnation-class-frequency` are the eight `ruleConvertible: true` types.
220
+ - **`ruleConvertible`** — the type may appear in `CONVERT_TYPES` (the FA2 reconciliation engine may convert a learning of this type into a conditional `.claude/rules/*.md` proposal). `fragile-file`, `recurring-issue`, `anti-pattern`, `convention`, `architecture-pattern`, `design-pattern`, `fragile-pattern`, `stagnation-class-frequency`, `workflow-pattern`, and `proven-pattern` are the ten `ruleConvertible: true` types (issue #900 flipped the last two from `false` — the real corpus census showed a large volume of these records carrying usable `file_paths` scope that were structurally unconvertible before the flip).
221
+
222
+ ### Type aliasing (issue #900)
223
+
224
+ The real learnings corpus also accumulated free-form type names that were never registered — the same semantic classes as two registered types, written with a different literal. `LEARNING_TYPE_ALIASES` in `scripts/lib/learnings/schema.mjs` maps these to their canonical counterpart (`gotcha` → `anti-pattern`, `pattern` → `proven-pattern`), applied by `normalizeDialects()` on both the read and write/migration funnels — mirroring the existing `files` → `file_paths` dialect-normalization pattern one level up (a type-name alias instead of a field-name alias). No alias key may collide with a registry key (guarded by a test in `tests/lib/learnings-schema-normalization.test.mjs`).
178
225
 
179
226
  `LEARNING_TTL_DAYS[type]` derives its value from `LEARNING_TYPE_REGISTRY[type].ttlDays` for every listed type, plus a `default: 60` fallback entry for any type not present in the registry (`deriveExpiresAt()` looks up `LEARNING_TTL_DAYS[type] ?? LEARNING_TTL_DAYS.default`).
180
227
 
@@ -303,7 +350,9 @@ timeout masks real perf regressions.
303
350
 
304
351
  - [`skills/_shared/config-reading.md`](../skills/_shared/config-reading.md) § "Glob-Scoped Rule Injection (#336)" — wave-boundary injection flow + match algorithm
305
352
  - [`scripts/lib/rule-loader.mjs`](../scripts/lib/rule-loader.mjs) — `loadApplicableRules()` implementation (the contract this doc specifies)
306
- - `scripts/lib/validate/check-rules.mjs` — FA4 CI validation gate (forward-reference; lands with #697)
353
+ - `scripts/lib/validate/check-rules.mjs` — FA4 hard-fail CI gate (auto-generated invariants, #697) + FA5 warn-mode symmetric check (handwritten rules, #880)
354
+ - [`tests/lib/validate/check-rules.test.mjs`](../tests/lib/validate/check-rules.test.mjs) — auto-generated invariant coverage (FA4 #697)
355
+ - [`tests/rules/check-rules-handwritten.test.mjs`](../tests/rules/check-rules-handwritten.test.mjs) — handwritten warn-mode coverage (#880 FA5)
307
356
  - [`scripts/print-applicable-rules.mjs`](../scripts/print-applicable-rules.mjs) — `--context wave|coordinator` flag exercises the tier gate (#692)
308
357
  - [`scripts/lib/validate-vendored-rules.mjs`](../scripts/lib/validate-vendored-rules.mjs) — pre-write vendoring validator (issue #722 Epic A Wave 2)
309
358
  - [`scripts/lib/rules-sync.mjs`](../scripts/lib/rules-sync.mjs) — `syncRules()` implementation, archetype resolution (issue #722 Epic A)
@@ -313,4 +362,4 @@ timeout masks real perf regressions.
313
362
  - [`scripts/lib/reconcile/eligibility.mjs`](../scripts/lib/reconcile/eligibility.mjs) — `CONVERT_TYPES` (derived from `LEARNING_TYPE_REGISTRY`), rule-conversion eligibility gates
314
363
  - [`scripts/lib/memory-proposals/schema.mjs`](../scripts/lib/memory-proposals/schema.mjs) — `PROPOSAL_TYPES` (derived from `LEARNING_TYPE_REGISTRY`)
315
364
  - [`docs/session-config-reference.md`](session-config-reference.md#reconcile-693--696--697) § Reconcile — `reconcile.rule-expiry-days` / `min-rule-days` / `min-insight-chars` config keys that tune the emitted `expires-at` and eligibility gates
316
- - Issues: #336 (glob-scoping), #668 (instruction-budget), #692 (tier load-context gating), #693 (Rule Activation epic), #694 (FA1 foundation), #697 (FA4 validation), #722 (vendoring validation + archetype-scoped manifest), #723 B6 / #733 (type-taxonomy + provenance standard)
365
+ - Issues: #336 (glob-scoping), #668 (instruction-budget), #692 (tier load-context gating), #693 (Rule Activation epic), #694 (FA1 foundation), #697 (FA4 validation), #722 (vendoring validation + archetype-scoped manifest), #723 B6 / #733 (type-taxonomy + provenance standard), #880 (FA5 — handwritten-rule symmetric check, warn mode)