@event4u/agent-config 14.21.0 → 14.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +198 -0
  5. package/CREDITS.md +37 -0
  6. package/dist/agent-src/commands/analyze/inbox.md +380 -5
  7. package/dist/agent-src/commands/analyze/repo.md +4 -2
  8. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  9. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  10. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  11. package/dist/agent-src/rules/token-efficiency.md +26 -3
  12. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  13. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  14. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  15. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  16. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  17. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  18. package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
  19. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  20. package/dist/agent-src/templates/agent-settings.md +0 -1
  21. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  22. package/dist/cli-delegate/_cli_entry.js +8 -0
  23. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  24. package/dist/cli-delegate/archival_sweep.js +8 -0
  25. package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
  26. package/dist/cli-delegate/check_update_banner.js +923 -0
  27. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  28. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  29. package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-BIXEDQPZ.js} +6 -0
  30. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  31. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  32. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  33. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  34. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
  35. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  36. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  37. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  38. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  39. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  40. package/dist/cli-delegate/cmd_conformance.js +2 -2
  41. package/dist/cli-delegate/cmd_export.js +1 -1
  42. package/dist/cli-delegate/cmd_handoff.js +26 -2
  43. package/dist/cli-delegate/cmd_prune.js +1 -1
  44. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  45. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  46. package/dist/cli-delegate/cmd_sync.js +1 -1
  47. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  48. package/dist/cli-delegate/cmd_validate.js +1 -1
  49. package/dist/cli-delegate/dashboard_mode.js +16 -0
  50. package/dist/cli-delegate/gate_budget.js +24 -0
  51. package/dist/cli-delegate/gate_execute.js +25 -0
  52. package/dist/cli-delegate/guarded_baseline.js +18 -0
  53. package/dist/cli-delegate/resume_probe.js +22 -0
  54. package/dist/cli-delegate/roadmap_gates.js +638 -0
  55. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  56. package/dist/cli-delegate/stub_queue.js +47 -0
  57. package/dist/cli-delegate/stubs_due.js +38 -0
  58. package/dist/cli-delegate/ticket_bundles.js +59 -0
  59. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  60. package/dist/discovery/deprecation-report.md +1 -1
  61. package/dist/discovery/discovery-manifest.json +8 -8
  62. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  63. package/dist/discovery/discovery-manifest.summary.md +1 -1
  64. package/dist/discovery/dormancy-report.md +1 -1
  65. package/dist/discovery/orphan-report.md +1 -1
  66. package/dist/discovery/packs.json +2 -2
  67. package/dist/discovery/trust-report.md +1 -1
  68. package/dist/discovery/workspaces.json +2 -2
  69. package/dist/hooks/dispatch.js +1193 -917
  70. package/dist/install/claudeLayerCarriage.js +237 -0
  71. package/dist/install/claudePathsPlan.js +79 -0
  72. package/dist/install/hostLayerFingerprint.js +1 -1
  73. package/dist/install/install.mjs +6 -0
  74. package/dist/install/partitionEligibility.js +231 -117
  75. package/dist/install/ruleLayerPartition.js +10 -7
  76. package/dist/mcp/registry-manifest.json +2 -2
  77. package/dist/mcp/server.json +2 -2
  78. package/dist/mcp/server.mjs +39219 -38995
  79. package/dist/scripts/condense.js +32 -25
  80. package/docs/MIGRATION.md +49 -1
  81. package/docs/contracts/settings-classes.md +1 -1
  82. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  83. package/package.json +7 -4
  84. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  85. package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  86. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  87. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  88. package/src/agent-src/scripts/stubs_due.ts +4 -18
  89. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  90. package/src/config/continuity-surface.json +509 -0
  91. package/src/config/estate-count-budget.json +1 -1
  92. package/src/config/gate-coverage.yml +29 -8
  93. package/src/config/gate-violation-baselines.json +8 -8
  94. package/src/config/gitignore-block.txt +19 -0
  95. package/src/config/host-injection-effect.json +73 -0
  96. package/src/config/packed-binary-manifest.json +33 -0
  97. package/src/config/preamble-payload-budget.json +112 -105
  98. package/src/config/publish-surface.json +4 -2
  99. package/src/install/claudeLayerCarriage.ts +273 -0
  100. package/src/install/claudePathsPlan.ts +76 -0
  101. package/src/install/hostLayerFingerprint.ts +1 -1
  102. package/src/install/partitionEligibility.ts +261 -127
  103. package/src/install/ruleLayerPartition.ts +19 -23
  104. package/src/scripts/_dispatch.bash +77 -27
  105. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  106. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  107. package/src/scripts/_lib/continuity_surface.ts +296 -0
  108. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  109. package/src/scripts/_lib/hook_settings.ts +1 -1
  110. package/src/scripts/_lib/injection_effect.ts +151 -0
  111. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  112. package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
  113. package/src/scripts/_lib/pin_resolver.ts +13 -0
  114. package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
  115. package/src/scripts/_lib/retired_status.ts +74 -0
  116. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  117. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  118. package/src/scripts/ai_council/api_transport.ts +5 -0
  119. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  120. package/src/scripts/ai_council/prompts.ts +9 -0
  121. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  122. package/src/scripts/capture_skill_catalogue.ts +24 -11
  123. package/src/scripts/check_candidate_lines.ts +499 -0
  124. package/src/scripts/check_continuity_surface.ts +475 -0
  125. package/src/scripts/check_dependency_floors.ts +13 -6
  126. package/src/scripts/check_estate_count.ts +40 -79
  127. package/src/scripts/check_pack_size.ts +262 -23
  128. package/src/scripts/check_roadmap_trackable.ts +19 -16
  129. package/src/scripts/check_rule_layer_partition.ts +54 -34
  130. package/src/scripts/check_single_delivery.ts +97 -31
  131. package/src/scripts/check_update_banner.ts +13 -0
  132. package/src/scripts/code_graph/build.ts +138 -15
  133. package/src/scripts/code_graph/cli.ts +76 -8
  134. package/src/scripts/code_graph/extract.ts +11 -0
  135. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  136. package/src/scripts/code_graph/loader.ts +88 -14
  137. package/src/scripts/code_graph/query.ts +121 -11
  138. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  139. package/src/scripts/code_graph/sqlite_store.ts +247 -6
  140. package/src/scripts/code_graph/types.ts +66 -1
  141. package/src/scripts/code_graph/validate.ts +23 -0
  142. package/src/scripts/condense.ts +34 -22
  143. package/src/scripts/corpus_manifest.ts +5 -5
  144. package/src/scripts/council_cli.ts +11 -12
  145. package/src/scripts/count_intent_disagreement.ts +426 -0
  146. package/src/scripts/handoff_context_hook.ts +79 -0
  147. package/src/scripts/hook_effect_doctor.ts +82 -1
  148. package/src/scripts/hook_manifest.json +1 -1
  149. package/src/scripts/hook_manifest.yaml +64 -12
  150. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  151. package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
  152. package/src/scripts/hooks/concern_registry.ts +4 -2
  153. package/src/scripts/hooks/host_semantics.ts +1 -1
  154. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  155. package/src/scripts/hooks/rule_inject_hook.ts +1 -1
  156. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  157. package/src/scripts/inbox_source_census.ts +322 -0
  158. package/src/scripts/install-hooks.sh +82 -0
  159. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  160. package/src/scripts/lint_plan_risk_register.ts +13 -17
  161. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  162. package/src/scripts/mcp_server/tools.ts +2 -56
  163. package/src/scripts/media/lib/adapter-contract.md +7 -2
  164. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  165. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  166. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  167. package/src/scripts/rdp_candidates_delta.ts +276 -0
  168. package/src/scripts/rdp_quality_eval.ts +217 -19
  169. package/src/scripts/release_mix_taxonomy.json +3 -2
  170. package/src/scripts/report_host_injection_effect.ts +189 -0
  171. package/src/scripts/report_layer_overlap.ts +41 -0
  172. package/src/scripts/report_skill_menu_census.ts +341 -0
  173. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  174. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  175. package/src/vendor/grammars/README.md +61 -0
  176. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  177. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  178. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  179. package/src/scripts/_lib/carrier_status.ts +0 -118
  180. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  181. package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Does the CLAUDE host-global layer already carry a given artefact?
