@event4u/agent-config 14.21.0 → 14.23.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 (235) 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 +164 -187
  5. package/CREDITS.md +37 -0
  6. package/README.md +1 -1
  7. package/dist/agent-src/commands/analyze/inbox.md +380 -5
  8. package/dist/agent-src/commands/analyze/repo.md +4 -2
  9. package/dist/agent-src/commands/pr/merge.md +47 -38
  10. package/dist/agent-src/commands/roadmap/next.md +8 -0
  11. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  12. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  13. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +12 -0
  14. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  15. package/dist/agent-src/rules/notes-first-reasoning.md +36 -25
  16. package/dist/agent-src/rules/token-efficiency.md +26 -3
  17. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  18. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
  19. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  20. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  21. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  22. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  23. package/dist/agent-src/skills/code-intelligence/SKILL.md +82 -1
  24. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +9 -0
  25. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  26. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +52 -2
  27. package/dist/agent-src/templates/agent-settings.md +0 -1
  28. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  29. package/dist/cli/registry.js +1 -1
  30. package/dist/cli-delegate/_cli_entry.js +8 -0
  31. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  32. package/dist/cli-delegate/archival_sweep.js +8 -0
  33. package/dist/cli-delegate/archive_completed_roadmaps.js +542 -0
  34. package/dist/cli-delegate/check_update_banner.js +923 -0
  35. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-2KKXJW25.js} +23 -9
  36. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  37. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  38. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  39. package/dist/cli-delegate/{chunk-4FS4WRXP.js → chunk-IAI3XL43.js} +15 -3
  40. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  41. package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-KCNTUSPI.js} +7 -1
  42. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  43. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  44. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  45. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  46. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  47. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  48. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  49. package/dist/cli-delegate/cmd_conformance.js +2 -2
  50. package/dist/cli-delegate/cmd_export.js +2 -2
  51. package/dist/cli-delegate/cmd_handoff.js +45 -13
  52. package/dist/cli-delegate/cmd_prune.js +2 -2
  53. package/dist/cli-delegate/cmd_session_recycle.js +4 -14
  54. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  55. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  56. package/dist/cli-delegate/cmd_sync.js +2 -2
  57. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  58. package/dist/cli-delegate/cmd_validate.js +2 -2
  59. package/dist/cli-delegate/dashboard_mode.js +16 -0
  60. package/dist/cli-delegate/gate_budget.js +24 -0
  61. package/dist/cli-delegate/gate_execute.js +25 -0
  62. package/dist/cli-delegate/guarded_baseline.js +18 -0
  63. package/dist/cli-delegate/resume_probe.js +22 -0
  64. package/dist/cli-delegate/roadmap_gates.js +638 -0
  65. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  66. package/dist/cli-delegate/stub_queue.js +47 -0
  67. package/dist/cli-delegate/stubs_due.js +38 -0
  68. package/dist/cli-delegate/ticket_bundles.js +59 -0
  69. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  70. package/dist/discovery/deprecation-report.md +1 -1
  71. package/dist/discovery/discovery-manifest.json +13 -13
  72. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  73. package/dist/discovery/discovery-manifest.summary.md +1 -1
  74. package/dist/discovery/dormancy-report.md +1 -1
  75. package/dist/discovery/orphan-report.md +1 -1
  76. package/dist/discovery/packs.json +2 -2
  77. package/dist/discovery/trust-report.md +1 -1
  78. package/dist/discovery/workspaces.json +2 -2
  79. package/dist/hooks/dispatch.js +1813 -946
  80. package/dist/install/claudeLayerCarriage.js +237 -0
  81. package/dist/install/claudePathsPlan.js +79 -0
  82. package/dist/install/hostLayerFingerprint.js +1 -1
  83. package/dist/install/install.mjs +21 -3
  84. package/dist/install/partitionEligibility.js +231 -117
  85. package/dist/install/ruleLayerPartition.js +10 -7
  86. package/dist/mcp/registry-manifest.json +2 -2
  87. package/dist/mcp/server.json +2 -2
  88. package/dist/mcp/server.mjs +10870 -7641
  89. package/dist/scripts/_lib/lean_projection_mode.js +108 -0
  90. package/dist/scripts/_lib/router_match.js +243 -0
  91. package/dist/scripts/_lib/rule_injection.js +216 -0
  92. package/dist/scripts/condense.js +52 -45
  93. package/dist/scripts/project_thin_rules.js +139 -19
  94. package/dist/server/schemas/settings.js +16 -3
  95. package/docs/MIGRATION.md +105 -1
  96. package/docs/contracts/settings-classes.md +40 -8
  97. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  98. package/docs/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
  99. package/llms.txt +1 -1
  100. package/package.json +7 -4
  101. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  102. package/src/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
  103. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  104. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  105. package/src/agent-src/scripts/stubs_due.ts +4 -18
  106. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  107. package/src/config/agent-settings.template.yml +47 -1
  108. package/src/config/continuity-surface.json +516 -0
  109. package/src/config/estate-count-budget.json +1 -1
  110. package/src/config/evaluator-budgets.json +4 -4
  111. package/src/config/gate-coverage.yml +29 -8
  112. package/src/config/gate-violation-baselines.json +8 -8
  113. package/src/config/gitignore-block.txt +19 -0
  114. package/src/config/hook-token-budget.json +7 -6
  115. package/src/config/host-injection-effect.json +73 -0
  116. package/src/config/packed-binary-manifest.json +33 -0
  117. package/src/config/preamble-payload-budget.json +112 -105
  118. package/src/config/publish-surface.json +4 -2
  119. package/src/config/routing-coverage-seed.json +9 -2
  120. package/src/install/claudeLayerCarriage.ts +273 -0
  121. package/src/install/claudePathsPlan.ts +76 -0
  122. package/src/install/hostLayerFingerprint.ts +1 -1
  123. package/src/install/partitionEligibility.ts +261 -127
  124. package/src/install/ruleLayerPartition.ts +19 -23
  125. package/src/scripts/_cli/cmd_session_recycle.ts +2 -25
  126. package/src/scripts/_cli/handoff_sessions.ts +14 -44
  127. package/src/scripts/_dispatch.bash +77 -27
  128. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  129. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  130. package/src/scripts/_lib/continuity_slot.ts +295 -0
  131. package/src/scripts/_lib/continuity_surface.ts +296 -0
  132. package/src/scripts/_lib/continuity_writer.ts +253 -0
  133. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  134. package/src/scripts/_lib/hook_settings.ts +86 -1
  135. package/src/scripts/_lib/injection_effect.ts +151 -0
  136. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  137. package/src/scripts/_lib/lean_projection_mode.ts +127 -0
  138. package/src/scripts/_lib/packed_binary_predicate.ts +262 -0
  139. package/src/scripts/_lib/pin_resolver.ts +13 -0
  140. package/src/scripts/_lib/recycle_envelope_paths.ts +109 -21
  141. package/src/scripts/_lib/regression_neighbourhood.ts +159 -22
  142. package/src/scripts/_lib/retired_status.ts +74 -0
  143. package/src/scripts/_lib/session_eol.ts +44 -0
  144. package/src/scripts/_lib/session_index_trust.ts +346 -0
  145. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  146. package/src/scripts/_lib/standing_bound_ratchet.ts +154 -0
  147. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  148. package/src/scripts/_lib/vendored_grammar_upstream.ts +280 -0
  149. package/src/scripts/ai_council/api_transport.ts +5 -0
  150. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  151. package/src/scripts/ai_council/prompts.ts +9 -0
  152. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  153. package/src/scripts/audit_mcp_tools.ts +69 -25
  154. package/src/scripts/build_mcp_catalog.ts +24 -1
  155. package/src/scripts/capture_skill_catalogue.ts +24 -11
  156. package/src/scripts/check_candidate_lines.ts +499 -0
  157. package/src/scripts/check_continuity_surface.ts +475 -0
  158. package/src/scripts/check_dependency_floors.ts +13 -6
  159. package/src/scripts/check_estate_count.ts +40 -79
  160. package/src/scripts/check_host_tree_parity.ts +291 -0
  161. package/src/scripts/check_pack_size.ts +262 -23
  162. package/src/scripts/check_preamble_payload_budget.ts +136 -7
  163. package/src/scripts/check_roadmap_trackable.ts +19 -16
  164. package/src/scripts/check_rule_layer_partition.ts +54 -34
  165. package/src/scripts/check_rule_projection_integrity.ts +102 -2
  166. package/src/scripts/check_single_delivery.ts +97 -31
  167. package/src/scripts/check_update_banner.ts +13 -0
  168. package/src/scripts/ci_settle.ts +205 -5
  169. package/src/scripts/code_graph/build.ts +217 -15
  170. package/src/scripts/code_graph/cli.ts +334 -15
  171. package/src/scripts/code_graph/detect.ts +38 -0
  172. package/src/scripts/code_graph/extract.ts +11 -0
  173. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  174. package/src/scripts/code_graph/loader.ts +88 -14
  175. package/src/scripts/code_graph/query.ts +144 -11
  176. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  177. package/src/scripts/code_graph/sqlite_store.ts +262 -6
  178. package/src/scripts/code_graph/types.ts +117 -3
  179. package/src/scripts/code_graph/validate.ts +26 -1
  180. package/src/scripts/code_graph/verbs.ts +590 -0
  181. package/src/scripts/condense.ts +54 -42
  182. package/src/scripts/corpus_manifest.ts +5 -5
  183. package/src/scripts/council_cli.ts +11 -12
  184. package/src/scripts/count_intent_disagreement.ts +426 -0
  185. package/src/scripts/handoff_context_hook.ts +118 -1
  186. package/src/scripts/hook_effect_doctor.ts +82 -1
  187. package/src/scripts/hook_manifest.json +1 -1
  188. package/src/scripts/hook_manifest.yaml +87 -12
  189. package/src/scripts/hooks/block_speaking_inbox_dir.ts +182 -16
  190. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  191. package/src/scripts/hooks/code_graph_context_hook.ts +201 -0
  192. package/src/scripts/hooks/concern_registry.ts +4 -2
  193. package/src/scripts/hooks/host_semantics.ts +1 -1
  194. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  195. package/src/scripts/hooks/push_settle_hook.ts +3 -2
  196. package/src/scripts/hooks/rule_inject_hook.ts +108 -21
  197. package/src/scripts/hooks/session_eol_hook.ts +120 -2
  198. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  199. package/src/scripts/hot_context_hook.ts +62 -7
  200. package/src/scripts/inbox_source_census.ts +322 -0
  201. package/src/scripts/install-hooks.sh +82 -0
  202. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  203. package/src/scripts/lint_plan_risk_register.ts +13 -17
  204. package/src/scripts/lint_roadmap_blockers.ts +164 -1
  205. package/src/scripts/mcp_server/consumer_tool_catalog.json +152 -2
  206. package/src/scripts/mcp_server/graph_tools.ts +294 -0
  207. package/src/scripts/mcp_server/path_util.ts +31 -0
  208. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  209. package/src/scripts/mcp_server/tool_catalog_source.ts +7 -1
  210. package/src/scripts/mcp_server/tools.ts +5 -69
  211. package/src/scripts/media/lib/adapter-contract.md +7 -2
  212. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  213. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  214. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  215. package/src/scripts/model_rule_injection.ts +61 -4
  216. package/src/scripts/project_thin_rules.ts +149 -16
  217. package/src/scripts/rdp_candidates_delta.ts +276 -0
  218. package/src/scripts/rdp_quality_eval.ts +217 -19
  219. package/src/scripts/release_mix_taxonomy.json +3 -2
  220. package/src/scripts/report_host_injection_effect.ts +189 -0
  221. package/src/scripts/report_layer_overlap.ts +41 -0
  222. package/src/scripts/report_skill_menu_census.ts +341 -0
  223. package/src/scripts/report_standing_payload_by_host.ts +650 -0
  224. package/src/scripts/schemas/agent-settings.schema.json +7 -1
  225. package/src/scripts/session_memory_index.ts +81 -8
  226. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  227. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  228. package/src/server/schemas/settings.ts +23 -4
  229. package/src/vendor/grammars/README.md +86 -0
  230. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  231. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  232. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  233. package/src/scripts/_lib/carrier_status.ts +0 -118
  234. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  235. package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
