session-orchestrator 3.17.0 → 3.20.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/030-wave-execution.mdc +17 -1
- package/CHANGELOG.md +185 -412
- package/README.md +12 -9
- package/SECURITY.md +190 -27
- package/agents/AGENTS.md +20 -3
- package/agents/code-implementer.md +6 -6
- package/agents/db-specialist.md +1 -1
- package/agents/qa-strategist.md +31 -6
- package/agents/schemas/qa-strategist.schema.json +27 -0
- package/agents/schemas/test-writer.schema.json +60 -2
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +1 -1
- package/agents/test-writer.md +29 -10
- package/agents/ui-developer.md +1 -1
- package/commands/contract-version-bump.md +28 -0
- package/commands/portfolio.md +1 -1
- package/commands/session.md +6 -2
- package/docs/USER-GUIDE.md +9 -4
- package/docs/ci-setup.md +121 -7
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +6 -6
- package/docs/cursor-setup.md +22 -9
- package/docs/events-schema.md +5 -1
- package/docs/instruction-delivery.md +794 -0
- package/docs/rule-authoring.md +58 -9
- package/docs/session-config-reference.md +245 -50
- package/docs/session-config-template.md +39 -26
- package/hooks/_lib/guard-source-loader.mjs +680 -0
- package/hooks/_lib/lock-bootstrap.mjs +21 -0
- package/hooks/_lib/vcs-create-matcher.mjs +119 -0
- package/hooks/config-protection.mjs +0 -0
- package/hooks/enforce-commands.mjs +226 -19
- package/hooks/enforce-scope.mjs +133 -9
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +11 -2
- package/hooks/hooks-pi.json +10 -0
- package/hooks/hooks.json +21 -1
- package/hooks/on-session-end.mjs +178 -18
- package/hooks/on-session-start.mjs +30 -4
- package/hooks/post-bash-write-verify.mjs +977 -0
- package/hooks/post-subagent-discovery-validator.mjs +256 -41
- package/hooks/pre-bash-destructive-guard.mjs +616 -164
- package/hooks/pre-bash-issue-budget.mjs +167 -0
- package/hooks/pre-bash-sessions-ledger-guard.mjs +1054 -0
- package/hooks/pre-bash-templates-first.mjs +96 -63
- package/hooks/subagent-telemetry.mjs +527 -37
- package/package.json +6 -3
- package/pi/prompts/contract-version-bump.md +12 -0
- package/rules/README.md +32 -0
- package/scripts/archive-closed-prds.mjs +12 -22
- package/scripts/autopilot-multi.mjs +103 -20
- package/scripts/backfill-abandoned-sessions.mjs +160 -4
- package/scripts/backfill-learnings-from-vault.mjs +967 -0
- package/scripts/check-doc-consistency.sh +17 -1
- package/scripts/emit-session.mjs +3 -40
- package/scripts/eval-session.mjs +50 -9
- package/scripts/fleet-instruction-scan.mjs +141 -0
- package/scripts/lib/autopilot/mr-draft.mjs +31 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
- package/scripts/lib/backlog-scan.mjs +39 -6
- package/scripts/lib/blocked-commands-policy.mjs +340 -0
- package/scripts/lib/ci-status-banner.mjs +75 -12
- package/scripts/lib/claude-md-budget-lint.mjs +283 -34
- package/scripts/lib/command-blocker.mjs +1273 -58
- package/scripts/lib/config/config-protection.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +9 -1
- package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
- package/scripts/lib/config/issue-budget.mjs +123 -0
- package/scripts/lib/config/reconcile.mjs +21 -0
- package/scripts/lib/config/section-extractor.mjs +121 -1
- package/scripts/lib/config-schema.mjs +23 -3
- package/scripts/lib/config.mjs +17 -0
- package/scripts/lib/convergence-monitor.mjs +49 -3
- package/scripts/lib/description-surface.mjs +535 -0
- package/scripts/lib/dispatcher/enumerate.mjs +26 -40
- package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
- package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
- package/scripts/lib/eval/engine.mjs +47 -5
- package/scripts/lib/events.mjs +59 -7
- package/scripts/lib/gates/gate-full.mjs +15 -3
- package/scripts/lib/gates/gate-helpers.mjs +132 -6
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
- package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
- package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
- package/scripts/lib/handover-gate.mjs +7 -3
- package/scripts/lib/hardening.mjs +9 -9
- package/scripts/lib/harness-audit/categories/category4.mjs +9 -3
- package/scripts/lib/instruction-budget-guard.mjs +402 -51
- package/scripts/lib/io.mjs +345 -10
- package/scripts/lib/issue-budget.mjs +269 -0
- package/scripts/lib/issue-close-strip-labels.mjs +39 -9
- package/scripts/lib/label-scope.mjs +47 -0
- package/scripts/lib/learnings/affinity.mjs +434 -0
- package/scripts/lib/learnings/candidates.mjs +736 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +408 -53
- package/scripts/lib/learnings/judgment.mjs +782 -0
- package/scripts/lib/learnings/kebab.mjs +128 -0
- package/scripts/lib/learnings/schema.mjs +43 -3
- package/scripts/lib/learnings/select.mjs +550 -0
- package/scripts/lib/lock-reaper.mjs +1 -2
- package/scripts/lib/memory-proposals/schema.mjs +36 -1
- package/scripts/lib/peer-discovery.mjs +645 -0
- package/scripts/lib/pi-hook-bridge.mjs +146 -17
- package/scripts/lib/product-repo-detect.mjs +9 -8
- package/scripts/lib/project-hygiene.mjs +432 -0
- package/scripts/lib/quality-gate.mjs +167 -0
- package/scripts/lib/recommendations-v0.mjs +1 -1
- package/scripts/lib/reconcile/eligibility.mjs +1 -1
- package/scripts/lib/reconcile/emitter.mjs +128 -24
- package/scripts/lib/reconcile/engine.mjs +156 -54
- package/scripts/lib/reconcile/idempotency.mjs +114 -14
- package/scripts/lib/reconcile/renderer.mjs +141 -25
- package/scripts/lib/reconcile/sanitize.mjs +518 -0
- package/scripts/lib/reconcile/writer.mjs +95 -1
- package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
- package/scripts/lib/resource-probe/evaluate.mjs +70 -4
- package/scripts/lib/resource-probe.mjs +19 -0
- package/scripts/lib/rule-loader.mjs +6 -0
- package/scripts/lib/scope-baseline.mjs +564 -0
- package/scripts/lib/scope-gate.mjs +568 -145
- package/scripts/lib/session-close-backfill.mjs +63 -8
- package/scripts/lib/session-end/phase-skip.mjs +1 -0
- package/scripts/lib/session-id.mjs +221 -41
- package/scripts/lib/session-lock.mjs +304 -6
- package/scripts/lib/session-record-repair.mjs +551 -0
- package/scripts/lib/session-schema/constants.mjs +22 -3
- package/scripts/lib/session-schema/serializer.mjs +54 -0
- package/scripts/lib/session-schema/validator.mjs +16 -0
- package/scripts/lib/session-schema.mjs +1 -0
- package/scripts/lib/session-token-rollup.mjs +68 -6
- package/scripts/lib/sessions-integrity-banner.mjs +294 -0
- package/scripts/lib/sessions-staleness-banner.mjs +121 -12
- package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
- package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
- package/scripts/lib/soul-resolve.mjs +12 -0
- package/scripts/lib/spiral-carryover.mjs +142 -30
- package/scripts/lib/state-md/mission-status.mjs +53 -3
- package/scripts/lib/subagents-schema.mjs +43 -9
- package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
- package/scripts/lib/tests-src-ratio.mjs +484 -0
- package/scripts/lib/tmux-layout/telemetry.mjs +43 -10
- package/scripts/lib/validate/check-agents.mjs +56 -0
- package/scripts/lib/validate/check-banner-parity.mjs +376 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +1148 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
- package/scripts/lib/validate/check-learning-provenance.mjs +511 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +3 -3
- package/scripts/lib/validate/check-rules.mjs +244 -36
- package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-unwired-features.mjs +549 -0
- package/scripts/lib/validate-vendored-rules.mjs +10 -2
- package/scripts/lib/vault-archive.mjs +17 -2
- package/scripts/lib/vault-backfill/glab.mjs +8 -0
- package/scripts/lib/vault-mirror/process.mjs +30 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
- package/scripts/lib/vcs-repo-spec.mjs +362 -0
- package/scripts/lib/wave-resource-gate.mjs +115 -11
- package/scripts/lib/worktree/listing.mjs +44 -7
- package/scripts/mcp-server.sh +17 -3
- package/scripts/measure-context-overhead.sh +151 -0
- package/scripts/memory-propose.mjs +72 -9
- package/scripts/print-applicable-rules.mjs +218 -16
- package/scripts/print-learnings-index.mjs +474 -0
- package/scripts/release.mjs +534 -0
- package/scripts/repair-invalid-sessions.mjs +209 -0
- package/scripts/run-quality-gate.mjs +123 -5
- package/scripts/sweep-expired-learnings.mjs +192 -32
- package/scripts/validate-plugin.mjs +21 -0
- package/scripts/validate-wave-scope.mjs +182 -17
- package/scripts/vault-integration-watcher.mjs +32 -10
- package/skills/_shared/config-reading.md +2 -2
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/brainstorm/soul.md +47 -1
- package/skills/claude-md-drift-check/checker.mjs +145 -28
- package/skills/contract-version-bump/SKILL.md +219 -0
- package/skills/discovery/SKILL.md +4 -4
- package/skills/discovery/issue-templates.md +11 -11
- package/skills/discovery/probes-audit.md +1 -1
- package/skills/discovery/probes-feature.md +1 -1
- package/skills/discovery/probes-session.md +26 -5
- package/skills/ecosystem-health/SKILL.md +1 -1
- package/skills/ecosystem-health/wizard.md +4 -4
- package/skills/evolve/SKILL.md +117 -18
- package/skills/gitlab-ops/SKILL.md +25 -12
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/grill/soul.md +44 -1
- package/skills/hook-development/SKILL.md +1 -1
- package/skills/mode-selector/SKILL.md +1 -1
- package/skills/npm-publish/SKILL.md +17 -1
- package/skills/plan/SKILL.md +5 -5
- package/skills/plan/mode-feature.md +4 -4
- package/skills/plan/mode-new.md +10 -10
- package/skills/plan/mode-retro.md +1 -1
- package/skills/plan/soul.md +46 -3
- package/skills/quality-gates/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +21 -4
- package/skills/session-end/SKILL.md +34 -36
- package/skills/session-end/discovery-scan.md +4 -2
- package/skills/session-end/drift-operations.md +4 -4
- package/skills/session-end/metrics-collection.md +13 -0
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +32 -2
- package/skills/session-end/plan-verification.md +6 -7
- package/skills/session-end/session-metrics-write.md +2 -0
- package/skills/session-end/vault-operations.md +1 -1
- package/skills/session-end/verification-checklist.md +1 -1
- package/skills/session-plan/SKILL.md +6 -2
- package/skills/session-plan/wave-template.md +2 -0
- package/skills/session-start/SKILL.md +75 -7
- package/skills/session-start/phase-4-5-resource-health.md +15 -2
- package/skills/session-start/soul.md +41 -1
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +108 -7
- package/skills/wave-executor/SKILL.md +6 -7
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/wave-loop.md +198 -80
- package/templates/_shared/loop.md +4 -4
|
@@ -0,0 +1,794 @@
|
|
|
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
|
+
## 7. The learnings index is not a second delivery path (GitLab #1014)
|
|
419
|
+
|
|
420
|
+
> Measured **2026-08-13** at commit `c87102b`, branch `feat/memory-pipeline-1015-1014-1016`.
|
|
421
|
+
> `.claude/rules/` and `CLAUDE.md` were **clean at HEAD** for every measurement below
|
|
422
|
+
> (`git status --porcelain -- .claude/rules CLAUDE.md` → 0 lines); the dirty paths in the
|
|
423
|
+
> working tree were sibling agents' `scripts/` and `tests/` files, none of which this
|
|
424
|
+
> section measures.
|
|
425
|
+
|
|
426
|
+
§5 recommends against building a second delivery path, because injecting a scoped rule
|
|
427
|
+
block alongside undiminished native loading costs **+72%**. Issue #1014 then shipped an
|
|
428
|
+
injected block. This section exists to answer the obvious question — *is the learnings
|
|
429
|
+
index the thing §5 forbids?* — with numbers rather than with an assurance.
|
|
430
|
+
|
|
431
|
+
**It is not, by a factor of 92.** The forbidden path re-sends 122,875 bytes the agent is
|
|
432
|
+
already receiving. The index sends **1,405–1,727 bytes the agent receives nowhere else**:
|
|
433
|
+
**+0.79% to +0.97%** of the re-measured baseline.
|
|
434
|
+
|
|
435
|
+
> **⚠ Amendment, 2026-08-13 — every byte figure in §7 below is PRE-FRAMING.**
|
|
436
|
+
>
|
|
437
|
+
> §7 was measured at `c87102b`. A review panel then found that this block shipped
|
|
438
|
+
> agent-authored text with none of the neutralisation `<APPLICABLE-RULES>` had received in
|
|
439
|
+
> the same session — no fence, no wrapper-forgery rejection, no invisible-character
|
|
440
|
+
> stripping. The fix added a content-derived block fence and a framing preamble, at a
|
|
441
|
+
> **constant +320 B** independent of entry count.
|
|
442
|
+
>
|
|
443
|
+
> Re-measured by the coordinator after the fix (2 scopes, `wc -c` on the real CLI):
|
|
444
|
+
> `scripts/lib/reconcile/**` → **2,047 B** (was 1,727); `docs/**` → **1,234 B** (was 914).
|
|
445
|
+
>
|
|
446
|
+
> | | §7 as measured | after framing |
|
|
447
|
+
> |---|---:|---:|
|
|
448
|
+
> | band | 912–1,727 B | **1,234–2,047 B** |
|
|
449
|
+
> | share of the 178,096 B baseline | +0.51% – +0.97% | **+0.69% – +1.15%** |
|
|
450
|
+
> | ceiling (entry caps lifted) | 2,221 B / +1.25% | **2,541 B / +1.43%** |
|
|
451
|
+
>
|
|
452
|
+
> The 92× argument is unaffected — the comparison is against +122,875 B, and +320 B does not
|
|
453
|
+
> move it. `LEARNINGS_INDEX_MAX_CHARS = 2000` is likewise untouched: the framing sits outside
|
|
454
|
+
> the capped body, as the header and retrieval pointer already did.
|
|
455
|
+
>
|
|
456
|
+
> The per-scope table in §7.2 and the figures in §7.4 are left at their measured values rather
|
|
457
|
+
> than overwritten, so each keeps the SHA it was taken at. Read them as pre-framing.
|
|
458
|
+
|
|
459
|
+
### 7.1 The baseline, re-measured — and it did not drift
|
|
460
|
+
|
|
461
|
+
The wave-1 baseline was captured at `0fbea29`. Re-measuring at `c87102b` (3 commits later,
|
|
462
|
+
`git rev-list --count 0fbea29..HEAD` → `3`) gives a **byte-identical** rule corpus:
|
|
463
|
+
|
|
464
|
+
```console
|
|
465
|
+
$ find .claude/rules -name '*.md' | wc -l
|
|
466
|
+
29
|
|
467
|
+
$ find .claude/rules -name '*.md' -exec cat {} + | wc -c
|
|
468
|
+
165097
|
|
469
|
+
$ wc -c CLAUDE.md
|
|
470
|
+
9666
|
|
471
|
+
$ wc -c ~/.claude/CLAUDE.md
|
|
472
|
+
1220
|
|
473
|
+
$ sed -n '266,286p' skills/wave-executor/SKILL.md | wc -c # the fenced memory.propose block
|
|
474
|
+
2113
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
| Component | Bytes @ `0fbea29` | Bytes @ `c87102b` | Drift | Fires |
|
|
478
|
+
|---|---:|---:|---:|---|
|
|
479
|
+
| `.claude/rules/**` (29 files) | 165,097 | 165,097 | 0 | every agent, native project-instruction loading |
|
|
480
|
+
| `CLAUDE.md` / `AGENTS.md` (root) | 9,666 | 9,666 | 0 | every agent |
|
|
481
|
+
| `~/.claude/CLAUDE.md` (user-level) | 1,220 | 1,220 | 0 | every agent on this host, outside repo control |
|
|
482
|
+
| `memory.propose` boilerplate | 2,112 | **2,113** | +1 | every Impl-Core / Impl-Polish / Quality agent |
|
|
483
|
+
| **TOTAL** | **178,095** | **178,096** | **+1** | per Impl-wave agent, before task text |
|
|
484
|
+
|
|
485
|
+
The corpus figures are reproducible from the git object store rather than from the working
|
|
486
|
+
tree, which is what makes the zero-drift claim checkable:
|
|
487
|
+
|
|
488
|
+
```console
|
|
489
|
+
$ git ls-tree -r --name-only 0fbea29 -- .claude/rules | grep '\.md$' \
|
|
490
|
+
| while read f; do git cat-file blob "0fbea29:$f"; done | wc -c
|
|
491
|
+
165097
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
The single byte is the boilerplate's trailing newline: the block is lines 266–286 of
|
|
495
|
+
`skills/wave-executor/SKILL.md`, and wave 1 evidently measured it without the final `\n`.
|
|
496
|
+
Recording it rather than rounding it away is the point — a figure that reproduces to ±1
|
|
497
|
+
byte across two SHAs is a measurement; one that "matches" is an assertion.
|
|
498
|
+
|
|
499
|
+
**The rules corpus did not change this session.** The 100-record learnings corpus did. Those
|
|
500
|
+
are different corpora, and only the second one is what §7 adds.
|
|
501
|
+
|
|
502
|
+
### 7.2 What the index actually costs, per scope
|
|
503
|
+
|
|
504
|
+
Four **disjoint** agent file scopes, each naming real tracked files
|
|
505
|
+
(`git ls-files --error-unmatch` → TRACKED for all eight paths):
|
|
506
|
+
|
|
507
|
+
```console
|
|
508
|
+
$ echo '["hooks/pre-bash-destructive-guard.mjs","hooks/on-session-start.mjs"]' > /tmp/scope-hooks.json
|
|
509
|
+
$ node scripts/print-learnings-index.mjs --file-scope /tmp/scope-hooks.json --no-event | wc -c
|
|
510
|
+
1405
|
|
511
|
+
$ node scripts/print-learnings-index.mjs --file-scope /tmp/scope-hooks.json --no-event --json # count + scopeMatched
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
| Agent file scope | Bytes | Entries | Scoped | Global fill | % of 178,096 |
|
|
515
|
+
|---|---:|---:|---:|---:|---:|
|
|
516
|
+
| `hooks/**` (2 files) | 1,405 | 7 | 3 | 4 | +0.789% |
|
|
517
|
+
| `scripts/lib/reconcile/**` (2 files) | 1,727 | 9 | 5 | 4 | +0.970% |
|
|
518
|
+
| `tests/**` (2 files) | 912 | 4 | 0 | 4 | +0.512% |
|
|
519
|
+
| `docs/**` (2 files — this agent's own scope) | 914 | 4 | 0 | 4 | +0.513% |
|
|
520
|
+
| *control:* `tests/**` naming two paths the corpus does carry | 1,240 | 6 | 2 | 4 | +0.696% |
|
|
521
|
+
|
|
522
|
+
The corpus behind these numbers, measured rather than quoted:
|
|
523
|
+
|
|
524
|
+
```console
|
|
525
|
+
$ wc -l < .orchestrator/metrics/learnings.jsonl
|
|
526
|
+
100
|
|
527
|
+
$ node -e '…JSON.parse each line; count Array.isArray(r.file_paths) && r.file_paths.length>0…'
|
|
528
|
+
total=100 with_file_paths=17 pct=17.0%
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**17 of 100 records carry `file_paths`.** That fraction, not the CLI, is what governs the
|
|
532
|
+
scoped column — and the `tests/**` row is the honest demonstration. It returned **0 scoped
|
|
533
|
+
matches** even though the corpus holds 6 `tests/` path entries, because those 6 name
|
|
534
|
+
different files (`tests/lib/session-registry.test.mjs`,
|
|
535
|
+
`tests/scripts/gates/gate-helpers.test.mjs`, …) than the two a plausible agent declared.
|
|
536
|
+
The control row re-runs the same scope against two paths the corpus *does* carry and
|
|
537
|
+
recovers 2 scoped matches. Matching is exact-path; the 0 is correct behaviour, not a defect.
|
|
538
|
+
|
|
539
|
+
The practical reading: **at 17% `file_paths` coverage, a majority of scopes will fall back
|
|
540
|
+
entirely to the global fill.** The index degrades to "top 4 general learnings" rather than
|
|
541
|
+
to nothing, which is the right failure mode, but it is not yet per-agent for most agents.
|
|
542
|
+
The lever is `--file-paths` adoption at proposal time, not the selector.
|
|
543
|
+
|
|
544
|
+
### 7.3 The delta, and the comparison that settles the question
|
|
545
|
+
|
|
546
|
+
Absolute: **+912 to +1,727 bytes**, ceiling **+2,221** (§7.4). As a share of the
|
|
547
|
+
re-measured 178,096-byte baseline: **+0.51% to +0.97%**, ceiling **+1.25%**.
|
|
548
|
+
|
|
549
|
+
| Path | Added bytes | As % of baseline |
|
|
550
|
+
|---|---:|---:|
|
|
551
|
+
| §5's forbidden path — scoped rule block alongside native delivery | +122,875 | **+72.3%** |
|
|
552
|
+
| §7's learnings index — measured worst case of four scopes | +1,727 | **+0.97%** |
|
|
553
|
+
|
|
554
|
+
**92×.** Two structural facts produce that gap, and each is checkable:
|
|
555
|
+
|
|
556
|
+
1. **It rides a channel the repo already writes itself.** The index is prepended to the
|
|
557
|
+
dispatch prompt — the same channel that already carries the task text and the 2,113-byte
|
|
558
|
+
`memory.propose` boilerplate. It does not add a delivery mechanism; it adds a block to a
|
|
559
|
+
prompt the coordinator was already composing. §5's +72% is entirely the cost of
|
|
560
|
+
*duplicating* a channel that keeps delivering regardless.
|
|
561
|
+
2. **Learnings have no native delivery to duplicate.** `.orchestrator/metrics/learnings.jsonl`
|
|
562
|
+
is not a `.md` file under `.claude/rules/`, and neither `CLAUDE.md` nor its Codex-CLI
|
|
563
|
+
alias `AGENTS.md` imports it (`grep -c "^@" CLAUDE.md` → `0`, unchanged from §1). Nothing
|
|
564
|
+
in the 178,096-byte baseline carries this content. The index is the *first* delivery of
|
|
565
|
+
it, not the second.
|
|
566
|
+
|
|
567
|
+
#### 7.3.1 …except for 13 records, and the index does not exclude them
|
|
568
|
+
|
|
569
|
+
Fact 2 is true of the corpus but not of every record in it. `/reconcile` converts qualifying
|
|
570
|
+
learnings into `.claude/rules/*.md` files — which **are** natively delivered. Those rules
|
|
571
|
+
carry their source in provenance, so the overlap is measurable:
|
|
572
|
+
|
|
573
|
+
```console
|
|
574
|
+
$ grep -rlha "learning-id:" .claude/rules/*.md | wc -l
|
|
575
|
+
13
|
|
576
|
+
$ grep -rha "^- learning-id:" .claude/rules/*.md | sed 's/.*`\(.*\)`.*/\1/' > /tmp/rule-lids.txt
|
|
577
|
+
$ # then: for each scope, intersect the index's learning ids with /tmp/rule-lids.txt
|
|
578
|
+
hooks: entries=7 already_a_rule=1
|
|
579
|
+
scripts: entries=9 already_a_rule=0
|
|
580
|
+
tests: entries=4 already_a_rule=0
|
|
581
|
+
docs: entries=4 already_a_rule=1
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
**13 of 100 learnings have already become natively-delivered rules, and the selector does
|
|
585
|
+
not filter them out.** In two of four scopes, one indexed entry was content the agent was
|
|
586
|
+
already receiving in full — a genuine, if small, second delivery of that record. The
|
|
587
|
+
duplicated unit is one ~150-byte index line against a multi-KB rule file, so the waste is
|
|
588
|
+
bounded and far below the measurement noise of §7.1; it does not change the +0.97% figure
|
|
589
|
+
or the 92× conclusion. But it is a real instance of the exact pattern §5 forbids, found by
|
|
590
|
+
looking for it, and it is cheap to close: the selector has each record's id and the rule
|
|
591
|
+
files carry theirs. **Filed as a follow-up rather than fixed here — this section's file
|
|
592
|
+
scope is documentation, and the fix is a change to `scripts/lib/learnings/select.mjs`.**
|
|
593
|
+
|
|
594
|
+
### 7.4 The 2,000-character cap is slack; the entry-count caps bind
|
|
595
|
+
|
|
596
|
+
`LEARNINGS_INDEX_MAX_CHARS = 2000` (`scripts/lib/learnings/select.mjs:89`) is a hard cap on
|
|
597
|
+
the rendered **body**. Under the shipped defaults it is never reached:
|
|
598
|
+
|
|
599
|
+
```console
|
|
600
|
+
$ node scripts/print-learnings-index.mjs --file-scope /tmp/scope-hooks.json --no-event | wc -c
|
|
601
|
+
1405
|
|
602
|
+
$ node scripts/print-learnings-index.mjs --file-scope /tmp/scope-hooks.json --no-event \
|
|
603
|
+
--max-scoped 100 --max-global 100 | wc -c
|
|
604
|
+
2221
|
|
605
|
+
$ node scripts/print-learnings-index.mjs --file-scope /tmp/scope-hooks.json --no-event \
|
|
606
|
+
--max-scoped 100 --max-global 100 --max-chars 100000 | wc -c
|
|
607
|
+
15588
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
| Constraint | Result | Binds? |
|
|
611
|
+
|---|---:|---|
|
|
612
|
+
| default caps (8 scoped / 4 global / 2,000 chars) | 1,405–1,727 B | entry caps bind |
|
|
613
|
+
| entry caps lifted, char cap at 2,000 | 2,221 B | **char cap binds** |
|
|
614
|
+
| both lifted (whole corpus as an index) | 15,588 B | nothing binds |
|
|
615
|
+
|
|
616
|
+
The binding constraint under defaults is **`--max-global 4`**, not the char cap. The
|
|
617
|
+
telemetry's `truncated: true` says so precisely: it is set at `select.mjs:389` by
|
|
618
|
+
`scoped.length > maxScoped || global.length > maxGlobal` — an **entry-count** overflow, not
|
|
619
|
+
a character overflow. Reading `truncated: true` on a 1,405-byte block as "the 2,000 cap bit"
|
|
620
|
+
would be wrong.
|
|
621
|
+
|
|
622
|
+
Slack, stated plainly: **816 bytes of body headroom on the `hooks` scope, 494 on `scripts`**
|
|
623
|
+
(the rendered block carries a 221-byte header outside the capped body, which is why the
|
|
624
|
+
lifted-caps run reports 2,221 rather than 2,000). The cap is a backstop against a corpus
|
|
625
|
+
that grows an order of magnitude — at 100 records it never fires. The number that would
|
|
626
|
+
make it fire is 15,588: the whole corpus rendered as an index is **7.8×** the cap, so the
|
|
627
|
+
cap is doing real work as a ceiling even while slack today.
|
|
628
|
+
|
|
629
|
+
### 7.5 The per-agent claim, verified empirically
|
|
630
|
+
|
|
631
|
+
Not read off the code — run, on two disjoint scopes, comparing the emitted subjects:
|
|
632
|
+
|
|
633
|
+
```console
|
|
634
|
+
$ # hooks scope
|
|
635
|
+
## Learnings Index (selected for your file scope)
|
|
636
|
+
|
|
637
|
+
7 entries (3 matched your declared file scope, 4 general). One line each — this is an INDEX, not the corpus.
|
|
638
|
+
Full text of any line: `grep -F '"subject":"<subject>"' .orchestrator/metrics/learnings.jsonl`
|
|
639
|
+
|
|
640
|
+
- anti-pattern/an inline @returns-never warn helper at a rule-loop warn site flips a later block to ALLOW: …
|
|
641
|
+
- anti-pattern/policy rule with a new type field must ship with its consuming hook branch in the same change: …
|
|
642
|
+
- anti-pattern/release() proof gate hinges on proof !== undefined — every call site must spread-guard: …
|
|
643
|
+
[4 more]
|
|
644
|
+
|
|
645
|
+
$ # scripts scope
|
|
646
|
+
## Learnings Index (selected for your file scope)
|
|
647
|
+
|
|
648
|
+
9 entries (5 matched your declared file scope, 4 general). One line each — this is an INDEX, not the corpus.
|
|
649
|
+
Full text of any line: `grep -F '"subject":"<subject>"' .orchestrator/metrics/learnings.jsonl`
|
|
650
|
+
|
|
651
|
+
- anti-pattern/A hardcoded expected value that coincides with a clamp/floor for one day is a green that proves nothing: …
|
|
652
|
+
- proven-pattern/security-floor policy loaders must merge, not first-hit-resolve: …
|
|
653
|
+
- recurring-issue/Eine Session ohne durchgelaufenes /close hinterlaesst unpushed Commits …
|
|
654
|
+
[6 more]
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
Set-compared rather than eyeballed:
|
|
658
|
+
|
|
659
|
+
```console
|
|
660
|
+
$ # intersect the two --json subject lists
|
|
661
|
+
hooks entries: 7 | scripts entries: 9
|
|
662
|
+
intersection: 2 | hooks-only: 5 | scripts-only: 7 | Jaccard: 0.143
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
**Two disjoint scopes share 2 of 14 distinct entries (Jaccard 0.143).** Both shared entries
|
|
666
|
+
are global fill; the scoped selections are fully disjoint. The block is genuinely
|
|
667
|
+
per-agent, not a constant wearing a scope's name — which is the claim §3.1 could *not* make
|
|
668
|
+
about glob-scoped rules, where the "scoped" saving turned out to be a constant 40,254 bytes
|
|
669
|
+
of tier filtering.
|
|
670
|
+
|
|
671
|
+
### 7.6 The telemetry answers "did the injector ever run?"
|
|
672
|
+
|
|
673
|
+
§1 could establish that `rule-loader.mjs` does not run at delivery time only by grepping for
|
|
674
|
+
the absence of a caller. That is an argument from silence, and §1.2 records how it failed:
|
|
675
|
+
a census keyed on the payload missed the prose call site entirely. The index does not
|
|
676
|
+
require that argument — it emits an event:
|
|
677
|
+
|
|
678
|
+
```console
|
|
679
|
+
$ grep -ac 'orchestrator.learnings.index.injected' .orchestrator/metrics/events.jsonl
|
|
680
|
+
1
|
|
681
|
+
$ node scripts/print-learnings-index.mjs --file-scope /tmp/scope-hooks.json > /dev/null
|
|
682
|
+
$ grep -ac 'orchestrator.learnings.index.injected' .orchestrator/metrics/events.jsonl
|
|
683
|
+
2
|
|
684
|
+
$ grep -a 'orchestrator.learnings.index.injected' .orchestrator/metrics/events.jsonl | tail -1
|
|
685
|
+
{
|
|
686
|
+
"timestamp": "2026-08-13T05:53:29.824Z",
|
|
687
|
+
"event": "orchestrator.learnings.index.injected",
|
|
688
|
+
"count": 7,
|
|
689
|
+
"scope_matched": 3,
|
|
690
|
+
"global_count": 4,
|
|
691
|
+
"candidates": 94,
|
|
692
|
+
"truncated": true,
|
|
693
|
+
"bytes": 1405,
|
|
694
|
+
"scope_source": "file-scope"
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
The counter moves 1 → 2 on one invocation, and the record's `bytes: 1405` is independently
|
|
699
|
+
equal to this section's `wc -c`. "Did it run, on what scope, and how big was it" is now a
|
|
700
|
+
`grep` over `events.jsonl` instead of an inference. `candidates: 94` also shows the pool is
|
|
701
|
+
the 94 *active* records of the 100 on disk — 6 are filtered before ranking.
|
|
702
|
+
|
|
703
|
+
### 7.7 What is NOT established — explicitly unbelegt
|
|
704
|
+
|
|
705
|
+
Four gaps. Each would move the denominator, and none is closed by anything above.
|
|
706
|
+
|
|
707
|
+
- **Whether this Claude Code build loads a root `AGENTS.md`.** One exists on disk —
|
|
708
|
+
`ls -la AGENTS.md` → 14,661 bytes, **untracked** (`git ls-files AGENTS.md | wc -l` → `0`).
|
|
709
|
+
The coordinator of this session confirmed from its own context window that it is *not*
|
|
710
|
+
delivered to the coordinator. **Whether a dispatched subagent receives it is unverified**,
|
|
711
|
+
and this agent cannot settle it: a subagent introspecting its own context is the
|
|
712
|
+
self-report §1.1 already flagged as corroborating-not-load-bearing. If subagents do
|
|
713
|
+
receive it, the baseline is 192,757 rather than 178,096 and the index's share *falls* to
|
|
714
|
+
+0.73%. Tracked as issue **#973**.
|
|
715
|
+
- **Whether the `<APPLICABLE-RULES>` block fires in practice.** `wave-loop.md` makes the
|
|
716
|
+
pre-dispatch injection a SHOULD, not a gate. In **this** session it was deliberately
|
|
717
|
+
skipped and logged as a deviation, so the 178,096 figure **excludes it**. If it fires, the
|
|
718
|
+
denominator grows by up to the 122,875 bytes §3 measured for a live wave scope and the
|
|
719
|
+
index's share falls correspondingly — to roughly +0.57%. Every percentage in §7.3 is
|
|
720
|
+
therefore a *conservative* upper bound on the index's share: the honest reading is
|
|
721
|
+
"≤1% under the smallest defensible denominator".
|
|
722
|
+
- **Whether 17% `file_paths` coverage is representative.** It is the coverage of a 100-record
|
|
723
|
+
corpus, 11 of whose records were recovered from a data-loss incident. Whether the recovered
|
|
724
|
+
records are systematically poorer in `file_paths` than organically-proposed ones was not
|
|
725
|
+
measured, and it would bias the scoped column if so.
|
|
726
|
+
- **Whether the §7.3.1 rule/learning overlap grows.** Measured once, at 13/100, on four
|
|
727
|
+
scopes. `/reconcile` promotes learnings into rules continuously, so this ratio rises by
|
|
728
|
+
construction. Nothing currently measures it on a schedule.
|
|
729
|
+
|
|
730
|
+
### 7.8 Correction to §1.3
|
|
731
|
+
|
|
732
|
+
§1.3 records a second delivery source outside this repo — the parent workspace's
|
|
733
|
+
`.claude/rules/parallel-sessions.md`, 5,623 bytes — and concludes the real delivered corpus
|
|
734
|
+
is 175,584 rather than 169,961. **That file no longer exists on this host:**
|
|
735
|
+
|
|
736
|
+
```console
|
|
737
|
+
$ ls -la "$(dirname "$PWD")"/.claude/rules/
|
|
738
|
+
ls: /Users/…/Projects/.claude/rules/: No such file or directory
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
§7's baseline therefore does not carry a parent-workspace term. This is a host-local
|
|
742
|
+
observation about one machine at one date, not proof the mechanism is gone — the directory
|
|
743
|
+
could be recreated at any time, and §1.3's structural point (that
|
|
744
|
+
`print-applicable-rules.mjs` reads only `<repoRoot>/.claude/rules` and cannot see such a
|
|
745
|
+
file) stands unchanged.
|
|
746
|
+
|
|
747
|
+
---
|
|
748
|
+
|
|
749
|
+
## Reproducing this document
|
|
750
|
+
|
|
751
|
+
```bash
|
|
752
|
+
# corpus size
|
|
753
|
+
find .claude/rules -name '*.md' | wc -l
|
|
754
|
+
find .claude/rules -name '*.md' -exec cat {} + | wc -c
|
|
755
|
+
|
|
756
|
+
# no repo-local injector
|
|
757
|
+
grep -c "^@" CLAUDE.md
|
|
758
|
+
node -e 'const s=require("./.claude/settings.json");console.log(Object.keys(s))'
|
|
759
|
+
git grep -n "loadApplicableRules" -- '*.mjs' | grep -v '^tests/'
|
|
760
|
+
|
|
761
|
+
# scoped block for an arbitrary scope (never overwrite .claude/wave-scope.json)
|
|
762
|
+
echo '{"allowedPaths":["scripts/lib/x.mjs"]}' > /tmp/scope.json
|
|
763
|
+
node scripts/print-applicable-rules.mjs --wave-scope /tmp/scope.json --context wave | wc -c
|
|
764
|
+
node scripts/print-applicable-rules.mjs --wave-scope /tmp/scope.json | wc -c # glob axis only
|
|
765
|
+
|
|
766
|
+
# blast radius
|
|
767
|
+
git grep -l "\.claude/rules/" | wc -l
|
|
768
|
+
|
|
769
|
+
# §7 — baseline re-measure, reproducible from the object store at any SHA
|
|
770
|
+
git ls-tree -r --name-only <sha> -- .claude/rules | grep '\.md$' \
|
|
771
|
+
| while read f; do git cat-file blob "<sha>:$f"; done | wc -c
|
|
772
|
+
sed -n '266,286p' skills/wave-executor/SKILL.md | wc -c # memory.propose boilerplate
|
|
773
|
+
|
|
774
|
+
# §7 — the learnings index for one agent scope (never writes; --no-event suppresses telemetry)
|
|
775
|
+
echo '["hooks/on-session-start.mjs"]' > /tmp/scope.json
|
|
776
|
+
node scripts/print-learnings-index.mjs --file-scope /tmp/scope.json --no-event | wc -c
|
|
777
|
+
node scripts/print-learnings-index.mjs --file-scope /tmp/scope.json --no-event --json
|
|
778
|
+
|
|
779
|
+
# §7.4 — which cap binds
|
|
780
|
+
node scripts/print-learnings-index.mjs --file-scope /tmp/scope.json --no-event \
|
|
781
|
+
--max-scoped 100 --max-global 100 --max-chars 100000 | wc -c
|
|
782
|
+
|
|
783
|
+
# §7.6 — did the injector run?
|
|
784
|
+
grep -ac 'orchestrator.learnings.index.injected' .orchestrator/metrics/events.jsonl
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
## See also
|
|
788
|
+
|
|
789
|
+
- `scripts/lib/rule-loader.mjs` — `loadApplicableRules()`, the loader that does not run at delivery time
|
|
790
|
+
- `scripts/print-applicable-rules.mjs` — the CLI bridge; reads only `<repoRoot>/.claude/rules`
|
|
791
|
+
- `scripts/lib/instruction-budget-guard.mjs` — the existing measurement + ceiling instrument
|
|
792
|
+
- `docs/rule-authoring.md` — frontmatter contract (and the stale claim at line 8)
|
|
793
|
+
- `scripts/print-learnings-index.mjs` — §7's per-agent index CLI; emits `orchestrator.learnings.index.injected`
|
|
794
|
+
- `scripts/lib/learnings/select.mjs` — the selector; `LEARNINGS_INDEX_MAX_CHARS = 2000` at line 89, `truncated` at line 389
|