3
+ *
4
+ * **Why this module exists — the veto it replaces.**
5
+ *
6
+ * ADR-236 partitions artefacts between `~/.claude/**` and `<repo>/.claude/**`.
7
+ * Its first implementation asked ONE question for the whole run: does
8
+ * `installed.lock` record the same version as this checkout, and does a content
9
+ * fingerprint of the host layer still match what the installer stamped? Any
10
+ * answer but yes fell back to the full projection.
11
+ *
12
+ * That veto is repo-wide, and it fires on facts that have nothing to do with any
13
+ * individual artefact. Measured 2026-09-07 on the maintainer machine, `installed.lock`
14
+ * at 14.19.0 against a 14.21.0 checkout:
15
+ *
16
+ * ```
17
+ * rules global 104 · project 15 · BOTH 0 ← already correct
18
+ * skills global 307 · project 414 · BOTH 261 ← the veto's cost
19
+ * personas global 32 · project 29 · BOTH 29
20
+ * commands global 94 · project 0 · BOTH 0
21
+ * ```
22
+ *
23
+ * The rules row is correct because rules are partitioned by
24
+ * {@link hostLayerCarries} — per host directory, on that directory's own
25
+ * contents — while skills, personas and commands hung off the version veto and
26
+ * were therefore delivered twice, in every session, for as long as the install
27
+ * lagged a release. A version number is not evidence about an artefact.
28
+ *
29
+ * This module supplies the evidence the other three families were missing, in
30
+ * the shape rules already use: read the host directory, and answer per NAME.
31
+ *
32
+ * **Why per-name rather than per-family.**
33
+ *
34
+ * A partition is a removal and has no repair path — withholding a name the
35
+ * surviving layer does not hold deletes it from every session. Asking per name
36
+ * makes the fail-safe granular: an artefact the host layer lacks stays in the
37
+ * project layer on its own, and one absent name no longer keeps 300 duplicates
38
+ * alive with it. That is the property the repo-wide veto could not express.
39
+ *
40
+ * **Honest limits.**
41
+ *
42
+ * Presence of a NAME, never equality of CONTENT — and never PROVENANCE either.
43
+ * Two consequences, the second added 2026-09-07 after a neutral review named it.
44
+ *
45
+ * **Staleness.** A stale `~/.claude/skills/foo` satisfies this check, so an
46
+ * install that lags a release delivers that release's older copy of `foo` rather
47
+ * than two copies. That is the trade the
48
+ * owner chose on 2026-09-07, stated rather than implied: the remedy for a stale
49
+ * global layer is `agent-config install`, and {@link verifyHostLayer} still
50
+ * reports the staleness so a run says so out loud.
51
+ *
52
+ * **Shared namespace.** `~/.claude/skills` is not this package's directory. On
53
+ * the machine this was written, 307 entries = 299 package skills + 8 Cloudflare
54
+ * PLUGIN skills. No name collides today, but the mechanism needs no collision to
55
+ * be real: a plugin — or a hand-made `~/.claude/skills/<name>` — whose name
56
+ * matches a package skill makes this function withhold the package's project-layer
57
+ * copy, and the session then loads the foreign body under the package's skill
58
+ * name. The withhold is a removal with no repair path, which is what makes it
59
+ * worth naming rather than filing.
60
+ *
61
+ * **A NEIGHBOURING defect that WAS fixed, 2026-09-07.** This paragraph claimed
62
+ * "the skill is still delivered, under the right name, from one layer" — and that
63
+ * was false while `_readLayer` returned every directory name without checking
64
+ * that `SKILL.md` resolved: an empty `~/.claude/skills/<name>/` withheld the
65
+ * project copy and the artefact came from neither layer. A name now counts as
66
+ * carried only when the artefact behind it resolves, so the sentence above is
67
+ * true as written.
68
+ *
69
+ * NOT FIXED here, deliberately: distinguishing a package artefact from a foreign
70
+ * one needs a provenance marker the installer does not write today, and inventing
71
+ * one in this module would be a guess dressed as a check. What IS true is that
72
+ * the failure is loud in the direction that matters — the skill is still
73
+ * delivered, under the right name, from one layer — and silent only about whose
74
+ * body it is.
75
+ *
76
+ * Contract, same as its siblings in this directory: no `process.exit`, no CLI
77
+ * entry, node builtins only — it ships inside the consumer installer bundle.
78
+ */
79
+
80
+ import * as fs from 'node:fs';
81
+ import * as os from 'node:os';
82
+ import * as path from 'node:path';
83
+
84
+ import type { CarriesVerdict } from './globalRuleLayers.js';
85
+
86
+ /**
87
+ * The artefact families whose project-layer copy is decided here.
88
+ *
89
+ * `rules` is deliberately ABSENT. Rules project to five host directories and are
90
+ * already partitioned per directory by {@link globalRuleLayers.hostLayerCarries};
91
+ * adding a claude-only row for them would put a second decision on the one
92
+ * family that does not need it. Skills, personas and the colon-form commands
93
+ * project to `.claude/` alone, which is why one host is the whole answer for them.
94
+ */
95
+ export type ClaudeArtefactFamily = 'skills' | 'personas' | 'commands';
96
+
97
+ /** Family → its directory under the user's home. */
98
+ export const CLAUDE_ARTEFACT_DIRS: Readonly<Record<ClaudeArtefactFamily, string>> = {
99
+ skills: '.claude/skills',
100
+ personas: '.claude/personas',
101
+ commands: '.claude/commands',
102
+ };
103
+
104
+ const _defaultHome = (): string => process.env['HOME'] ?? os.homedir();
105
+
106
+ /**
107
+ * One directory read per (family, home) per process.
108
+ *
109
+ * The generators ask per NAME — hundreds of times for skills, ~90 for commands —
110
+ * and the `commands` read walks a tree. Without the memo the per-name grain that
111
+ * makes the fail-safe granular would also make it quadratic. `null` is memoised
112
+ * too: an unreadable layer stays unreadable for the run, and re-probing it once
113
+ * per name would answer the same thing 300 times.
114
+ */
115
+ const _memo = new Map<string, ReadonlySet<string> | null>();
116
+
117
+ /** Test seam — drop the memo so one process can read a layer it just changed. */
118
+ export function _resetClaudeLayerMemoForTest(): void {
119
+ _memo.clear();
120
+ }
121
+
122
+ /** Absolute path to the host-global directory for one family. */
123
+ export function claudeLayerPath(family: ClaudeArtefactFamily, home: string = _defaultHome()): string {
124
+ return path.join(home, CLAUDE_ARTEFACT_DIRS[family]);
125
+ }
126
+
127
+ /**
128
+ * The names the host layer holds for one family, or null when it cannot be read.
129
+ *
130
+ * Null is a real answer and every caller must read it as "no evidence, withhold
131
+ * nothing" — not as an empty layer, which would authorise withholding everything.
132
+ *
133
+ * The name shape is the family's own, because that is what the caller compares
134
+ * against: a skill is a directory basename (`design-tokens`), a persona a
135
+ * filename (`ai-agent.md`), and a command a posix path relative to the commands
136
+ * root (`roadmap/process-full.md` for the colon form, `agent-status.md` for a
137
+ * flat one). `commands` therefore walks; the other two list one level.
138
+ */
139
+ export function claudeLayerNames(
140
+ family: ClaudeArtefactFamily,
141
+ home: string = _defaultHome(),
142
+ ): ReadonlySet<string> | null {
143
+ const dir = claudeLayerPath(family, home);
144
+ const key = `${family}\u0000${dir}`;
145
+ const hit = _memo.get(key);
146
+ if (hit !== undefined) {
147
+ return hit;
148
+ }
149
+ const answer = _readLayer(family, dir);
150
+ _memo.set(key, answer);
151
+ return answer;
152
+ }
153
+
154
+ function _readLayer(family: ClaudeArtefactFamily, dir: string): ReadonlySet<string> | null {
155
+ let entries: fs.Dirent[];
156
+ try {
157
+ entries = fs.readdirSync(dir, { withFileTypes: true });
158
+ } catch {
159
+ return null;
160
+ }
161
+ if (family !== 'commands') {
162
+ // A name is only CARRIED when the artefact behind it resolves. Corrected
163
+ // 2026-09-07 after a second neutral review: this returned every dirent
164
+ // name, so an empty `~/.claude/skills/<name>/` — no `SKILL.md` — made
165
+ // `claudeLayerHolds` true, the project layer withheld its copy, and the
166
+ // artefact was then delivered from NEITHER layer. That is the
167
+ // removal-with-no-repair-path this module's own limits section says it
168
+ // avoids, and the first fixture written for the wrapper gate created
169
+ // exactly that state and called the withhold correct.
170
+ //
171
+ // Reachable without a crash: a skill directory holding some other file
172
+ // survives `reap_stale`, and `install.ts` mkdirs before it writes.
173
+ // `readProjectedCatalogue` already keys on `SKILL.md` for this reason;
174
+ // the `commands` branch below already required a real `.md`. This is the
175
+ // same requirement for the two families that lacked it.
176
+ const carries = (name: string): boolean => {
177
+ const abs = path.join(dir, name);
178
+ try {
179
+ if (fs.statSync(abs).isDirectory()) {
180
+ return fs.statSync(path.join(abs, 'SKILL.md')).isFile();
181
+ }
182
+ return name.endsWith('.md');
183
+ } catch {
184
+ return false;
185
+ }
186
+ };
187
+ return new Set(
188
+ entries
189
+ .map((e) => e.name)
190
+ .filter((n) => n !== 'README.md')
191
+ .filter(carries),
192
+ );
193
+ }
194
+ const out = new Set<string>();
195
+ const walk = (abs: string, rel: string): void => {
196
+ let kids: fs.Dirent[];
197
+ try {
198
+ kids = fs.readdirSync(abs, { withFileTypes: true });
199
+ } catch {
200
+ return;
201
+ }
202
+ for (const k of kids) {
203
+ const childRel = rel === '' ? k.name : `${rel}/${k.name}`;
204
+ if (k.isDirectory()) {
205
+ walk(path.join(abs, k.name), childRel);
206
+ } else if (k.name.endsWith('.md') && k.name !== 'README.md') {
207
+ out.add(childRel);
208
+ }
209
+ }
210
+ };
211
+ walk(dir, '');
212
+ return out;
213
+ }
214
+
215
+ /**
216
+ * Does the host layer hold this one name?
217
+ *
218
+ * The single-name form of {@link keepInProjectLayer}, for a caller that decides
219
+ * inside a loop and already knows the host-side name. `false` on an unreadable
220
+ * layer — no evidence, so nothing is withheld.
221
+ */
222
+ export function claudeLayerHolds(
223
+ family: ClaudeArtefactFamily,
224
+ name: string,
225
+ home: string = _defaultHome(),
226
+ ): boolean {
227
+ return claudeLayerNames(family, home)?.has(name) ?? false;
228
+ }
229
+
230
+ /**
231
+ * Which of `names` the host layer carries, and which it does not.
232
+ *
233
+ * `carries` is the all-or-nothing answer the rules path reports; `missing` is the
234
+ * one callers act on, because withholding is decided per name. An empty `names`
235
+ * is vacuously carried — withholding nothing needs no evidence.
236
+ */
237
+ export function claudeLayerCarries(
238
+ family: ClaudeArtefactFamily,
239
+ names: readonly string[],
240
+ home: string = _defaultHome(),
241
+ ): CarriesVerdict {
242
+ const layerPath = claudeLayerPath(family, home);
243
+ const have = claudeLayerNames(family, home);
244
+ if (have === null) {
245
+ return { carries: false, layerPath, missing: [...names], reason: 'layer-absent' };
246
+ }
247
+ const missing = names.filter((n) => !have.has(n));
248
+ return {
249
+ carries: missing.length === 0,
250
+ layerPath,
251
+ missing,
252
+ reason: missing.length === 0 ? 'carries' : 'missing-names',
253
+ };
254
+ }
255
+
256
+ /**
257
+ * The subset of `names` the project layer must still deliver.
258
+ *
259
+ * The one function the generators call. Returns the names the host layer does
260
+ * NOT hold, in the order given — so an unreadable layer returns everything
261
+ * (fail safe) and a complete layer returns nothing.
262
+ */
263
+ export function keepInProjectLayer(
264
+ family: ClaudeArtefactFamily,
265
+ names: readonly string[],
266
+ home: string = _defaultHome(),
267
+ ): string[] {
268
+ const have = claudeLayerNames(family, home);
269
+ if (have === null) {
270
+ return [...names];
271
+ }
272
+ return names.filter((n) => !have.has(n));
273
+ }
@@ -49,6 +49,82 @@ export function derive_trigger_globs(meta: Record<string, unknown>): string[] {
49
49
  return [...new Set(globs)];
50
50
  }
