@event4u/agent-config 14.20.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 (242) 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 +441 -0
  5. package/CREDITS.md +37 -0
  6. package/README.md +1 -1
  7. package/agents/templates/.ai-council.yml.example +10 -0
  8. package/dist/agent-src/commands/analyze/inbox.md +381 -6
  9. package/dist/agent-src/commands/analyze/{reference-repo.md → repo.md} +251 -34
  10. package/dist/agent-src/commands/analyze/roadmap-repos.md +129 -0
  11. package/dist/agent-src/commands/analyze.md +12 -7
  12. package/dist/agent-src/commands/feature/plan.md +55 -16
  13. package/dist/agent-src/commands/feature/refactor.md +8 -3
  14. package/dist/agent-src/commands/humanize.md +48 -4
  15. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  16. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  17. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +22 -1
  18. package/dist/agent-src/contexts/execution/host-capability-manifest.md +4 -1
  19. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +35 -2
  20. package/dist/agent-src/contexts/execution/terminal-states.md +4 -1
  21. package/dist/agent-src/rules/evaluator-independence.md +4 -4
  22. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  23. package/dist/agent-src/rules/external-reference-deep-dive.md +2 -2
  24. package/dist/agent-src/rules/token-efficiency.md +26 -3
  25. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  26. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  27. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  28. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  29. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  30. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  31. package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
  32. package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
  33. package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +27 -11
  34. package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
  35. package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
  36. package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
  37. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  38. package/dist/agent-src/skills/technical-specification/SKILL.md +4 -0
  39. package/dist/agent-src/templates/agent-settings.md +0 -1
  40. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  41. package/dist/agent-src/templates/features.md +5 -0
  42. package/dist/agent-src/templates/roadmaps.md +15 -0
  43. package/dist/cli-delegate/_cli_entry.js +8 -0
  44. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  45. package/dist/cli-delegate/archival_sweep.js +8 -0
  46. package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
  47. package/dist/cli-delegate/check_update_banner.js +923 -0
  48. package/dist/cli-delegate/{chunk-KPBWFOPZ.js → chunk-2FTMENJL.js} +194 -37
  49. package/dist/cli-delegate/{chunk-PI2GU3DP.js → chunk-2TAQ2K4V.js} +4 -1
  50. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  51. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  52. package/dist/cli-delegate/{chunk-C6IPZRDA.js → chunk-BIXEDQPZ.js} +7 -1
  53. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  54. package/dist/cli-delegate/{chunk-NUMQHLS5.js → chunk-J3RFNKG4.js} +21 -2
  55. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  56. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  57. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  58. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
  59. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  60. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  61. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  62. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  63. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  64. package/dist/cli-delegate/cmd_conformance.js +19 -5
  65. package/dist/cli-delegate/cmd_doctor.js +3 -3
  66. package/dist/cli-delegate/cmd_export.js +2 -2
  67. package/dist/cli-delegate/cmd_handoff.js +27 -3
  68. package/dist/cli-delegate/cmd_prune.js +2 -2
  69. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  70. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  71. package/dist/cli-delegate/cmd_sync.js +2 -2
  72. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  73. package/dist/cli-delegate/cmd_validate.js +2 -2
  74. package/dist/cli-delegate/dashboard_mode.js +16 -0
  75. package/dist/cli-delegate/gate_budget.js +24 -0
  76. package/dist/cli-delegate/gate_execute.js +25 -0
  77. package/dist/cli-delegate/guarded_baseline.js +18 -0
  78. package/dist/cli-delegate/resume_probe.js +22 -0
  79. package/dist/cli-delegate/roadmap_gates.js +638 -0
  80. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  81. package/dist/cli-delegate/stub_queue.js +47 -0
  82. package/dist/cli-delegate/stubs_due.js +38 -0
  83. package/dist/cli-delegate/ticket_bundles.js +59 -0
  84. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  85. package/dist/discovery/deprecation-report.md +1 -1
  86. package/dist/discovery/discovery-manifest.json +60 -29
  87. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  88. package/dist/discovery/discovery-manifest.summary.md +3 -3
  89. package/dist/discovery/dormancy-report.md +1 -1
  90. package/dist/discovery/orphan-report.md +1 -1
  91. package/dist/discovery/packs.json +7 -6
  92. package/dist/discovery/trust-report.md +2 -2
  93. package/dist/discovery/workspaces.json +6 -5
  94. package/dist/hooks/dispatch.js +1522 -1008
  95. package/dist/install/claudeLayerCarriage.js +237 -0
  96. package/dist/install/claudePathsPlan.js +79 -0
  97. package/dist/install/hostLayerFingerprint.js +1 -1
  98. package/dist/install/install.mjs +10 -1
  99. package/dist/install/partitionEligibility.js +231 -117
  100. package/dist/install/ruleLayerPartition.js +10 -7
  101. package/dist/mcp/registry-manifest.json +3 -3
  102. package/dist/mcp/server.json +2 -2
  103. package/dist/mcp/server.mjs +39376 -38902
  104. package/dist/scripts/ai_council/content_ceiling.js +63 -0
  105. package/dist/scripts/ai_council/seat_policy.js +106 -0
  106. package/dist/scripts/ai_council/transport_resolver.js +21 -0
  107. package/dist/scripts/condense.js +32 -25
  108. package/docs/MIGRATION.md +49 -1
  109. package/docs/contracts/settings-classes.md +1 -1
  110. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  111. package/llms.txt +14 -14
  112. package/package.json +7 -4
  113. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  114. package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  115. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  116. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  117. package/src/agent-src/scripts/stubs_due.ts +4 -18
  118. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  119. package/src/config/continuity-surface.json +509 -0
  120. package/src/config/estate-count-budget.json +1 -1
  121. package/src/config/gate-coverage.yml +29 -8
  122. package/src/config/gate-violation-baselines.json +8 -8
  123. package/src/config/gitignore-block.txt +19 -0
  124. package/src/config/host-injection-effect.json +73 -0
  125. package/src/config/packed-binary-manifest.json +33 -0
  126. package/src/config/preamble-payload-budget.json +112 -105
  127. package/src/config/publish-surface.json +4 -2
  128. package/src/config/release-gate-locality.yml +4 -3
  129. package/src/install/claudeLayerCarriage.ts +273 -0
  130. package/src/install/claudePathsPlan.ts +76 -0
  131. package/src/install/hostLayerFingerprint.ts +1 -1
  132. package/src/install/partitionEligibility.ts +261 -127
  133. package/src/install/ruleLayerPartition.ts +19 -23
  134. package/src/scripts/_dispatch.bash +77 -27
  135. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  136. package/src/scripts/_lib/blocked_by_marker.ts +189 -0
  137. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  138. package/src/scripts/_lib/continuity_surface.ts +296 -0
  139. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  140. package/src/scripts/_lib/hook_settings.ts +1 -1
  141. package/src/scripts/_lib/host_capability.ts +38 -1
  142. package/src/scripts/_lib/injection_effect.ts +151 -0
  143. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  144. package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
  145. package/src/scripts/_lib/pin_resolver.ts +13 -0
  146. package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
  147. package/src/scripts/_lib/release_highlights.ts +19 -211
  148. package/src/scripts/_lib/release_material.ts +27 -77
  149. package/src/scripts/_lib/retired_status.ts +74 -0
  150. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  151. package/src/scripts/_lib/structured_ask.ts +117 -0
  152. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  153. package/src/scripts/_lib/tty_prompt.ts +76 -0
  154. package/src/scripts/ai_council/api_transport.ts +168 -0
  155. package/src/scripts/ai_council/clients.ts +58 -63
  156. package/src/scripts/ai_council/config.ts +13 -0
  157. package/src/scripts/ai_council/content_ceiling.ts +79 -0
  158. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  159. package/src/scripts/ai_council/jury_aggregate.ts +178 -0
  160. package/src/scripts/ai_council/prompts.ts +9 -0
  161. package/src/scripts/ai_council/seat_policy.ts +137 -0
  162. package/src/scripts/ai_council/status_surface.ts +79 -0
  163. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  164. package/src/scripts/ai_council/transport_resolver.ts +59 -1
  165. package/src/scripts/ai_tells_rules.ts +446 -5
  166. package/src/scripts/ask_block_census.ts +499 -0
  167. package/src/scripts/bench_humanizer_eval.ts +264 -40
  168. package/src/scripts/bench_prose_tells_fp.ts +198 -0
  169. package/src/scripts/capture_skill_catalogue.ts +24 -11
  170. package/src/scripts/check_branch_work_committed.ts +234 -0
  171. package/src/scripts/check_candidate_lines.ts +499 -0
  172. package/src/scripts/check_continuity_surface.ts +475 -0
  173. package/src/scripts/check_dependency_floors.ts +13 -6
  174. package/src/scripts/check_estate_count.ts +40 -79
  175. package/src/scripts/check_pack_size.ts +263 -24
  176. package/src/scripts/check_release_highlights.ts +0 -157
  177. package/src/scripts/check_roadmap_trackable.ts +19 -16
  178. package/src/scripts/check_rule_layer_partition.ts +54 -34
  179. package/src/scripts/check_single_delivery.ts +97 -31
  180. package/src/scripts/check_update_banner.ts +13 -0
  181. package/src/scripts/code_graph/build.ts +138 -15
  182. package/src/scripts/code_graph/cli.ts +76 -8
  183. package/src/scripts/code_graph/extract.ts +11 -0
  184. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  185. package/src/scripts/code_graph/loader.ts +88 -14
  186. package/src/scripts/code_graph/query.ts +121 -11
  187. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  188. package/src/scripts/code_graph/sqlite_store.ts +247 -6
  189. package/src/scripts/code_graph/types.ts +66 -1
  190. package/src/scripts/code_graph/validate.ts +23 -0
  191. package/src/scripts/condense.ts +34 -22
  192. package/src/scripts/corpus_manifest.ts +5 -5
  193. package/src/scripts/council_cli.ts +29 -39
  194. package/src/scripts/count_intent_disagreement.ts +426 -0
  195. package/src/scripts/detect_ai_tells.ts +142 -20
  196. package/src/scripts/handoff_context_hook.ts +79 -0
  197. package/src/scripts/harvest_reference_tokens.ts +698 -0
  198. package/src/scripts/hook_effect_doctor.ts +82 -1
  199. package/src/scripts/hook_manifest.json +1 -1
  200. package/src/scripts/hook_manifest.yaml +94 -12
  201. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  202. package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
  203. package/src/scripts/hooks/concern_registry.ts +6 -2
  204. package/src/scripts/hooks/delegation_nudge_hook.ts +45 -1
  205. package/src/scripts/hooks/host_semantics.ts +1 -1
  206. package/src/scripts/hooks/one_question_per_ask_hook.ts +191 -0
  207. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  208. package/src/scripts/hooks/rule_inject_hook.ts +1 -1
  209. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  210. package/src/scripts/inbox_source_census.ts +322 -0
  211. package/src/scripts/install-hooks.sh +134 -0
  212. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  213. package/src/scripts/lint_plan_risk_register.ts +13 -17
  214. package/src/scripts/lint_roadmap_blockers.ts +51 -1
  215. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  216. package/src/scripts/mcp_server/tools.ts +2 -56
  217. package/src/scripts/media/lib/adapter-contract.md +7 -2
  218. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  219. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  220. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  221. package/src/scripts/migrate_command_suggestions.ts +1 -1
  222. package/src/scripts/probe_unblocked_ask.ts +135 -2
  223. package/src/scripts/rdp_candidates_delta.ts +276 -0
  224. package/src/scripts/rdp_quality_eval.ts +217 -19
  225. package/src/scripts/release.ts +26 -71
  226. package/src/scripts/release_drill.ts +6 -10
  227. package/src/scripts/release_env.ts +1 -0
  228. package/src/scripts/release_mix_taxonomy.json +3 -2
  229. package/src/scripts/release_publication.ts +34 -29
  230. package/src/scripts/report_host_injection_effect.ts +189 -0
  231. package/src/scripts/report_layer_overlap.ts +41 -0
  232. package/src/scripts/report_skill_menu_census.ts +341 -0
  233. package/src/scripts/self_review_gate.ts +409 -15
  234. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  235. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  236. package/src/vendor/grammars/README.md +61 -0
  237. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  238. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  239. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  240. package/src/scripts/_lib/carrier_status.ts +0 -118
  241. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  242. 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,