@@ -0,0 +1,253 @@
1
+ /**
2
+ * The deterministic continuity-record writer.
3
+ *
4
+ * `road-to-continuity-writer-activation` step 1.2. Until this module existed
5
+ * `agent-config session:recycle` was the ONLY writer of the continuity record
6
+ * in the tree, which is why `verb:session:recycle` counts on the
7
+ * `public_continuity_commands` axis and why an advisory has to tell a human to
8
+ * run it before `/clear`: continuity did not happen unless somebody
9
+ * remembered.
10
+ *
11
+ * Every field here is computed from on-disk state and nothing is asked of a
12
+ * model. That is not a cost optimisation, it is what makes the record a
13
+ * `continuity_record` rather than a `main_session` one: the judgement fields
14
+ * (`decisions`, `failed_approaches`, `successful_approaches`, …) are FORBIDDEN
15
+ * on the variant precisely so a deterministic writer cannot fill one with a
16
+ * guess and a reader cannot be left unable to tell a derived record from a
17
+ * judged one.
18
+ *
19
+ * Two gates, and both are needed:
20
+ *
21
+ * 1. `continuity.auto_record` must be `on`. It ships `off`, so with the
22
+ * shipped default this module is never reached and the tree behaves
23
+ * exactly as it does today.
24
+ * 2. The session must be SUBSTANTIVE by the committed floor
25
+ * (`is_substantive`) and must have a resolvable task. Both come from the
26
+ * concern's own counters and claim state, never from file presence.
27
+ *
28
+ * The task gate is the sharper of the two, and its bound is stated rather than
29
+ * hidden: a record needs at least one `acceptance_criteria` entry, and the only
30
+ * deterministic source of one in this tree is the roadmap a session has
31
+ * claimed. A session with no claim therefore leaves no record — and the
32
+ * alternative, writing a placeholder acceptance criterion, would assert a
33
+ * definition of "done" nobody set, which is the exact failure the variant's
34
+ * forbidden-key set exists to prevent.
35
+ */
36
+ import * as fs from 'node:fs';
37
+
38
+ import { load_agent_settings } from './agent_settings.js';
39
+ import { resolvePredecessor } from './recycle_envelope_paths.js';
40
+ import { CHECKBOX_LINE, phaseLines } from './roadmap_checkboxes.js';
41
+ import { readHead, roadmapPath } from './run_checkpoint.js';
42
+ import { is_substantive, type StoredEolCounters } from './session_eol.js';
43
+ import { CAPSULE_SCHEMA_VERSION, MAX_ENTRIES, MAX_LINE_CHARS } from './subagent_capsule.js';
44
+
45
+ /** Settings key that arms this writer. Ships `off`. */
46
+ export const AUTO_RECORD_KEY = 'continuity.auto_record';
47
+
48
+ /**
49
+ * Read `continuity.auto_record` from the merged settings cascade.
50
+ *
51
+ * The cascade merges the shipped template, so a workspace with no settings file
52
+ * resolves `off` through the template rather than through the trailing
53
+ * `return false` — which is why that line is defence against an unreadable
54
+ * cascade and not the path a default install takes. Both directions are pinned
55
+ * by fixtures, because "absent" and "off" arriving by different routes is
56
+ * exactly the kind of thing a reader assumes and a test has to establish.
57
+ */
58
+ export function auto_record_enabled(root: string): boolean {
59
+ try {
60
+ const settings = load_agent_settings({ cwd: root });
61
+ const section = settings['continuity'];
62
+ if (section && typeof section === 'object' && !Array.isArray(section)) {
63
+ const v = (section as Record<string, unknown>)['auto_record'];
64
+ // YAML 1.1 parses a bare `on` as boolean true — accept both.
65
+ return v === 'on' || v === true;
66
+ }
67
+ } catch {
68
+ // fail-closed: unreadable settings leave the writer disarmed
69
+ }
70
+ return false;
71
+ }
72
+
73
+ /** Settings key that governs run-checkpoint production. Ships `on`. */
74
+ export const RUN_CHECKPOINTS_KEY = 'continuity.run_checkpoints';
75
+
76
+ /**
77
+ * Read `continuity.run_checkpoints` from the merged settings cascade.
78
+ *
79
+ * Fails OPEN, unlike its sibling above, and the asymmetry is the point: this
80
+ * switch governs behaviour the tree already had, so an unreadable cascade must
81
+ * leave it running. Its sibling arms something new, so an unreadable cascade
82
+ * must leave that disarmed. A single helper with one polarity would have been
83
+ * wrong for one of the two.
84
+ */
85
+ export function run_checkpoints_enabled(root: string): boolean {
86
+ try {
87
+ const settings = load_agent_settings({ cwd: root });
88
+ const section = settings['continuity'];
89
+ if (section && typeof section === 'object' && !Array.isArray(section)) {
90
+ const v = (section as Record<string, unknown>)['run_checkpoints'];
91
+ if (v === 'off' || v === false) return false;
92
+ }
93
+ } catch {
94
+ // fail-open: an unreadable cascade never silently removes a recovery aid
95
+ }
96
+ return true;
97
+ }
98
+
99
+ /** Why no record was built, when none was. Always a stated reason. */
100
+ export interface WriterDecision {
101
+ record: Record<string, unknown> | null;
102
+ reason: string;
103
+ }
104
+
105
+ /** How many `remaining` / `acceptance_criteria` lines a record carries. */
106
+ const LIST_CAP = 12;
107
+
108
+ function _clip(line: string): string {
109
+ const one = line.replace(/\s+/g, ' ').trim();
110
+ return one.length <= MAX_LINE_CHARS ? one : `${one.slice(0, MAX_LINE_CHARS - 1)}…`;
111
+ }
112
+
113
+ /**
114
+ * Open, closed and parked step counts plus the open step bodies.
115
+ *
116
+ * Deliberately reuses `phaseLines` + `CHECKBOX_LINE` rather than a fourth
117
+ * regex: a writer that disagreed with the dashboard about whether a roadmap is
118
+ * finished would put that disagreement into the record a successor resumes
119
+ * from.
120
+ */
121
+ export function roadmapShape(text: string): { done: number; total: number; open: string[] } {
122
+ let done = 0;
123
+ let total = 0;
124
+ const open: string[] = [];
125
+ for (const raw of phaseLines(text)) {
126
+ const m = CHECKBOX_LINE.exec(raw);
127
+ if (m === null) continue;
128
+ total += 1;
129
+ const mark = m[1];
130
+ if (mark === 'x' || mark === 'X') {
131
+ done += 1;
132
+ continue;
133
+ }
134
+ if (mark === '~' || mark === '-') continue;
135
+ if (open.length < LIST_CAP) {
136
+ open.push(_clip((m[2] ?? '').replace(/\*\*/g, '')));
137
+ }
138
+ }
139
+ return { done, total, open };
140
+ }
141
+
142
+ /**
143
+ * Checkbox bodies under an `## Acceptance Criteria` heading.
144
+ *
145
+ * `phaseLines` deliberately excludes this section — an autonomous run must not
146
+ * mistake an acceptance criterion for its next step — so it is read separately
147
+ * here, which is the one place the two readings legitimately differ.
148
+ */
149
+ export function acceptanceCriteria(text: string): string[] {
150
+ const lines = text.split('\n');
151
+ const out: string[] = [];
152
+ let inside = false;
153
+ for (const raw of lines) {
154
+ const heading = /^(#{2,3})[ \t]+(.*?)[ \t]*$/.exec(raw);
155
+ if (heading !== null) {
156
+ inside = /^Acceptance[ \t]+Criteria\b/i.test(heading[2] ?? '');
157
+ continue;
158
+ }
159
+ if (!inside) continue;
160
+ const m = CHECKBOX_LINE.exec(raw);
161
+ if (m === null) continue;
162
+ if (out.length < LIST_CAP) {
163
+ out.push(_clip((m[2] ?? '').replace(/\*\*/g, '')));
164
+ }
165
+ }
166
+ return out;
167
+ }
168
+
169
+ export interface BuildInput {
170
+ /** Workspace root — the record's `workspace`, and the root every read is relative to. */
171
+ root: string;
172
+ /** Raw session id, as the record paths and the claim state key on it. */
173
+ sessionId: string;
174
+ /** The roadmap slug this session claimed, or `null` when it claimed none. */
175
+ slug: string | null;
176
+ /** The concern's own counters for this session. */
177
+ counters: StoredEolCounters | null;
178
+ /** Injected for determinism in tests. */
179
+ now?: Date;
180
+ }
181
+
182
+ /**
183
+ * Build the record, or say why there is none.
184
+ *
185
+ * Never throws and never shells out: this runs on the Stop path, where a
186
+ * subprocess is a latency cost paid on every turn's end and an exception is
187
+ * worse than a missing record. `head` is read from `.git` directly (worktree
188
+ * aware) rather than through `git`, and the richer anchor fields
189
+ * `session:recycle` collects with `git status` are deliberately omitted — the
190
+ * trade is one drift line for a Stop path that spawns nothing, and it is stated
191
+ * in `docs/contracts/continuity-record-slot.md` rather than left for a reader
192
+ * to discover by diffing two producers.
193
+ */
194
+ export function buildContinuityRecord(input: BuildInput): WriterDecision {
195
+ const { root, sessionId, slug, counters } = input;
196
+ const now = input.now ?? new Date();
197
+
198
+ if (!is_substantive(counters)) {
199
+ return {
200
+ record: null,
201
+ reason: 'no record — the session is not substantive by the committed floor',
202
+ };
203
+ }
204
+ if (slug === null || slug.trim() === '') {
205
+ return {
206
+ record: null,
207
+ reason: 'no record — no claimed roadmap, so no deterministic acceptance criterion exists',
208
+ };
209
+ }
210
+
211
+ let text: string;
212
+ try {
213
+ text = fs.readFileSync(roadmapPath(root, slug), 'utf-8');
214
+ } catch {
215
+ return { record: null, reason: `no record — the claimed roadmap ${slug} is not readable` };
216
+ }
217
+
218
+ const shape = roadmapShape(text);
219
+ if (shape.total === 0) {
220
+ return { record: null, reason: `no record — ${slug} carries no phase checkboxes to count` };
221
+ }
222
+
223
+ const declared = acceptanceCriteria(text);
224
+ const criteria =
225
+ declared.length > 0
226
+ ? declared
227
+ : // A roadmap with no declared criteria still has one definitional
228
+ // completion condition, and it is derived from the artefact class
229
+ // rather than judged: every step closed. Anything richer would be
230
+ // a definition of done nobody wrote.
231
+ [`every phase step in agents/roadmaps/${slug}.md is closed`];
232
+
233
+ const record: Record<string, unknown> = {
234
+ capsule_version: CAPSULE_SCHEMA_VERSION,
235
+ variant: 'continuity_record',
236
+ summary: _clip(`${slug}: ${shape.done} of ${shape.total} steps closed`),
237
+ task: _clip(slug),
238
+ workspace: root,
239
+ written_at: now.toISOString(),
240
+ acceptance_criteria: criteria.slice(0, MAX_ENTRIES),
241
+ remaining: shape.open.slice(0, MAX_ENTRIES),
242
+ predecessor: resolvePredecessor(root, sessionId),
243
+ artifact_paths: [`agents/roadmaps/${slug}.md`],
244
+ };
245
+ if (sessionId.trim() !== '') {
246
+ record['session_id'] = sessionId.trim();
247
+ }
248
+ const head = readHead(root);
249
+ if (head !== null) {
250
+ record['head'] = head;
251
+ }
252
+ return { record, reason: `record built for ${slug} (${shape.done}/${shape.total} closed)` };
253
+ }
@@ -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,
@@ -110,3 +110,88 @@ export function leanProjectionModeRaw(root: string): string {
110
110
  }
111
111
  return '';
112
112
  }
