@event4u/agent-config 14.21.0 → 14.22.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 (181) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +198 -0
  5. package/CREDITS.md +37 -0
  6. package/dist/agent-src/commands/analyze/inbox.md +380 -5
  7. package/dist/agent-src/commands/analyze/repo.md +4 -2
  8. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  9. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  10. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  11. package/dist/agent-src/rules/token-efficiency.md +26 -3
  12. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  13. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  14. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  15. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  16. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  17. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  18. package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
  19. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  20. package/dist/agent-src/templates/agent-settings.md +0 -1
  21. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  22. package/dist/cli-delegate/_cli_entry.js +8 -0
  23. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  24. package/dist/cli-delegate/archival_sweep.js +8 -0
  25. package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
  26. package/dist/cli-delegate/check_update_banner.js +923 -0
  27. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  28. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  29. package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-BIXEDQPZ.js} +6 -0
  30. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  31. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  32. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  33. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  34. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
  35. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  36. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  37. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  38. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  39. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  40. package/dist/cli-delegate/cmd_conformance.js +2 -2
  41. package/dist/cli-delegate/cmd_export.js +1 -1
  42. package/dist/cli-delegate/cmd_handoff.js +26 -2
  43. package/dist/cli-delegate/cmd_prune.js +1 -1
  44. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  45. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  46. package/dist/cli-delegate/cmd_sync.js +1 -1
  47. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  48. package/dist/cli-delegate/cmd_validate.js +1 -1
  49. package/dist/cli-delegate/dashboard_mode.js +16 -0
  50. package/dist/cli-delegate/gate_budget.js +24 -0
  51. package/dist/cli-delegate/gate_execute.js +25 -0
  52. package/dist/cli-delegate/guarded_baseline.js +18 -0
  53. package/dist/cli-delegate/resume_probe.js +22 -0
  54. package/dist/cli-delegate/roadmap_gates.js +638 -0
  55. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  56. package/dist/cli-delegate/stub_queue.js +47 -0
  57. package/dist/cli-delegate/stubs_due.js +38 -0
  58. package/dist/cli-delegate/ticket_bundles.js +59 -0
  59. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  60. package/dist/discovery/deprecation-report.md +1 -1
  61. package/dist/discovery/discovery-manifest.json +8 -8
  62. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  63. package/dist/discovery/discovery-manifest.summary.md +1 -1
  64. package/dist/discovery/dormancy-report.md +1 -1
  65. package/dist/discovery/orphan-report.md +1 -1
  66. package/dist/discovery/packs.json +2 -2
  67. package/dist/discovery/trust-report.md +1 -1
  68. package/dist/discovery/workspaces.json +2 -2
  69. package/dist/hooks/dispatch.js +1193 -917
  70. package/dist/install/claudeLayerCarriage.js +237 -0
  71. package/dist/install/claudePathsPlan.js +79 -0
  72. package/dist/install/hostLayerFingerprint.js +1 -1
  73. package/dist/install/install.mjs +6 -0
  74. package/dist/install/partitionEligibility.js +231 -117
  75. package/dist/install/ruleLayerPartition.js +10 -7
  76. package/dist/mcp/registry-manifest.json +2 -2
  77. package/dist/mcp/server.json +2 -2
  78. package/dist/mcp/server.mjs +39219 -38995
  79. package/dist/scripts/condense.js +32 -25
  80. package/docs/MIGRATION.md +49 -1
  81. package/docs/contracts/settings-classes.md +1 -1
  82. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  83. package/package.json +7 -4
  84. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  85. package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  86. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  87. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  88. package/src/agent-src/scripts/stubs_due.ts +4 -18
  89. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  90. package/src/config/continuity-surface.json +509 -0
  91. package/src/config/estate-count-budget.json +1 -1
  92. package/src/config/gate-coverage.yml +29 -8
  93. package/src/config/gate-violation-baselines.json +8 -8
  94. package/src/config/gitignore-block.txt +19 -0
  95. package/src/config/host-injection-effect.json +73 -0
  96. package/src/config/packed-binary-manifest.json +33 -0
  97. package/src/config/preamble-payload-budget.json +112 -105
  98. package/src/config/publish-surface.json +4 -2
  99. package/src/install/claudeLayerCarriage.ts +273 -0
  100. package/src/install/claudePathsPlan.ts +76 -0
  101. package/src/install/hostLayerFingerprint.ts +1 -1
  102. package/src/install/partitionEligibility.ts +261 -127
  103. package/src/install/ruleLayerPartition.ts +19 -23
  104. package/src/scripts/_dispatch.bash +77 -27
  105. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  106. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  107. package/src/scripts/_lib/continuity_surface.ts +296 -0
  108. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  109. package/src/scripts/_lib/hook_settings.ts +1 -1
  110. package/src/scripts/_lib/injection_effect.ts +151 -0
  111. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  112. package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
  113. package/src/scripts/_lib/pin_resolver.ts +13 -0
  114. package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
  115. package/src/scripts/_lib/retired_status.ts +74 -0
  116. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  117. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  118. package/src/scripts/ai_council/api_transport.ts +5 -0
  119. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  120. package/src/scripts/ai_council/prompts.ts +9 -0
  121. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  122. package/src/scripts/capture_skill_catalogue.ts +24 -11
  123. package/src/scripts/check_candidate_lines.ts +499 -0
  124. package/src/scripts/check_continuity_surface.ts +475 -0
  125. package/src/scripts/check_dependency_floors.ts +13 -6
  126. package/src/scripts/check_estate_count.ts +40 -79
  127. package/src/scripts/check_pack_size.ts +262 -23
  128. package/src/scripts/check_roadmap_trackable.ts +19 -16
  129. package/src/scripts/check_rule_layer_partition.ts +54 -34
  130. package/src/scripts/check_single_delivery.ts +97 -31
  131. package/src/scripts/check_update_banner.ts +13 -0
  132. package/src/scripts/code_graph/build.ts +138 -15
  133. package/src/scripts/code_graph/cli.ts +76 -8
  134. package/src/scripts/code_graph/extract.ts +11 -0
  135. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  136. package/src/scripts/code_graph/loader.ts +88 -14
  137. package/src/scripts/code_graph/query.ts +121 -11
  138. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  139. package/src/scripts/code_graph/sqlite_store.ts +247 -6
  140. package/src/scripts/code_graph/types.ts +66 -1
  141. package/src/scripts/code_graph/validate.ts +23 -0
  142. package/src/scripts/condense.ts +34 -22
  143. package/src/scripts/corpus_manifest.ts +5 -5
  144. package/src/scripts/council_cli.ts +11 -12
  145. package/src/scripts/count_intent_disagreement.ts +426 -0
  146. package/src/scripts/handoff_context_hook.ts +79 -0
  147. package/src/scripts/hook_effect_doctor.ts +82 -1
  148. package/src/scripts/hook_manifest.json +1 -1
  149. package/src/scripts/hook_manifest.yaml +64 -12
  150. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  151. package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
  152. package/src/scripts/hooks/concern_registry.ts +4 -2
  153. package/src/scripts/hooks/host_semantics.ts +1 -1
  154. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  155. package/src/scripts/hooks/rule_inject_hook.ts +1 -1
  156. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  157. package/src/scripts/inbox_source_census.ts +322 -0
  158. package/src/scripts/install-hooks.sh +82 -0
  159. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  160. package/src/scripts/lint_plan_risk_register.ts +13 -17
  161. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  162. package/src/scripts/mcp_server/tools.ts +2 -56
  163. package/src/scripts/media/lib/adapter-contract.md +7 -2
  164. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  165. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  166. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  167. package/src/scripts/rdp_candidates_delta.ts +276 -0
  168. package/src/scripts/rdp_quality_eval.ts +217 -19
  169. package/src/scripts/release_mix_taxonomy.json +3 -2
  170. package/src/scripts/report_host_injection_effect.ts +189 -0
  171. package/src/scripts/report_layer_overlap.ts +41 -0
  172. package/src/scripts/report_skill_menu_census.ts +341 -0
  173. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  174. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  175. package/src/vendor/grammars/README.md +61 -0
  176. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  177. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  178. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  179. package/src/scripts/_lib/carrier_status.ts +0 -118
  180. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  181. package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Continuity-surface inventory — the behavioural half of the ratchet.