@@ -67,6 +67,23 @@ export interface HostCapabilityManifest {
67
67
  * stop-loss behaviour — loudly, never silently.
68
68
  */
69
69
  worker_respawn: boolean;
70
+ /**
71
+ * The host exposes its own structured-ask tool — a native question picker
72
+ * the agent can call instead of rendering a numbered-options block as prose.
73
+ *
74
+ * A STRICT BOOLEAN, and the per-host detail deliberately lives elsewhere.
75
+ * The source draft for road-to-asked-not-parked proposed a nested object
76
+ * carrying the tool name and the question/option ceilings; `asBool` below
77
+ * coerces every non-`true` value to `false`, so such an object would
78
+ * normalize silently to nothing. The shape lives in
79
+ * `_lib/structured_ask.ts` (`STRUCTURED_ASK_SHAPES`), which a caller reads
80
+ * explicitly; this field answers only whether there is anything to read.
81
+ *
82
+ * `false` is the safe default on purpose: an unknown host degrades to a
83
+ * text ask, and the agent never fires a tool call into a host that has no
84
+ * such tool.
85
+ */
86
+ structured_ask: boolean;
70
87
  /**
71
88
  * The host reads a PROJECT-scope MCP config file (`.mcp.json` or its
72
89
  * per-host equivalent) and starts the servers it names.
@@ -105,6 +122,7 @@ const SAFE_DEFAULT: HostCapabilityManifest = {
105
122
  separate_quota_pool: false,
106
123
  agent_teams: false,
107
124
  worker_respawn: false,
125
+ structured_ask: false,
108
126
  reads_project_mcp_config: false,
109
127
  // `true` because the safe answer here is "there is still a step", not
110
128
  // "there is nothing to do" — see the field's own note.
@@ -137,6 +155,7 @@ export function normalizeHostManifest(input: unknown): HostCapabilityManifest {
137
155
  separate_quota_pool: asBool(src.separate_quota_pool),
138
156
  agent_teams: asBool(src.agent_teams),
139
157
  worker_respawn: asBool(src.worker_respawn),
158
+ structured_ask: asBool(src.structured_ask),
140
159
  reads_project_mcp_config: asBool(src.reads_project_mcp_config),
141
160
  // Inverted coercion, matching the inverted default: only an explicit
142
161
  // `false` clears the residual, so an absent or malformed value keeps
@@ -215,9 +234,26 @@ export function normalizeHostManifest(input: unknown): HostCapabilityManifest {
215
234
  *
216
235
  * `agent_teams` — out of scope for a row by construction; see its own field
217
236
  * doc comment and the protocol section.
237
+ *
238
+ * `structured_ask: false` — OBSERVED ABSENT, not never-looked, and the
239
+ * difference is why it is written rather than omitted. claude (Claude Code
240
+ * 2.1.263, Opus 5 1M session, 2026-09-07). The host delivers the session's
241
+ * tool surface at session start; that surface carried no structured-ask tool
242
+ * under any name `_lib/structured_ask.ts` matches, and no per-host shape is
243
+ * recorded for it in `STRUCTURED_ASK_SHAPES`. Artefact:
244
+ * `agents/evidence/analysis/structured-ask-host-observation-2026-09.md`. code-comment-allow provenance-comment -- the observation protocol declares a row inadmissible without its artefact citation, so this pointer is part of the contract the row satisfies, not evidence duplicated from a roadmap
245
+ * The observation is about the delivered surface of one session on one
246
+ * version; it is not a claim that the vendor ships no such tool anywhere,
247
+ * and a later session observing one writes the `true` row over it.
248
+ *
249
+ * NO ROW ANYWHERE SETS THE STRUCTURED-ASK FIELD TRUE, and the observation
250
+ * protocol above is the only path to the first one that does. A vendor's
251
+ * documentation is not that path — and the phrasing here avoids the literal
252
+ * key-value pair on purpose, because the roadmap step that ships this field
253
+ * verifies the absence with a grep for exactly that string.
218
254
  */
219
255
  const HOST_CAPABILITY_REGISTRY: Readonly<Record<string, Partial<HostCapabilityManifest>>> = {
220
- claude: { subagent_spawn: true, parallel_spawn: true },
256
+ claude: { subagent_spawn: true, parallel_spawn: true, structured_ask: false },
221
257
  };
222
258
 
223
259
  /**
@@ -323,6 +359,7 @@ const CAPABILITY_FIELDS: readonly CapabilityField[] = [
323
359
  'separate_quota_pool',
324
360
  'agent_teams',
325
361
  'worker_respawn',
362
+ 'structured_ask',
326
363
  'reads_project_mcp_config',
327
364
  'mcp_needs_manual_activation',
328
365
  ];
@@ -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
+ }