113
+
114
+ /**
115
+ * Raw `lean_projection.hosts` list out of `.agent-settings.yml`, or `[]`.
116
+ *
117
+ * Same indentation-shaped discipline as `leanProjectionModeRaw` above and for
118
+ * the same reason — a hook must never fail a tool call because a YAML parser
119
+ * could not load — extended to the three list shapes a human writes:
120
+ *
121
+ * lean_projection:
122
+ * hosts: [claude-code] # inline
123
+ * lean_projection:
124
+ * hosts: # block, indented under the key
125
+ * - claude-code
126
+ * lean_projection:
127
+ * hosts: # block, at the key's own indent
128
+ * - claude-code
129
+ *
130
+ * All three are legal YAML and `js-yaml` loads them identically; the third was
131
+ * read as `[]` until 2026-09-08 (R2 finding 2) and is covered by three fixtures
132
+ * in `tests/scripts/lean_projection_hosts.test.ts`.
133
+ *
134
+ * Interpretation is NOT done here. `[]` means "nothing was written", and
135
+ * `_lib/lean_projection_mode.ts::resolveLeanProjectionHosts` decides that this
136
+ * means the default rather than "no host" — keeping that decision in one place
137
+ * is what stops the projector and the delivery concern from disagreeing, which
138
+ * is the whole reason the mode reader is split this way.
139
+ */
140
+ export function leanProjectionHostsRaw(root: string): string[] {
141
+ const file = path.join(root, SETTINGS_FILE);
142
+ let text: string;
143
+ try {
144
+ if (!fs.statSync(file).isFile()) return [];
145
+ text = fs.readFileSync(file, 'utf-8');
146
+ } catch {
147
+ return [];
148
+ }
149
+ const strip = (s: string): string => s.trim().replace(/^["']|["']$/g, '');
150
+ let inSection = false;
151
+ let inBlock = false;
152
+ let blockIndent = -1;
153
+ const out: string[] = [];
154
+ for (const raw of text.split(/\r\n|\r|\n/)) {
155
+ const line = raw.replace(/\s+$/, '');
156
+ if (!line || line.replace(/^\s+/, '').startsWith('#')) continue;
157
+ if (!(line.startsWith(' ') || line.startsWith('\t'))) {
158
+ inSection = /^lean_projection\s*:\s*$/.test(line);
159
+ inBlock = false;
160
+ continue;
161
+ }
162
+ if (!inSection) continue;
163
+ const indent = (/^\s*/.exec(line) as RegExpExecArray)[0].length;
164
+ if (inBlock) {
165
+ const item = /^\s*-\s*(\S.*)$/.exec(line);
166
+ // `>=`, not `>`: YAML lets a block sequence sit at its KEY's own
167
+ // indent, and `js-yaml` — the parser `condense` uses on this same
168
+ // file — loads that shape identically to the deeper-indented one.
169
+ // With `>` the same-indent form parsed as `[]`, which
170
+ // `resolveLeanProjectionHosts` reads as "nothing was written" and
171
+ // answers with the default `[claude-code]` while the projector
172
+ // thins whatever the file actually says. Two readers disagreeing
173
+ // about the delivery-host set is the one failure this split exists
174
+ // to prevent (R2 finding 2). A SHALLOWER item still ends the block:
175
+ // it belongs to a different mapping level, never to `hosts`.
176
+ if (item !== null && indent >= blockIndent) {
177
+ const v = strip(item[1] ?? '');
178
+ if (v !== '') out.push(v);
179
+ continue;
180
+ }
181
+ inBlock = false;
182
+ }
183
+ const inline = /^\s+hosts\s*:\s*\[([^\]]*)\]\s*$/.exec(line);
184
+ if (inline !== null) {
185
+ for (const part of (inline[1] ?? '').split(',')) {
186
+ const v = strip(part);
187
+ if (v !== '') out.push(v);
188
+ }
189
+ continue;
190
+ }
191
+ if (/^\s+hosts\s*:\s*$/.test(line)) {
192
+ inBlock = true;
193
+ blockIndent = indent;
194
+ }
195
+ }
196
+ return out;
197
+ }
@@ -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);