51
51
 
52
+ /**
53
+ * The ceiling for a description a native lazy form routes on.
54
+ *
55
+ * NOT a host-published limit, and saying so is the point: nothing in this tree
56
+ * records what Cursor or Windsurf actually truncate at, so a number claiming to
57
+ * be theirs would be invented. This is a PACKAGE cap, chosen against the corpus
58
+ * it applies to — the 119 projected rule descriptions measure max 187 and mean
59
+ * 115 characters, so 400 leaves every existing description intact and gives the
60
+ * appended trigger terms roughly twice the mean to work in.
61
+ *
62
+ * Lower it if a host publishes a real limit below 400, or if a rule's emitted
63
+ * description is ever observed truncated by one.
64
+ */
65
+ export const APPLIES_WHEN_CAP = 400;
66
+
67
+ /** How the appended clause opens. Exported so a detector cannot re-spell it. */
68
+ export const APPLIES_WHEN_PREFIX = ' Applies when: ';
69
+
70
+ /**
71
+ * Keyword and phrase trigger terms, in declaration order, de-duplicated.
72
+ *
73
+ * Path-shaped triggers are deliberately excluded: they already activate the
74
+ * host natively through `globs`, so repeating them in prose spends description
75
+ * budget to say something the host is already acting on.
76
+ */
77
+ export function trigger_terms(meta: Record<string, unknown>): string[] {
78
+ const triggers = meta['triggers'];
79
+ if (!Array.isArray(triggers)) return [];
80
+ const out: string[] = [];
81
+ for (const t of triggers) {
82
+ if (t === null || typeof t !== 'object' || Array.isArray(t)) continue;
83
+ const obj = t as Record<string, unknown>;
84
+ for (const key of ['keyword', 'phrase'] as const) {
85
+ const v = obj[key];
86
+ if (typeof v === 'string' && v.trim() !== '') out.push(v.trim());
87
+ }
88
+ }
89
+ return [...new Set(out)];
90
+ }
91
+
92
+ /**
93
+ * Lower a rule's keyword and phrase triggers into the description a native lazy
94
+ * form routes on (road-to-delivery-on-hook-hosts 3.1).
95
+ *
96
+ * Cursor and Windsurf have no delivery mechanism: an `auto` rule with no
97
+ * path-shaped trigger reaches them as `alwaysApply: false` / `model_decision`
98
+ * with nothing but the description to route on. The rule's own trigger terms
99
+ * are the most routing-relevant text that exists for it, and they were going
100
+ * nowhere.
101
+ *
102
+ * Three properties, each of which is a way this could go wrong:
103
+ *
104
+ * · **Never truncates the original description.** Terms are appended and the
105
+ * clause is dropped WHOLE when it will not fit, so a description can only
106
+ * gain. A cap that ate the sentence would trade routing for readability
107
+ * without saying so.
108
+ * · **Whole terms only.** Terms are added one at a time while they fit; a
109
+ * half term is never emitted, because a truncated phrase routes on nothing
110
+ * and reads as a typo.
111
+ * · **Idempotent.** A description that already carries the clause is returned
112
+ * unchanged, so a re-run of the emitter cannot stack clauses.
113
+ */
114
+ export function applies_when(description: string, meta: Record<string, unknown>, cap: number = APPLIES_WHEN_CAP): string {
115
+ if (description.includes(APPLIES_WHEN_PREFIX.trim())) return description;
116
+ const terms = trigger_terms(meta);
117
+ if (terms.length === 0) return description;
118
+ const kept: string[] = [];
119
+ for (const term of terms) {
120
+ const candidate = `${description}${APPLIES_WHEN_PREFIX}${[...kept, term].join(', ')}.`;
121
+ if (candidate.length > cap) break;
122
+ kept.push(term);
123
+ }
124
+ if (kept.length === 0) return description;
125
+ return `${description}${APPLIES_WHEN_PREFIX}${kept.join(', ')}.`;
126
+ }
127
+
52
128
  /**
53
129
  * A brace group the host expands, versus an agent-config placeholder that only
54
130
  * LOOKS like one.
@@ -4,7 +4,7 @@
4
4
  * ## Why a fingerprint and not a version number
5
5
  *
6
6
  * The single-delivery partition is a *removal*: after it, `task generate-tools`
7
- * writes 16 rules and zero skills into `<repo>/.claude/` and every other
7
+ * writes 13 rules and zero skills (15 package-only, 2 byte-identically deduped at user scope) plus 49 flat-command wrappers into `<repo>/.claude/` and every other
8
8
  * artefact is delivered ONLY by the host-global layer. That removes the build's
9
9
  * own repair path — regeneration can no longer heal a stale global layer,
10
10
  * because it stops writing the affected files at all.