3
+ *
4
+ * WHY THIS EXISTS
5
+ * ---------------
6
+ * `road-to-one-continuity-record` measured its own success as five numbers —
7
+ * public continuity commands, session resume pickers, persistent continuity
8
+ * artefacts, continuity schemas, normal-path manual actions — and its council
9
+ * of 2026-09-07 named the defect in measuring them by name:
10
+ *
11
+ * "Any persisted state written for later session/run recovery, verification,
12
+ * or context restoration must be listed, whether or not it is labeled
13
+ * continuity."
14
+ *
15
+ * A count keyed on the WORD `continuity` is gameable by renaming. So the
16
+ * artefact axis is DISCOVERED behaviourally — every distinct leaf under
17
+ * `agents/runtime/state/` that any source file names — and the inventory then
18
+ * has to dispose of each one as `counted` or `excluded` WITH A REASON. A leaf
19
+ * nothing disposes of is a hard failure, which is what makes a newly added
20
+ * persisted-state surface impossible to slip past the count silently.
21
+ *
22
+ * The exclusions are the load-bearing half, not the leftovers. `checkpoints/`
23
+ * is the worked example the council named: it is kept (D2 — run-integrity
24
+ * evidence is keyed by run id, consumed by `run:supervise`, and a disagreement
25
+ * there is an integrity alarm rather than a degraded resume), so it is excluded
26
+ * from the continuity axis, and that exclusion is PRINTED on every run rather
27
+ * than buried in a predicate.
28
+ *
29
+ * WHAT THIS MODULE IS NOT
30
+ * -----------------------
31
+ * It does not decide policy. It reads a committed inventory, checks that the
32
+ * inventory still describes the tree (every `locus` resolves; every discovered
33
+ * leaf is disposed of), and derives the five numbers from the dispositions.
34
+ * The ratchet that forbids growth lives in `check_continuity_surface.ts`, which
35
+ * runs these same functions over a materialised base tree.
36
+ */
37
+ import * as fs from 'node:fs';
38
+ import * as path from 'node:path';
39
+
40
+ /** Repo-relative POSIX path of the committed inventory. */
41
+ export const INVENTORY_POSIX = 'src/config/continuity-surface.json';
42
+
43
+ /** Source roots the behavioural discovery walks. */
44
+ export const DISCOVERY_ROOTS: readonly string[] = [
45
+ 'src/scripts',
46
+ 'src/agent-src/scripts',
47
+ 'src/cli',
48
+ 'src/shared',
49
+ 'src/server',
50
+ 'src/install',
51
+ ];
52
+
53
+ /**
54
+ * The five axes, in the order the parent roadmap's goal states them. The order
55
+ * is part of the contract: the goal reads `0 / 0 / 1 / 1 / 0` positionally, and
56
+ * a reader comparing a run against that line needs the same positions.
57
+ */
58
+ export const AXES = [
59
+ 'public_continuity_commands',
60
+ 'session_resume_pickers',
61
+ 'persistent_continuity_artefacts',
62
+ 'continuity_schemas',
63
+ 'normal_path_manual_actions',
64
+ ] as const;
65
+
66
+ export type Axis = (typeof AXES)[number];
67
+
68
+ /** The target the parent roadmap's goal states, by axis. */
69
+ export const TARGETS: Readonly<Record<Axis, number>> = {
70
+ public_continuity_commands: 0,
71
+ session_resume_pickers: 0,
72
+ persistent_continuity_artefacts: 1,
73
+ continuity_schemas: 1,
74
+ normal_path_manual_actions: 0,
75
+ };
76
+
77
+ export interface InventoryRow {
78
+ /** Stable identifier. For a discovered artefact this is the state leaf. */
79
+ id: string;
80
+ /** Which axis this row belongs to. */
81
+ axis: Axis;
82
+ /** `counted` contributes to the axis number; `excluded` does not. */
83
+ disposition: 'counted' | 'excluded';
84
+ /**
85
+ * Repo-relative `file` or `file:line` the row is anchored to. It must
86
+ * resolve, which is what makes a retirement MEASURED: deleting the surface
87
+ * without deleting or re-disposing its row reddens.
88
+ */
89
+ locus: string;
90
+ /**
91
+ * Why. Mandatory on every row, not only on exclusions — a `counted` row
92
+ * whose reason nobody wrote is a number without a meaning.
93
+ */
94
+ reason: string;
95
+ }
96
+
97
+ export interface Inventory {
98
+ rows: readonly InventoryRow[];
99
+ }
100
+
101
+ /** A leaf the discovery found, with the first source position that names it. */
102
+ export interface DiscoveredLeaf {
103
+ leaf: string;
104
+ locus: string;
105
+ }
106
+
107
+ const LEAF_RE =
108
+ /agents\/runtime\/state\/([A-Za-z0-9_.-]+)|['"]runtime['"],\s*['"]state['"],\s*['"]([A-Za-z0-9_.-]+)['"]/g;
109
+
110
+ const SOURCE_EXT = /\.(ts|js|mjs|sh|bash)$/;
111
+
112
+ function _walk(dir: string, out: string[]): void {
113
+ let entries: fs.Dirent[];
114
+ try {
115
+ entries = fs.readdirSync(dir, { withFileTypes: true });
116
+ } catch {
117
+ return;
118
+ }
119
+ for (const entry of entries) {
120
+ const full = path.join(dir, entry.name);
121
+ if (entry.isDirectory()) {
122
+ _walk(full, out);
123
+ continue;
124
+ }
125
+ if (!SOURCE_EXT.test(entry.name)) continue;
126
+ // Test files name state paths in fixtures; a fixture is not a surface.
127
+ if (entry.name.endsWith('.test.ts')) continue;
128
+ out.push(full);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Every distinct `agents/runtime/state/<leaf>` any source file names, with the
134
+ * first `file:line` that names it. Sorted, so two runs over the same tree
135
+ * produce byte-identical output and a diff of the count is a diff of the tree.
136
+ */
137
+ export function discoverStateLeaves(root: string): DiscoveredLeaf[] {
138
+ const files: string[] = [];
139
+ for (const rel of DISCOVERY_ROOTS) {
140
+ _walk(path.join(root, rel), files);
141
+ }
142
+ files.sort();
143
+ const found = new Map<string, string>();
144
+ for (const file of files) {
145
+ let text: string;
146
+ try {
147
+ text = fs.readFileSync(file, 'utf-8');
148
+ } catch {
149
+ continue;
150
+ }
151
+ const rel = path.relative(root, file).split(path.sep).join('/');
152
+ const lines = text.split('\n');
153
+ for (let i = 0; i < lines.length; i += 1) {
154
+ const line = lines[i] ?? '';
155
+ LEAF_RE.lastIndex = 0;
156
+ let match: RegExpExecArray | null;
157
+ while ((match = LEAF_RE.exec(line)) !== null) {
158
+ const raw = match[1] ?? match[2] ?? '';
159
+ // A trailing dot is string concatenation in the source
160
+ // (`'...json' + '.tmp'`), never part of the leaf name.
161
+ const leaf = raw.replace(/\.+$/, '');
162
+ if (leaf === '' || leaf === '.') continue;
163
+ if (!found.has(leaf)) found.set(leaf, `${rel}:${String(i + 1)}`);
164
+ }
165
+ }
166
+ }
167
+ return [...found.keys()].sort().map((leaf) => ({ leaf, locus: found.get(leaf) ?? '' }));
168
+ }
169
+
170
+ export function readInventory(root: string): Inventory {
171
+ const file = path.join(root, INVENTORY_POSIX);
172
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf-8')) as { rows?: unknown };
173
+ const rows = Array.isArray(parsed.rows) ? (parsed.rows as InventoryRow[]) : [];
174
+ return { rows };
175
+ }
176
+
177
+ /** Does a `file` or `file:line` locus still resolve in this tree? */
178
+ export function locusResolves(root: string, locus: string): boolean {
179
+ const match = /^(.*?):(\d+)$/.exec(locus);
180
+ const rel = match ? (match[1] ?? '') : locus;
181
+ const target = path.join(root, rel);
182
+ let stat: fs.Stats;
183
+ try {
184
+ stat = fs.statSync(target);
185
+ } catch {
186
+ return false;
187
+ }
188
+ if (!match) return true;
189
+ if (!stat.isFile()) return false;
190
+ // A line anchor must still BE a line. Content is deliberately not compared:
191
+ // a locus that pins prose would rot on every reflow and turn the inventory
192
+ // into a line-number chore, which is how anchor checks get deleted.
193
+ const line = Number(match[2]);
194
+ let text: string;
195
+ try {
196
+ text = fs.readFileSync(target, 'utf-8');
197
+ } catch {
198
+ return false;
199
+ }
200
+ return line >= 1 && line <= text.split('\n').length;
201
+ }
202
+
203
+ export interface SurfaceCounts {
204
+ public_continuity_commands: number;
205
+ session_resume_pickers: number;
206
+ persistent_continuity_artefacts: number;
207
+ continuity_schemas: number;
208
+ normal_path_manual_actions: number;
209
+ }
210
+
211
+ export function countAxes(inv: Inventory): SurfaceCounts {
212
+ const counts = {
213
+ public_continuity_commands: 0,
214
+ session_resume_pickers: 0,
215
+ persistent_continuity_artefacts: 0,
216
+ continuity_schemas: 0,
217
+ normal_path_manual_actions: 0,
218
+ };
219
+ for (const row of inv.rows) {
220
+ if (row.disposition !== 'counted') continue;
221
+ if (!AXES.includes(row.axis)) continue;
222
+ counts[row.axis] += 1;
223
+ }
224
+ return counts;
225
+ }
226
+
227
+ export interface InventoryFinding {
228
+ kind: 'undisposed-leaf' | 'dead-locus' | 'bad-row' | 'duplicate-row';
229
+ detail: string;
230
+ }
231
+
232
+ /**
233
+ * Validate the inventory against the tree.
234
+ *
235
+ * Three failure classes, and each exists because a specific way of faking a
236
+ * lower number would otherwise work:
237
+ *
238
+ * - `undisposed-leaf` — a persisted-state leaf the source names and the
239
+ * inventory does not mention. This is the add-a-surface-quietly route.
240
+ * - `dead-locus` — a row anchored to something that no longer exists. This
241
+ * is the claim-a-retirement-that-did-not-happen route in reverse: it forces
242
+ * the row to be deleted (a real subtraction) rather than left standing.
243
+ * - `bad-row` / `duplicate-row` — shape defects that would make either of the
244
+ * above unenforceable.
245
+ */
246
+ export function validateInventory(
247
+ root: string,
248
+ inv: Inventory,
249
+ discovered: readonly DiscoveredLeaf[],
250
+ ): InventoryFinding[] {
251
+ const findings: InventoryFinding[] = [];
252
+ const seen = new Set<string>();
253
+ for (const row of inv.rows) {
254
+ const id = typeof row.id === 'string' ? row.id : '';
255
+ if (id === '') {
256
+ findings.push({ kind: 'bad-row', detail: 'a row carries no `id`' });
257
+ continue;
258
+ }
259
+ if (seen.has(id)) {
260
+ findings.push({ kind: 'duplicate-row', detail: `two rows share the id \`${id}\`` });
261
+ }
262
+ seen.add(id);
263
+ if (!AXES.includes(row.axis)) {
264
+ findings.push({ kind: 'bad-row', detail: `\`${id}\`: unknown axis \`${String(row.axis)}\`` });
265
+ }
266
+ if (row.disposition !== 'counted' && row.disposition !== 'excluded') {
267
+ findings.push({
268
+ kind: 'bad-row',
269
+ detail: `\`${id}\`: disposition must be \`counted\` or \`excluded\`, got \`${String(row.disposition)}\``,
270
+ });
271
+ }
272
+ if (typeof row.reason !== 'string' || row.reason.trim().length < 20) {
273
+ findings.push({
274
+ kind: 'bad-row',
275
+ detail: `\`${id}\`: every row needs a reason of substance — a number without a stated meaning is not an inventory`,
276
+ });
277
+ }
278
+ if (typeof row.locus !== 'string' || row.locus.trim() === '') {
279
+ findings.push({ kind: 'bad-row', detail: `\`${id}\`: no \`locus\`` });
280
+ } else if (!locusResolves(root, row.locus)) {
281
+ findings.push({
282
+ kind: 'dead-locus',
283
+ detail: `\`${id}\`: locus \`${row.locus}\` does not resolve — if the surface was retired, DELETE the row (that is the subtraction); if it moved, re-anchor it`,
284
+ });
285
+ }
286
+ }
287
+ for (const item of discovered) {
288
+ if (!seen.has(item.leaf)) {
289
+ findings.push({
290
+ kind: 'undisposed-leaf',
291
+ detail: `\`${item.leaf}\` (${item.locus}) is persisted state under agents/runtime/state/ that the inventory does not dispose of — add a row with \`disposition: counted\` (it is continuity state) or \`disposition: excluded\` with the reason it is not`,
292
+ });
293
+ }
294
+ }
295
+ return findings;
296
+ }
@@ -46,7 +46,19 @@ import {
46
46
  import { isExclusivelyPackageOnly } from '../../install/partitionEligibility.js';
47
47
  import { dedupableRules } from '../../install/ruleLayerPartition.js';
48
48
 
49
- export const CORPUS_MANIFEST_VERSION = 'corpus-manifest-v1';
49
+ /**
50
+ * Bumped to v2 on 2026-09-07 with the `partition_active` -> `host_layer_verified`
51
+ * rename, after a neutral review named the alternative: `parseManifest` accepts
52
+ * any object carrying the declared version and validates three fields, and
53
+ * `diffManifests` compares with `String(e) !== String(a)`. So a v1 manifest
54
+ * (which has `partition_active` and no `host_layer_verified`) compared against
55
+ * another v1 yields `"undefined" === "undefined"` on the renamed field — NO
56
+ * difference reported, on a field neither side has. That is verbatim the failure
57
+ * `parseManifest`'s own error text says the version check exists to prevent:
58
+ * "comparing it partially would report equivalence from the fields that happened
59
+ * to survive". A rename inside a frozen version is exactly that.
60
+ */
61
+ export const CORPUS_MANIFEST_VERSION = 'corpus-manifest-v2';
50
62
 
51
63
  /** The tool directory the experiment's corpus is enumerated from. */
52
64
  export const CORPUS_TOOL_DIR = '.claude/rules';
@@ -142,7 +154,7 @@ export interface ProjectionDecision {
142
154
  * `null` is the default and is the honest value for a capture that cannot
143
155
  * establish it. Every other field in this struct is a function of the
144
156
  * `(repoRoot, userHome)` pair the capture was given; this one is not.
145
- * `partitionActive` resolves through `resolvePartitionVerdict`, which reads
157
+ * `hostLayerVerified` resolves through `resolveHostLayerVerdict`, which reads
146
158
  * the host layer at `os.homedir()` and the install lockfile at its own
147
159
  * fixed location — ignoring `userHome` entirely — and memoises the answer
148
160
  * for the life of the process. So a capture over a temporary tree that
@@ -150,7 +162,7 @@ export interface ProjectionDecision {
150
162
  * one process would reuse the first verdict. A caller for which those
151
163
  * globals ARE the run's own host supplies the predicate; nobody else does.
152
164
  */
153
- readonly partition_active: boolean | null;
165
+ readonly host_layer_verified: boolean | null;
154
166
  readonly tool_id: string | null;
155
167
  /** The global layer directory, home-relative; `null` when this host has none. */
156
168
  readonly layer_dir: string | null;
@@ -365,7 +377,7 @@ export interface ProjectionDecisionOptions {
365
377
  * The host partition verdict, supplied by a caller for which the process
366
378
  * globals it reads are this run's own host. Omitted → `null`.
367
379
  */
368
- readonly partitionActive?: ((repoRoot: string) => boolean) | undefined;
380
+ readonly hostLayerVerified?: ((repoRoot: string) => boolean) | undefined;
369
381
  }
370
382
 
371
383
  export function captureProjectionDecision(
@@ -401,7 +413,8 @@ export function captureProjectionDecision(
401
413
  sha256: layerPath === null ? null : sha256OfFile(path.join(layerPath, n)),
402
414
  }));
403
415
  return {
404
- partition_active: opts.partitionActive === undefined ? null : opts.partitionActive(repoRoot),
416
+ host_layer_verified:
417
+ opts.hostLayerVerified === undefined ? null : opts.hostLayerVerified(repoRoot),
405
418
  tool_id: toolId,
406
419
  layer_dir: layerPath === null ? null : homeRelative(layerPath, userHome),
407
420
  carries: verdict.carries,
@@ -451,8 +464,8 @@ export interface CaptureOptions {
451
464
  readonly limit: number;
452
465
  readonly enumerationRule: string;
453
466
  readonly env?: Readonly<Record<string, string | undefined>> | undefined;
454
- /** See {@link ProjectionDecisionOptions.partitionActive}. Omitted → `null`. */
455
- readonly partitionActive?: ((repoRoot: string) => boolean) | undefined;
467
+ /** See {@link ProjectionDecisionOptions.hostLayerVerified}. Omitted → `null`. */
468
+ readonly hostLayerVerified?: ((repoRoot: string) => boolean) | undefined;
456
469
  /** See {@link UserScopeOptions.ruleNames}. Omitted → the projection-source superset. */
457
470
  readonly ruleNames?: readonly string[] | undefined;
458
471
  }
@@ -531,7 +544,7 @@ export function captureManifest(opts: CaptureOptions): CorpusManifest {
531
544
  user_scope_population:
532
545
  opts.ruleNames === undefined ? 'projection-source-superset' : 'caller-supplied',
533
546
  projection: captureProjectionDecision(repoRoot, userHome, {
534
- partitionActive: opts.partitionActive,
547
+ hostLayerVerified: opts.hostLayerVerified,
535
548
  }),
536
549
  generator_config: generatorConfig,
537
550
  runtime: { node: process.version, platform: os.platform(), arch: os.arch() },
@@ -656,7 +669,11 @@ export function diffManifests(expected: CorpusManifest, actual: CorpusManifest):
656
669
  // The partition half, which is where the observed skips actually came from.
657
670
  // Diffed by digest rather than by name list because the inventory runs to
658
671
  // three figures and a diff nobody reads is a diff that does not exist.
659
- push('projection.partition_active', expected.projection.partition_active, actual.projection.partition_active);
672
+ push(
673
+ 'projection.host_layer_verified',
674
+ expected.projection.host_layer_verified,
675
+ actual.projection.host_layer_verified,
676
+ );
660
677
  push('projection.carries', expected.projection.carries, actual.projection.carries);
661
678
  push('projection.reason', expected.projection.reason, actual.projection.reason);
662
679
  push('projection.layer_digest', expected.projection.layer_digest, actual.projection.layer_digest);
@@ -3,7 +3,7 @@
3
3
  * `.agent-settings.yml` without dragging a YAML parser into a hook.
4
4
  *
5
5
  * Every default-OFF hook needs the same two-level lookup, and the tree already
6
- * carries two hand-rolled copies of it (`code_graph_nudge_hook.enabled` and the
6
+ * carries two hand-rolled copies of it (`code_graph_context_hook (no flag)` and the
7
7
  * equivalent inside `design_slop_hook`). Adding a third copy for the route
8
8
  * nudge is what this file prevents; the two existing copies are pre-existing
9
9
  * duplication and are deliberately left alone — folding them in is a separate,
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Does a delivered rule body actually reach the model's context on this host?
3
+ *
4
+ * `road-to-delivery-on-hook-hosts` step 1.1, and the record owner ruling E3
5
+ * admits a host on.
6
+ *
7
+ * THE ONE THING THIS MODULE EXISTS TO REFUSE
8
+ * ------------------------------------------
9
+ * A host that BINDS `user_prompt_submit` looks, from every artefact in this
10
+ * tree, exactly like a host that DELIVERS. Cowork is the proof that they are
11
+ * different: it binds eight slots and its trampoline discards dispatcher output
12
+ * and `exit 0`s (`docs/enforcement-by-host.md:21`). Cursor and Cline bind the
13
+ * same slot the delivery concern needs, and whether their hook stdout reaches
14
+ * the model is unmeasured.
15
+ *
16
+ * So the binding is computable and the effect is not. This module keeps them
17
+ * apart by construction:
18
+ *
19
+ * · `emits` — computed. Is the concern bound on the slot at all?
20
+ * Read from the manifest. Cheap, always available,
21
+ * and NEVER promoted to an observation.
22
+ * · `injection_effect` — observed. Did a model visibly act on a delivered
23
+ * body? Read from a committed record and from nowhere
24
+ * else. There is no code path that derives it.
25
+ *
26
+ * An `observed-*` value is admissible only with the four-part citation the
27
+ * host-capability observation protocol requires (host · host version ·
28
+ * transcript or artefact reference · date) — see
29
+ * `_lib/host_capability.ts:183`. A record missing any part is rejected rather
30
+ * than downgraded, because a half-cited observation is the shape that gets
31
+ * quoted as a whole one.
32
+ *
33
+ * `unobserved` is a RESULT, not a gap. Most hosts cannot be observed from a
34
+ * Claude Code session at all, and the roadmap's K6 makes that an outcome: the
35
+ * host keeps its full rule bodies and loses nothing while it waits.
36
+ */
37
+
38
+ import * as fs from 'node:fs';
39
+ import * as path from 'node:path';
40
+
41
+ /** Where the committed observations live. One file, so there is one place to audit. */
42
+ export const RECORD_REL = path.join('src', 'config', 'host-injection-effect.json');
43
+
44
+ /**
45
+ * The three states, and the reason there is no fourth.
46
+ *
47
+ * `observed-false` is not "probably not" — it is a recorded observation that a
48
+ * delivered body did NOT reach the model, which is as citable as the positive.
49
+ * Anything not observed in either direction is `unobserved`.
50
+ */
51
+ export const INJECTION_STATES = ['observed-true', 'observed-false', 'unobserved'] as const;
52
+ export type InjectionState = (typeof INJECTION_STATES)[number];
53
+
54
+ export interface InjectionObservation {
55
+ readonly state: InjectionState;
56
+ /** Host version the observation was taken on. Required for an `observed-*` state. */
57
+ readonly host_version: string | null;
58
+ /** Transcript or artefact reference. Required for an `observed-*` state. */
59
+ readonly transcript: string | null;
60
+ /** ISO date of the observation. Required for an `observed-*` state. */
61
+ readonly date: string | null;
62
+ /** Why it reads the way it does — required always, including for `unobserved`. */
63
+ readonly reason: string;
64
+ }
65
+
66
+ export type InjectionRecord = Readonly<Record<string, InjectionObservation>>;
67
+
68
+ /** A malformed or under-cited row, in the words a maintainer needs to fix it. */
69
+ export interface InjectionProblem {
70
+ readonly host: string;
71
+ readonly message: string;
72
+ }
73
+
74
+ /**
75
+ * Validate a record.
76
+ *
77
+ * The citation requirement is enforced HERE rather than at the reader, so every
78
+ * consumer gets the same answer: an `observed-*` row without all three citation
79
+ * fields is a problem, and `unobserved` needs only its reason.
80
+ */
81
+ export function recordProblems(rec: InjectionRecord): InjectionProblem[] {
82
+ const out: InjectionProblem[] = [];
83
+ for (const [host, obs] of Object.entries(rec)) {
84
+ // A leading `_comment` is the convention every config JSON in this tree
85
+ // uses to carry its own contract, and it is not a host row. Skipping the
86
+ // underscore prefix rather than a literal `_comment` keeps a second
87
+ // annotation key from becoming a fake host with a fake verdict.
88
+ if (host.startsWith('_')) continue;
89
+ if (!(INJECTION_STATES as readonly string[]).includes(obs.state)) {
90
+ out.push({ host, message: `state ${JSON.stringify(obs.state)} is not one of ${INJECTION_STATES.join(' | ')}` });
91
+ continue;
92
+ }
93
+ if (typeof obs.reason !== 'string' || obs.reason.trim().length < 20) {
94
+ out.push({ host, message: 'reason is missing or shorter than 20 characters — an unexplained row reads as answered' });
95
+ }
96
+ if (obs.state === 'unobserved') continue;
97
+ for (const field of ['host_version', 'transcript', 'date'] as const) {
98
+ const v = obs[field];
99
+ if (typeof v !== 'string' || v.trim() === '') {
100
+ out.push({
101
+ host,
102
+ message:
103
+ `state ${obs.state} without \`${field}\` — the observation protocol ` +
104
+ '(host · host version · transcript · date) admits no partial citation',
105
+ });
106
+ }
107
+ }
108
+ }
109
+ return out;
110
+ }
111
+
112
+ /** Load the committed record. A missing file is an empty record, never a throw. */
113
+ export function loadRecord(root: string): InjectionRecord {
114
+ try {
115
+ return JSON.parse(fs.readFileSync(path.join(root, RECORD_REL), 'utf-8')) as InjectionRecord;
116
+ } catch {
117
+ return {};
118
+ }
119
+ }
120
+
121
+ /**
122
+ * The state for one host — `unobserved` when the record says nothing.
123
+ *
124
+ * Defaulting to `unobserved` rather than to a computed guess is the whole
125
+ * contract: a host nobody has looked at must never inherit a neighbour's
126
+ * verdict, and a bound slot must never imply a delivered body.
127
+ */
128
+ export function stateFor(rec: InjectionRecord, host: string): InjectionState {
129
+ return rec[host]?.state ?? 'unobserved';
130
+ }
131
+
132
+ /**
133
+ * Is the concern bound on this host's slot? Computed from the manifest.
134
+ *
135
+ * Deliberately a separate function from `stateFor`, with a name that cannot be
136
+ * mistaken for it. This answers "could a body be emitted here", never "did one
137
+ * arrive".
138
+ */
139
+ export function concernBound(
140
+ platforms: Readonly<Record<string, Readonly<Record<string, readonly string[]>>>>,
141
+ host: string,
142
+ slot: string,
143
+ concern: string,
144
+ ): boolean {
145
+ return (platforms[host]?.[slot] ?? []).includes(concern);
146
+ }
147
+
148
+ /** Does this host admit to `lean_projection.hosts` under E3? Observation only. */
149
+ export function admissibleUnderE3(rec: InjectionRecord, host: string): boolean {
150
+ return stateFor(rec, host) === 'observed-true';
151
+ }
@@ -54,12 +54,16 @@ const TYPES = ['rules', 'skills', 'commands', 'personas', 'user-types', 'agents'
54
54
  export function overlapFindings(homeDir: string, projectRoot: string): string[] {
55
55
  const findings: string[] = [];
56
56
  for (const type of TYPES) {
57
- let g: string[];
58
- let p: string[];
59
- try {
60
- g = fs.readdirSync(path.join(homeDir, '.claude', type));
61
- p = fs.readdirSync(path.join(projectRoot, '.claude', type));
62
- } catch {
57
+ // `commands` keys on the POSIX SUBPATH, matching
58
+ // `check_single_delivery`'s own key. Top-level names are the CLUSTER, and
59
+ // since per-name withholding (2026-09-07) the project layer holds exactly
60
+ // the commands the host lacks — which can share a cluster with commands the
61
+ // host has. Observed the same day: `analyze/` in both layers over two
62
+ // disjoint command sets, reported here as `commands=1`. This notice and
63
+ // that gate must not disagree about the same question.
64
+ const g = _entries(path.join(homeDir, '.claude', type), type === 'commands');
65
+ const p = _entries(path.join(projectRoot, '.claude', type), type === 'commands');
66
+ if (g === null || p === null) {
63
67
  continue;
64
68
  }
65
69
  const gset = new Set(g);
@@ -69,6 +73,46 @@ export function overlapFindings(homeDir: string, projectRoot: string): string[]
69
73
  return findings;
70
74
  }
71
75
 
76
+ /**
77
+ * One layer's entry names — top level, or every `*.md` subpath when `recursive`.
78
+ *
79
+ * `null` means the directory is not readable, which the caller must treat as "skip
80
+ * this type": one layer absent means nothing is doubled there.
81
+ */
82
+ function _entries(dir: string, recursive: boolean): string[] | null {
83
+ if (!recursive) {
84
+ try {
85
+ return fs.readdirSync(dir);
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+ let top: fs.Dirent[];
91
+ try {
92
+ top = fs.readdirSync(dir, { withFileTypes: true });
93
+ } catch {
94
+ return null;
95
+ }
96
+ const out: string[] = [];
97
+ const walk = (kids: fs.Dirent[], abs: string, rel: string): void => {
98
+ for (const k of kids) {
99
+ const childRel = rel === '' ? k.name : `${rel}/${k.name}`;
100
+ if (k.isDirectory()) {
101
+ try {
102
+ walk(fs.readdirSync(path.join(abs, k.name), { withFileTypes: true }), path.join(abs, k.name), childRel);
103
+ } catch {
104
+ // An unreadable subdirectory contributes no names; the layer
105
+ // itself was readable, so the type stays compared.
106
+ }
107
+ } else if (k.name.endsWith('.md') && k.name !== 'README.md') {
108
+ out.push(childRel);
109
+ }
110
+ }
111
+ };
112
+ walk(top, dir, '');
113
+ return out;
114
+ }
115
+
72
116
  /** Render the notice, or `null` when there is nothing to say. */
73
117
  export function overlapNotice(homeDir: string, projectRoot: string): string | null {
74
118
  const findings = overlapFindings(homeDir, projectRoot);