@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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The single-delivery partition predicate — one artefact, one layer.
3
3
  *
4
- * ## What it decides
4
+ * **What it decides.**
5
5
  *
6
6
  * Two layers deliver agent artefacts: a machine-local project layer at
7
7
  * `<repo>/.claude/` (gitignored, 0 tracked files, rewritten by every
@@ -11,16 +11,23 @@
11
11
  * 203,873 tokens against a 110,000 cap (185.3 %).
12
12
  *
13
13
  * ADR-236 partitions them: an artefact that exists ONLY for this package stays
14
- * in the project layer; everything else is delivered only globally. That takes
15
- * `<repo>/.claude/` from 111 rules and 338 skills to **16 rules and zero
16
- * skills**.
14
+ * in the project layer; everything else is delivered only globally.
17
15
  *
18
- * ## Why the predicate is fail-safe and never fails the build
16
+ * The figure this note carried — "16 rules and zero skills" — was the 2026-08-19
17
+ * projection of the design. Measured 2026-09-07 on the amended implementation:
18
+ * **13 rules** (15 package-only, 2 of them byte-identically deduped at user
19
+ * scope) and **zero skills**, plus 49 flat-command wrappers, which are a
20
+ * different family and are withheld per name like everything else.
21
+ *
22
+ * **Why the predicate is fail-safe and never fails the build.**
19
23
  *
20
24
  * The partition is a removal, so the build loses its repair path: it can no
21
25
  * longer heal a stale global layer by regenerating, because it stops writing
22
- * those files. Every uncertainty therefore resolves to `standalone/full` — the
23
- * pre-partition behaviour — and **never** to a refusal:
26
+ * those files. **How that fail-safe is expressed changed on 2026-09-07** and the
27
+ * sentence below is the OLD form, kept because the constraint it records still
28
+ * binds: uncertainty used to resolve to a repo-wide `standalone/full`, a mode
29
+ * this file no longer has. It now resolves PER ARTEFACT — an unreadable host
30
+ * layer withholds nothing — and still never to a refusal:
24
31
  *
25
32
  * `.github/workflows/consistency.yml:169` runs `task generate-tools` on a fresh
26
33
  * checkout where, by that workflow's own comment at `:172-174`, the host rule
@@ -30,22 +37,68 @@
30
37
  * exists to prevent; breaking the Consistency pipeline is the other. Full
31
38
  * projection is the only branch that does neither.
32
39
  *
33
- * ## Why content and not a version number
40
+ * **Why content and not a version number.**
34
41
  *
35
42
  * Rationale and the 153-skill measurement that decided it: see
36
43
  * `hostLayerFingerprint.ts`. Version equality is checked too, but as a cheap
37
44
  * pre-filter — it is necessary, never sufficient.
38
45
  *
39
- * ## Contract
46
+ * **Contract.**
40
47
  *
41
- * Side-effect-free, no I/O of its own (callers supply the facts), no CLI entry,
42
- * no `process.exit`. Ships inside the consumer installer bundle, same
48
+ * `verifyHostLayer` itself is side-effect-free and takes its facts from the
49
+ * caller. The MODULE is not: `resolveHostLayerVerdict` reads the filesystem and
50
+ * the lockfile, and since 2026-09-07 it re-exports `keepInProjectLayer` /
51
+ * `claudeLayerHolds`, which read directories. The earlier blanket "no I/O of its
52
+ * own" was true of the pure predicate and false of the file. No CLI entry, no
53
+ * `process.exit`. Ships inside the consumer installer bundle, same
43
54
  * constraint as `ruleInScope.ts`.
44
55
  */
45
56
 
46
57
  import * as fs from 'node:fs';
47
58
  import * as os from 'node:os';
48
59
 
60
+ import { claudeLayerHolds, keepInProjectLayer } from './claudeLayerCarriage.js';
61
+
62
+ /**
63
+ * **`emittedWrapperSlugs` — why the skill sweep's protected set narrowed.**
64
+ *
65
+ * `generate_claude_skills` sweeps `.claude/skills/` of anything absent from the
66
+ * set it is about to write, and it spares command-wrapper slugs so the command
67
+ * emitter — which runs afterwards into the same directory — does not lose
68
+ * entries it is about to create. That protected set used to be EVERY command
69
+ * slug, which was safe only while skills were projected in full: a slug that is
70
+ * also a skill name was protected twice over.
71
+ *
72
+ * Once `~/.claude/skills` began withholding by name (2026-09-07) the
73
+ * over-protection became a leak — precisely the one
74
+ * `tests/scripts/single_delivery_emission.test.ts` exists for. A command-named
75
+ * skill survives in `.claude/skills/` while both counters read zero, because the
76
+ * skill sweep spares it and the command prune skips symlinks.
77
+ *
78
+ * So the set mirrors the emitter's own skips — **all of them**, which a neutral
79
+ * review found it did not on 2026-09-07: the note said "two" and named two while
80
+ * the emitter had three, so up to 8 builtin-reserved slugs (`bug`, `review`,
81
+ * `worktree`, `agents`, `context`, `cost`, `memory`, `skills`) were protected
82
+ * from a sweep that should reach them. Benign in the shipped pipeline order,
83
+ * because the command emitter's own stale-dir sweep cleans them afterwards — but
84
+ * the invariant the note asserted did not hold, and it reopens for any run that
85
+ * calls `generate_claude_skills` without `generate_claude_commands`.
86
+ *
87
+ * The four skips: a slug that is also a skill name · every CLUSTERED command
88
+ * (those reach the host as `/cluster:sub` and need no wrapper) · a Claude Code
89
+ * built-in name · a slug `~/.claude/skills` already carries.
90
+ * `condense.ts::_emitted_wrapper_slugs` is that set.
91
+ *
92
+ * **The re-export below.**
93
+ *
94
+ * `keepInProjectLayer` is re-exported so `condense.ts` reaches both halves of
95
+ * the partition through ONE import line. Not stylistic: that file sits ~1,200 lines past the 1,500-line
96
+ * source ceiling, where `check_source_size_budget` counts every added line as
97
+ * one unit of excess and its test asserts `baseline == live` — so a second
98
+ * import statement there is a blocking gate failure, while the same line here
99
+ * (535 lines, under the cap) costs nothing.
100
+ */
101
+ export { claudeLayerHolds, keepInProjectLayer } from './claudeLayerCarriage.js';
49
102
  import { parseFrontmatter } from './ruleInScope.js';
50
103
  import { fingerprintLayers, hostLayerInputs } from './hostLayerFingerprint.js';
51
104
  import {
@@ -57,12 +110,25 @@ import {
57
110
  /** The workspace id that marks an artefact as existing only for this package. */
58
111
  export const MAINTAINER_WORKSPACE = 'agent-config-maintainer';
59
112
 
60
- /** Delivery mode the build selected, and prints. */
61
- export type PartitionMode = 'standalone/full' | 'dual-layer/partitioned';
62
-
63
- export interface PartitionVerdict {
64
- readonly mode: PartitionMode;
65
- /** One clause, printable, saying WHY this mode was selected. */
113
+ /**
114
+ * Is the recorded install verified against the host layer on disk?
115
+ *
116
+ * This used to be a `PartitionMode` — `'standalone/full' | 'dual-layer/partitioned'` —
117
+ * and it used to DECIDE what the project layer got written. It no longer does
118
+ * (owner decision, 2026-09-07): the project layer withholds per artefact on the
119
+ * host layer's own contents, via `claudeLayerCarriage` and
120
+ * `globalRuleLayers.hostLayerCarries`. What survives here is the diagnostic —
121
+ * whether the layer a run is withholding against is the one this checkout's
122
+ * installer stamped.
123
+ *
124
+ * The type was renamed rather than kept, because a name that says "delivery mode"
125
+ * while deciding nothing about delivery is the half-alive machinery this change
126
+ * exists to remove.
127
+ */
128
+ export interface HostLayerVerdict {
129
+ /** True only when version AND content fingerprint both match the record. */
130
+ readonly verified: boolean;
131
+ /** One clause, printable, saying WHY — including how to fix a `false`. */
66
132
  readonly reason: string;
67
133
  }
68
134
 
@@ -72,7 +138,7 @@ export interface LockfileFacts {
72
138
  readonly host_layer_fingerprint?: string | undefined;
73
139
  }
74
140
 
75
- export interface PartitionInputs {
141
+ export interface HostLayerInputs {
76
142
  /** `package.json` version of the checkout being built. */
77
143
  readonly projectVersion: string;
78
144
  /** Parsed `installed.lock`, or `null` when no global install is recorded. */
@@ -88,31 +154,36 @@ export interface PartitionInputs {
88
154
  }
89
155
 
90
156
  /**
91
- * Select the delivery mode. Total function: always returns a verdict, never
92
- * throws, never refuses.
157
+ * Verify the host layer against the install record. Total function: always
158
+ * returns a verdict, never throws, never refuses.
159
+ *
160
+ * The order of the guards is cheapest-and-most-decisive first, so the
161
+ * fingerprint is computed only when everything else already agrees.
93
162
  *
94
- * The order of the guards is the fail-safe order — cheapest and most decisive
95
- * first, so the fingerprint is computed only when everything else already
96
- * agrees.
163
+ * **A `false` no longer changes what gets written.** It changes what the run
164
+ * SAYS — see `reportHostLayerVerdict`, whose warning names the remedy. The
165
+ * withhold decision moved to per-artefact evidence, where an unverified install
166
+ * that nonetheless carries an artefact is not a reason to deliver that artefact
167
+ * twice.
97
168
  */
98
- export function partitionVerdict(inputs: PartitionInputs): PartitionVerdict {
169
+ export function verifyHostLayer(inputs: HostLayerInputs): HostLayerVerdict {
99
170
  if (!inputs.hostLayerPresent) {
100
171
  return {
101
- mode: 'standalone/full',
172
+ verified: false,
102
173
  reason: 'no host-global layer on this machine',
103
174
  };
104
175
  }
105
176
  const lock = inputs.lockfile;
106
177
  if (lock === null) {
107
178
  return {
108
- mode: 'standalone/full',
179
+ verified: false,
109
180
  reason: 'host layer present but no install record (installed.lock absent)',
110
181
  };
111
182
  }
112
183
  const recorded = lock.agent_config_version;
113
184
  if (!recorded) {
114
185
  return {
115
- mode: 'standalone/full',
186
+ verified: false,
116
187
  reason: 'install record carries no version',
117
188
  };
118
189
  }
@@ -121,15 +192,15 @@ export function partitionVerdict(inputs: PartitionInputs): PartitionVerdict {
121
192
  // checkout still expects, and ordering does not establish substitutability.
122
193
  if (recorded !== inputs.projectVersion) {
123
194
  return {
124
- mode: 'standalone/full',
195
+ verified: false,
125
196
  reason: `version mismatch (installed ${recorded}, building ${inputs.projectVersion})`,
126
197
  };
127
198
  }
128
199
  const installedFp = lock.host_layer_fingerprint;
129
200
  if (!installedFp) {
130
201
  return {
131
- mode: 'standalone/full',
132
- reason: 'install predates host-layer fingerprinting — re-run `agent-config install` to enable the partition',
202
+ verified: false,
203
+ reason: 'install predates host-layer fingerprinting — re-run `agent-config install` to record one',
133
204
  };
134
205
  }
135
206
  let expected: string;
@@ -137,18 +208,18 @@ export function partitionVerdict(inputs: PartitionInputs): PartitionVerdict {
137
208
  expected = inputs.expectedFingerprint();
138
209
  } catch {
139
210
  return {
140
- mode: 'standalone/full',
211
+ verified: false,
141
212
  reason: 'could not compute the expected host-layer fingerprint',
142
213
  };
143
214
  }
144
215
  if (installedFp !== expected) {
145
216
  return {
146
- mode: 'standalone/full',
217
+ verified: false,
147
218
  reason: 'host-layer content differs from this checkout — re-run `agent-config install`',
148
219
  };
149
220
  }
150
221
  return {
151
- mode: 'dual-layer/partitioned',
222
+ verified: true,
152
223
  reason: `host layer verified at ${recorded} (fingerprint ${installedFp.slice(0, 12)})`,
153
224
  };
154
225
  }
@@ -167,7 +238,7 @@ export function partitionVerdict(inputs: PartitionInputs): PartitionVerdict {
167
238
  * the project layer. Both defaults resolve toward "the artefact is generally
168
239
  * useful"; only one of them is about withholding.
169
240
  *
170
- * ## The state space, MEASURED rather than enumerated defensively (2026-08-21)
241
+ * **The state space, MEASURED rather than enumerated defensively (2026-08-21).**
171
242
  *
172
243
  * Three of this function's branches — unreadable file, absent `workspaces:`,
173
244
  * empty list — all resolve to `false`, and the closure review asked whether one
@@ -206,71 +277,78 @@ export function isExclusivelyPackageOnly(source_path: string): boolean {
206
277
  return raw.every((w) => String(w) === MAINTAINER_WORKSPACE);
207
278
  }
208
279
 
209
- /** Emitter for the ONE mode line; see the visibility note on the resolver. */
280
+ /** Emitter for the ONE projection line; see the visibility note on the resolver. */
210
281
  export type Announce = (message: string) => void;
211
282
 
212
- let _memo: PartitionVerdict | null = null;
283
+ let _memo: HostLayerVerdict | null = null;
213
284
  let _announce: Announce = (m) => process.stdout.write(`${m}\n`);
214
285
 
215
286
  /**
216
- * Install the emitter used for the mode line. Callers pass a function visible at
217
- * their DEFAULT output level — the first implementation used one that prints only
218
- * at `verbose`, which withheld ~100 rules while saying nothing in a normal run.
287
+ * Install the emitter used for the projection line. Callers pass a function
288
+ * visible at their DEFAULT output level — the first implementation used one that
289
+ * prints only at `verbose`, which withheld ~100 rules while saying nothing in a
290
+ * normal run.
219
291
  */
220
292
  export function setPartitionAnnounce(fn: Announce): void {
221
293
  _announce = fn;
222
294
  }
223
295
 
224
- /** Test seam — drop the memo so one process can exercise both delivery modes. */
225
- export function _resetPartitionVerdictForTest(): void {
296
+ /** Test seam — drop the memo so one process can exercise both verdicts. */
297
+ export function _resetHostLayerVerdictForTest(): void {
226
298
  _memo = null;
227
299
  }
228
300
 
229
301
  /**
230
- * Is the partition active for this generation? Memoised per process, because the
231
- * fingerprint costs ~100 ms and the answer cannot change mid-run.
232
- *
233
- * This is the single entry point the generators call, deliberately: wiring a
234
- * feature into two files that are already past the 1,500-line source ceiling
235
- * costs a ratchet violation per line, so the decision, its fail-safe ordering,
236
- * its printed reason and its memo all live here and the call sites are one line
237
- * each.
238
- */
239
- export function partitionActive(projectRoot: string): boolean {
240
- return resolvePartitionVerdict(projectRoot).mode === 'dual-layer/partitioned';
241
- }
242
-
243
- /**
244
- * Resolve the delivery mode for one generation, and say so once.
245
- *
246
- * Lives here rather than in `condense.ts` for two reasons. The decision belongs
247
- * beside {@link partitionVerdict}, whose fail-safe ordering it depends on; and
248
- * `condense.ts` is 1,500+ lines, where the source-size ratchet counts every
249
- * added line as a violation — so a block that has no reason to live there is a
250
- * cost as well as a misplacement.
251
- *
252
- * **Both council seats (2026-08-20, 2/2) required that generation PRINT the mode
253
- * it selected** rather than partition silently: a withheld artefact nobody
254
- * announced is exactly the under-governance the partition exists to remove. The
255
- * caller supplies `announce`, and the level matters — the first implementation
256
- * used an `info()` that prints only at `verbose`, so it withheld ~100 rules while
257
- * saying nothing in a default run. Pass a function that is visible at the default
258
- * level. Residual, stated: at an explicitly silent output level the line is
259
- * dropped; that is an operator choice and the partition stays fail-safe anyway.
302
+ * Verify the host layer once per generation, and say so once.
303
+ *
304
+ * **What this no longer does.**
305
+ *
306
+ * It used to answer `partitionActive(projectRoot)`, and every generator gated its
307
+ * withhold on that one boolean. **That entry point is deleted** (owner decision,
308
+ * 2026-09-07). The project layer's contents are now decided per artefact, on the
309
+ * host layer's own contents, by `claudeLayerCarriage.keepInProjectLayer` and
310
+ * `globalRuleLayers.hostLayerCarries`. A version number is not evidence about an
311
+ * artefact, and using it as one delivered 261 skills and 29 personas twice per
312
+ * session for as long as an install lagged a release.
313
+ *
314
+ * So what survives is the DIAGNOSTIC. **Both council seats (2026-08-20, 2/2)
315
+ * required that generation print the mode it selected** rather than partition
316
+ * silently, and that requirement is still live — but this function is no longer
317
+ * where it is met, and the first version of this note claimed otherwise.
318
+ *
319
+ * **Corrected 2026-09-07 after a neutral review.** The note read "it still
320
+ * prints". It did not: removing `partitionActive` removed `condense.ts`'s only
321
+ * call into this resolver, so the emitter that file installed became dead and
322
+ * `task generate-tools` withheld ~299 skills and 29 personas while printing
323
+ * nothing about the layer it withheld against. The line is now emitted by
324
+ * `report_layer_overlap`, the step that already runs immediately after the
325
+ * generator in the same chain — see the comment there for why it lives in that
326
+ * file rather than in the 2,700-line generator.
327
+ *
328
+ * **What that line does and does not carry**, since a second review asked: it
329
+ * reports the VERIFICATION state of the layer being withheld against, not a
330
+ * withhold count, and this verdict decides nothing. The counts are the
331
+ * generator's own summary one line above (`skills=N`,
332
+ * `command_skills=N (M withheld …)`). So the council requirement is met by the
333
+ * pair of lines rather than by this one — stated that way instead of claiming
334
+ * the line alone honours it.
335
+ *
336
+ * The caller still supplies `announce`, and the level still matters — the first
337
+ * implementation used an `info()` that prints only at `verbose`. Residual,
338
+ * stated: at an explicitly silent output level the line is dropped.
260
339
  *
261
340
  * The fingerprint compares the host layer against **what the installer recorded
262
- * when it wrote that layer**, not against a re-derivation from source. That
263
- * avoids guessing the installer's byte representation — a mismatch there would
264
- * make the partition permanently unreachable rather than merely inactive — and it
265
- * is the property the partition needs: the omitted artefacts are still present,
266
- * in the form this version's installer left them.
341
+ * when it wrote that layer**, not against a re-derivation from source — the
342
+ * property that keeps a mismatch informative rather than permanent.
267
343
  *
268
344
  * **Known residual:** an installer that crashes mid-write and still reaches the
269
345
  * lockfile would fingerprint its own partial layer, and that fingerprint then
270
346
  * verifies. Ordering narrows the window (the lockfile is written last) without
271
- * closing it; a per-artefact manifest would close it and is not built here.
347
+ * closing it. It now costs less than it did: the withhold no longer trusts this
348
+ * verdict, so a wrongly-verified layer misreports a line rather than authorising
349
+ * a removal.
272
350
  */
273
- export function resolvePartitionVerdict(projectRoot: string): PartitionVerdict {
351
+ export function resolveHostLayerVerdict(projectRoot: string): HostLayerVerdict {
274
352
  if (_memo !== null) {
275
353
  return _memo;
276
354
  }
@@ -286,15 +364,19 @@ export function resolvePartitionVerdict(projectRoot: string): PartitionVerdict {
286
364
  try {
287
365
  lock = read_lockfile();
288
366
  } catch {
289
- lock = null; // unreadable record → fail safe
367
+ lock = null; // unreadable record → report it as unverified
290
368
  }
291
- _memo = partitionVerdict({
369
+ _memo = verifyHostLayer({
292
370
  projectVersion: current_package_version(projectRoot),
293
371
  lockfile: lock,
294
372
  hostLayerPresent: present,
295
373
  expectedFingerprint: () => fingerprintLayers(layers),
296
374
  });
297
- _announce(`projection mode: ${_memo.mode} — ${_memo.reason}`);
375
+ _announce(
376
+ _memo.verified
377
+ ? `projection: project layer carries only what ~/.claude lacks — ${_memo.reason}`
378
+ : `projection: project layer carries only what ~/.claude lacks · ⚠️ host layer UNVERIFIED — ${_memo.reason}`,
379
+ );
298
380
  return _memo;
299
381
  }
300
382
 
@@ -316,7 +398,7 @@ export function resolvePartitionVerdict(projectRoot: string): PartitionVerdict {
316
398
  * errors are not symmetric and this must never prefer the second.
317
399
  *
318
400
  * Lives here rather than inline in `install.ts` for the same reason
319
- * {@link resolvePartitionVerdict} does: it belongs beside the predicate it
401
+ * {@link resolveHostLayerVerdict} does: it belongs beside the predicate it
320
402
  * serves, and `install.ts` is 5,000+ lines where the source-size ratchet counts
321
403
  * every added line.
322
404
  *
@@ -360,29 +442,31 @@ export function stampHostLayerFingerprint(
360
442
  * names**, and neither `check_single_delivery` nor `_lib/layer_overlap_notice`
361
443
  * looked, because `personas` was in neither's `TYPES`.
362
444
  *
363
- * ## Two properties the caller depends on
445
+ * **Three properties the caller depends on.**
364
446
  *
365
- * **Scoped to `.claude/` only.** {@link partitionActive} verifies the CLAUDE host
366
- * layer against `installed.lock`. It says nothing about `~/.cursor`, so
367
- * withholding a cursor persona on the strength of a claude fingerprint would
368
- * deliver it nowhere — the one failure the fail-safe design exists to prevent.
369
- * Every other tool directory keeps the full projection.
447
+ * **Scoped to `.claude/` only.** The evidence is `~/.claude/personas`. It says
448
+ * nothing about `~/.cursor`, so withholding a cursor persona on the strength of
449
+ * a claude directory listing would deliver it nowhere — the one failure the
450
+ * fail-safe design exists to prevent. Every other tool directory keeps the full
451
+ * projection.
370
452
  *
371
- * **Reconciliation is the empty list, not a second code path.** The caller's
453
+ * **Withheld PER NAME, on that directory's own contents** (owner decision,
454
+ * 2026-09-07). This used to gate on `partitionActive` — a version and fingerprint
455
+ * check against `installed.lock` — which meant an install one release behind
456
+ * delivered all 29 personas twice. A persona `~/.claude/personas` actually holds
457
+ * is withheld whatever the lockfile says; one it lacks stays, on its own, without
458
+ * keeping the other 28 duplicates alive with it.
459
+ *
460
+ * **Reconciliation is the shorter list, not a second code path.** The caller's
372
461
  * stale-symlink sweep removes any link whose name is absent from the list it was
373
- * given for that directory, so returning `[]` empties a directory an earlier
374
- * version populated. A gate that only declined to WRITE would leave the existing
462
+ * given for that directory, so a shorter list empties what an earlier version
463
+ * populated. A gate that only declined to WRITE would leave the existing
375
464
  * duplicate standing — a partition that stops new duplication and keeps the old
376
465
  * is not a partition.
377
- *
378
- * Verified before shipping: every one of the 29 project personas is present in
379
- * the global layer (32 there, a strict superset). The partition is a removal and
380
- * has no repair path, so withholding is only safe once the surviving layer is
381
- * known to carry what is withheld.
382
466
  */
383
467
  export function personaPartition(
384
- projectRoot: string,
385
468
  all: readonly string[],
469
+ home?: string,
386
470
  ): {
387
471
  readonly all: readonly string[];
388
472
  listFor: (toolDir: string) => readonly string[];
@@ -398,34 +482,42 @@ export function personaPartition(
398
482
  */
399
483
  countFor: (toolDir: string) => number;
400
484
  } {
401
- const active = partitionActive(projectRoot);
485
+ const claudeList = keepInProjectLayer('personas', all, home);
486
+ const withheld = all.length - claudeList.length;
402
487
  return {
403
488
  all,
404
- listFor: (toolDir) => (personaWithheldFor(toolDir, active) ? [] : all),
405
- note: active ? ' — .claude/ withheld: ADR-236 partition, personas arrive from ~/.claude' : '',
406
- countFor: (toolDir) => (personaWithheldFor(toolDir, active) ? 0 : all.length),
489
+ listFor: (toolDir) => personaListFor(toolDir, all, claudeList),
490
+ note:
491
+ withheld > 0
492
+ ? ` — .claude/ withheld ${String(withheld)}: ADR-236, those personas arrive from ~/.claude`
493
+ : '',
494
+ countFor: (toolDir) => personaListFor(toolDir, all, claudeList).length,
407
495
  };
408
496
  }
409
497
 
410
498
  /**
411
- * The pure half of {@link personaPartition}: withhold iff the partition is active
412
- * AND this is a Claude tool directory.
413
- *
414
- * Split out so the decision is testable in BOTH directions. `personaPartition`
415
- * reads `installed.lock` through a memoized `partitionActive`, so a test over it
416
- * can only assert whatever this machine happens to be — which is a test that
417
- * passes either way and therefore proves nothing. The two properties worth
418
- * pinning are exactly the two this signature exposes: `.claude/` is withheld when
419
- * active, and NOTHING else ever is.
499
+ * The pure half of {@link personaPartition}: a Claude tool directory gets the
500
+ * narrowed list, every other directory gets the full one.
501
+ *
502
+ * Split out so the decision is testable in BOTH directions without reading this
503
+ * machine's `~/.claude`. A test over the un-injected path can only assert
504
+ * whatever the machine happens to be — a test that passes either way and
505
+ * therefore proves nothing. The two properties worth pinning are exactly the two
506
+ * this signature exposes: `.claude/` gets the narrowed list, and NOTHING else
507
+ * ever does.
420
508
  */
421
- export function personaWithheldFor(toolDir: string, active: boolean): boolean {
422
- return active && toolDir.startsWith('.claude/');
509
+ export function personaListFor(
510
+ toolDir: string,
511
+ all: readonly string[],
512
+ claudeList: readonly string[],
513
+ ): readonly string[] {
514
+ return toolDir.startsWith('.claude/') ? claudeList : all;
423
515
  }
424
516
 
425
517
  /**
426
518
  * Does the project layer withhold the colon-form `/cluster:sub` commands?
427
519
  *
428
- * ## The claim this replaces, and the measurement that revised it
520
+ * **The claim this replaces, and the measurement that revised it.**
429
521
  *
430
522
  * `generate_claude_project_commands` was written on the reasoning that "Claude
431
523
  * Code dedupes project and user scope by name, so the two copies of
@@ -447,16 +539,24 @@ export function personaWithheldFor(toolDir: string, active: boolean): boolean {
447
539
  * exists, those 40 symlinks are written, deduped away, and LOSE. They are dead
448
540
  * weight there — not a second listing, and not a reachability guarantee either.
449
541
  *
450
- * ## Why `partitionActive` is the right predicate rather than a new one
542
+ * **Why the predicate is the DIRECTORY, not `installed.lock`.**
451
543
  *
452
- * It is true exactly when a global layer is present and verified against
453
- * `installed.lock`, which is exactly the case where the project copy loses; and
454
- * false on a fresh checkout, an unverified install, or a version mismatch —
455
- * every case where the project copy is the only reachable one. So the
456
- * fail-safe direction the whole module is built around already matches the
457
- * measurement, and withholding needs no separate switch.
544
+ * This used to be `partitionActive` — true only when a global layer was present
545
+ * AND verified against `installed.lock` by version and fingerprint. That gets
546
+ * the measurement above backwards in the one case that matters: the project copy
547
+ * loses whenever the global copy EXISTS, and whether the recorded version is a
548
+ * release behind has no bearing on whether it exists. An install one release old
549
+ * therefore kept writing 40 symlinks the host deduped away — dead weight, chosen
550
+ * on a fact about a version number.
458
551
  *
459
- * ## Honest limits
552
+ * So the evidence is the host directory itself, asked per name (owner decision,
553
+ * 2026-09-07): `~/.claude/commands/<cluster>/<sub>.md` for a clustered command,
554
+ * `~/.claude/commands/<slug>.md` for a flat one. Present → the project copy loses
555
+ * and is withheld. Absent → the project copy is the only reachable one and stays.
556
+ * Unreadable layer → nothing is withheld. The fail-safe direction is unchanged;
557
+ * it is now per artefact rather than per repository.
558
+ *
559
+ * **Honest limits.**
460
560
  *
461
561
  * Self-report, n=1 per condition, one host version, one machine. What is NOT
462
562
  * claimed: that older or newer hosts dedupe the same way, or that precedence is
@@ -468,22 +568,56 @@ export function personaWithheldFor(toolDir: string, active: boolean): boolean {
468
568
  * It cannot. When this predicate returns true the project copy is never written,
469
569
  * so there is no second copy for the host to double-list or for the gate to
470
570
  * count — it would report zero overlap while the assumption underneath had
471
- * failed. Worse, `partitionActive` verifies the INSTALL (version + content
472
- * fingerprint), never the HOST version whose behaviour was measured, so a host
473
- * upgrade changes the premise and moves nothing this code reads.
571
+ * failed. The 2026-09-07 change does not repair that: reading the directory is
572
+ * evidence about PRESENCE, never about how the host resolves two copies of a
573
+ * name, so a host that stopped deduping is still undetected here.
474
574
  *
475
575
  * What actually holds: reachability survives a dedup change either way, because
476
576
  * the global copy is delivered regardless. What is lost is only the project
477
577
  * copy's redundancy, and nothing detects that. A real detector would have to
478
578
  * pin the host version this measurement was taken against and re-probe when it
479
579
  * moves — not built here, and named as absent rather than implied away.
580
+ *
581
+ * **Flat wrappers ARE withheld — corrected 2026-09-07 after a neutral review.**
582
+ *
583
+ * A CLUSTERED command reaches Claude Code as `/cluster:sub` from a `.md` under
584
+ * `commands/`, and `~/.claude/commands/` carries 41 such cluster directories — so
585
+ * the project copy has somewhere to lose to, and this predicate withholds it.
586
+ *
587
+ * A FLAT command reaches the host as a hyphen-named wrapper under `skills/`,
588
+ * because the host does not register flat command FILES (probed ≤ 2.1.204).
589
+ *
590
+ * **The first version of this note then claimed `~/.claude/skills` carries NONE
591
+ * of those wrappers, that "nothing else delivers them", and left
592
+ * `generate_claude_commands` ungated on that basis. All three were wrong.**
593
+ * `install.ts::_apply_claude_flat_command_wrappers`, wired for every
594
+ * `claude-code` deploy, writes `~/.claude/skills/<slug>/SKILL.md` for every
595
+ * VISIBLE flat command and deletes the flat file. The claim was a snapshot of one
596
+ * machine whose last install had not run that pass — 53 flat `.md` files still
597
+ * sat in `~/.claude/commands` — presented as a property of the installer.
598
+ *
599
+ * What it would have cost: the next `agent-config install` writes ~17 wrappers
600
+ * globally while the generator keeps writing all 49 project wrappers, so 17 flat
601
+ * commands arrive TWICE per session — the duplication class ADR-236 exists to
602
+ * remove — and `check_single_delivery` reports a `skills` overlap the same note
603
+ * said could not happen.
604
+ *
605
+ * So the wrapper emitter now applies the same per-name rule as everything else:
606
+ * `claudeLayerHolds('skills', slug)` withholds a wrapper the host layer carries,
607
+ * and keeps one it does not. Fail-safe per artefact, no repo-wide switch.
608
+ *
609
+ * A number in that note was wrong too, and is corrected here rather than left to
610
+ * be quoted: the project layer carries **49** wrappers, not 153. `153` was
611
+ * transplanted from the 153-skill measurement in `hostLayerFingerprint.ts`.
612
+ *
613
+ * @param globalName the host-side name — `<cluster>/<sub>.md` or `<slug>.md`.
480
614
  */
481
- export function commandsWithheld(projectRoot: string): boolean {
482
- return partitionActive(projectRoot);
615
+ export function commandWithheld(globalName: string, home?: string): boolean {
616
+ return claudeLayerHolds('commands', globalName, home);
483
617
  }
484
618
 
485
619
  /**
486
- * ## The caller's early return, flagged by review and kept
620
+ * **The caller's early return, flagged by review and kept.**
487
621
  *
488
622
  * `generate_claude_project_commands` applies this predicate and then returns
489
623
  * early when `src/domains/` is absent — BEFORE its stale-link sweep. So a tree