@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,341 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Every shipped skill, and the entry paths that can reach it.
4
+ *
5
+ * `road-to-skill-menu-economy` step 1.1. The roadmap's lever is COUNT ON THE
6
+ * MENU — the description corpus already sits at the schema cap, so there is no
7
+ * byte-shaving budget to spend (K2). Deciding which skills may leave the model
8
+ * menu needs to know, per skill, whether anything other than the menu can
9
+ * reach it. This report answers that mechanically and publishes the rule it
10
+ * used, so a later reader can disagree with the rule rather than with a number.
11
+ *
12
+ * WHAT IT IS NOT
13
+ * --------------
14
+ * It is a REPORT, deliberately: no threshold, no exit code that carries a
15
+ * verdict, and a `report_` prefix so `_lib/gate_population.ts` classifies it
16
+ * out of the gate population rather than by whoever last edited a manifest.
17
+ *
18
+ * It also does NOT decide "sole entry path", and that limit is load-bearing
19
+ * rather than an apology. A command reference proves a command CAN reach the
20
+ * skill; it cannot prove the menu never does. The roadmap's own Risk 1 is
21
+ * precisely this — marking a skill `user-invocable: false` is correct only if
22
+ * the command or flow path really is the only one — so this report produces
23
+ * the evidence for that judgement and never the judgement.
24
+ *
25
+ * THE CLASSIFICATION RULE, stated because it is the whole content
26
+ * -------------------------------------------------------------
27
+ * For each `src/skills/<name>/SKILL.md`, three signals are read:
28
+ *
29
+ * command_refs files under `src/domains/**\/command.md` that reference the
30
+ * skill in one of three DISAMBIGUATING shapes — `skill:<name>`,
31
+ * a `skills/<name>` path, or the bare name in backticks. A
32
+ * plain word-boundary match is deliberately NOT used: skill
33
+ * names like `security`, `database` and `docker` are ordinary
34
+ * English and would hit on every prose mention.
35
+ * flow_refs files under `src/flows/` whose own `skills:` list names it.
36
+ * Flows carry YAML name lists rather than the shapes above, so
37
+ * the command regex read 0 for all 299 on the first pass — a
38
+ * false zero, and the reason `flowSkillNames` exists.
39
+ * menu_present true unless the frontmatter carries `user-invocable: false`
40
+ * or `disable-model-invocation: true` — the two menu-economy
41
+ * keys the roadmap measures as nearly unused.
42
+ *
43
+ * and the row is classified:
44
+ *
45
+ * both command_refs > 0 AND flow_refs > 0
46
+ * command-only command_refs > 0, flow_refs = 0
47
+ * flow-only flow_refs > 0, command_refs = 0
48
+ * model-routed neither, and the skill is on the menu — the menu is then the
49
+ * only entry path this repository can point at
50
+ * orphan neither, and the skill is off the menu: nothing in the tree
51
+ * reaches it. Reported, never deleted (K3).
52
+ *
53
+ * `command-only` and `flow-only` name a CANDIDATE for `user-invocable: false`,
54
+ * never a decision — see the sole-entry limit above.
55
+ *
56
+ * Usage:
57
+ * ./scripts-run src/scripts/report_skill_menu_census # stdout summary
58
+ * ./scripts-run src/scripts/report_skill_menu_census --emit # write the artifact
59
+ * ./scripts-run src/scripts/report_skill_menu_census --emit --pin <sha>
60
+ *
61
+ * Deterministic: rows sort by skill name, no timestamp is written, and the pin
62
+ * is an argument rather than "now", so a re-run at the same pin over the same
63
+ * tree is byte-identical.
64
+ */
65
+
66
+ import { execFileSync } from 'node:child_process';
67
+ import * as fs from 'node:fs';
68
+ import * as path from 'node:path';
69
+ import { fileURLToPath, pathToFileURL } from 'node:url';
70
+
71
+ const _HERE = fileURLToPath(import.meta.url);
72
+ export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
73
+ export const ARTIFACT_REL = path.join('agents', 'evidence', 'analysis', 'skill-menu-census-2026-09.md');
74
+
75
+ export type EntryPath = 'both' | 'command-only' | 'flow-only' | 'model-routed' | 'orphan';
76
+
77
+ export interface SkillRow {
78
+ readonly name: string;
79
+ readonly commandRefs: number;
80
+ readonly flowRefs: number;
81
+ readonly menuPresent: boolean;
82
+ readonly hasTriggerCorpus: boolean;
83
+ readonly entryPath: EntryPath;
84
+ /** One command or flow file, for an operator to start from. Empty when none. */
85
+ readonly firstRef: string;
86
+ }
87
+
88
+ /** Escape a skill name for use inside a RegExp. Names are kebab-case, so this is cheap insurance. */
89
+ function escapeRe(s: string): string {
90
+ return s.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
91
+ }
92
+
93
+ /**
94
+ * The three disambiguating reference shapes, as one regex.
95
+ *
96
+ * A bare word-boundary match was tried first and rejected on inspection: 299
97
+ * skill names include `security`, `database`, `docker`, `performance` and
98
+ * `mode`, each of which appears in ordinary command prose. A reference that
99
+ * cannot be told from a sentence is not evidence of an entry path.
100
+ */
101
+ export function referenceRe(skill: string): RegExp {
102
+ const n = escapeRe(skill);
103
+ const tick = String.fromCharCode(96);
104
+ return new RegExp(
105
+ 'skill:\\s*' + n + '(?![a-z0-9-])' +
106
+ '|skills/' + n + '(?:[/\\s' + tick + ')\\]]|$)' +
107
+ '|' + tick + n + tick,
108
+ 'u',
109
+ );
110
+ }
111
+
112
+ /** Every file under `dir` matching `pred`, sorted, as repo-relative paths. */
113
+ export function walk(root: string, dir: string, pred: (rel: string) => boolean): string[] {
114
+ const out: string[] = [];
115
+ const abs = path.join(root, dir);
116
+ if (!fs.existsSync(abs)) return out;
117
+ const stack: string[] = [dir];
118
+ while (stack.length > 0) {
119
+ const cur = stack.pop() as string;
120
+ let entries: fs.Dirent[];
121
+ try {
122
+ entries = fs.readdirSync(path.join(root, cur), { withFileTypes: true });
123
+ } catch {
124
+ continue;
125
+ }
126
+ for (const e of entries) {
127
+ const rel = path.join(cur, e.name);
128
+ if (e.isDirectory()) stack.push(rel);
129
+ else if (e.isFile() && pred(rel)) out.push(rel);
130
+ }
131
+ }
132
+ return out.sort();
133
+ }
134
+
135
+ /** Frontmatter values this report needs. Hand-parsed: only two boolean keys matter. */
136
+ export function menuFlags(text: string): { userInvocable: boolean | null; disableModel: boolean | null } {
137
+ const head = text.slice(0, 4000);
138
+ const ui = /^user-invocable:\s*(true|false)\s*$/mu.exec(head);
139
+ const dm = /^disable-model-invocation:\s*(true|false)\s*$/mu.exec(head);
140
+ return {
141
+ userInvocable: ui === null ? null : ui[1] === 'true',
142
+ disableModel: dm === null ? null : dm[1] === 'true',
143
+ };
144
+ }
145
+
146
+ /**
147
+ * The skill names a flow file declares, from its own `skills:` lists.
148
+ *
149
+ * Flows do NOT use the three reference shapes commands use — they carry YAML
150
+ * lists of bare names (`skills: [code-review, adversarial-review]`, or a block
151
+ * sequence). Running the command-shaped regex over them returned flow_refs = 0
152
+ * for all 299 skills on the first pass, which was a false zero and is why this
153
+ * parser exists rather than a wider word-boundary match: a bare name is safe
154
+ * HERE precisely because the `skills:` key bounds where it may be read.
155
+ */
156
+ export function flowSkillNames(text: string): Set<string> {
157
+ const out = new Set<string>();
158
+ const lines = text.split('\n');
159
+ for (let i = 0; i < lines.length; i += 1) {
160
+ const line = lines[i] as string;
161
+ const inline = /^\s*skills:\s*\[([^\]]*)\]\s*$/u.exec(line);
162
+ if (inline !== null) {
163
+ for (const raw of (inline[1] as string).split(',')) {
164
+ const n = raw.trim().replace(/^['"]|['"]$/gu, '');
165
+ if (n !== '') out.add(n);
166
+ }
167
+ continue;
168
+ }
169
+ if (!/^\s*skills:\s*$/u.test(line)) continue;
170
+ const indent = (/^\s*/u.exec(line) as RegExpExecArray)[0].length;
171
+ for (let j = i + 1; j < lines.length; j += 1) {
172
+ const item = /^(\s*)-\s*(\S.*?)\s*$/u.exec(lines[j] as string);
173
+ if (item === null || (item[1] as string).length <= indent) break;
174
+ out.add((item[2] as string).replace(/^['"]|['"]$/gu, ''));
175
+ }
176
+ }
177
+ return out;
178
+ }
179
+
180
+ export function classify(commandRefs: number, flowRefs: number, menuPresent: boolean): EntryPath {
181
+ if (commandRefs > 0 && flowRefs > 0) return 'both';
182
+ if (commandRefs > 0) return 'command-only';
183
+ if (flowRefs > 0) return 'flow-only';
184
+ return menuPresent ? 'model-routed' : 'orphan';
185
+ }
186
+
187
+ export function census(root: string): SkillRow[] {
188
+ const skillsDir = path.join(root, 'src', 'skills');
189
+ const names = fs
190
+ .readdirSync(skillsDir, { withFileTypes: true })
191
+ .filter((e) => e.isDirectory() && fs.existsSync(path.join(skillsDir, e.name, 'SKILL.md')))
192
+ .map((e) => e.name)
193
+ .sort();
194
+
195
+ const commandFiles = walk(root, path.join('src', 'domains'), (rel) => path.basename(rel) === 'command.md');
196
+ const flowFiles = walk(root, path.join('src', 'flows'), () => true);
197
+ const commandText = commandFiles.map((rel) => [rel, fs.readFileSync(path.join(root, rel), 'utf-8')] as const);
198
+ const flowDecl = flowFiles.map(
199
+ (rel) => [rel, flowSkillNames(fs.readFileSync(path.join(root, rel), 'utf-8'))] as const,
200
+ );
201
+
202
+ const rows: SkillRow[] = [];
203
+ for (const name of names) {
204
+ const skillMd = path.join(skillsDir, name, 'SKILL.md');
205
+ const text = fs.readFileSync(skillMd, 'utf-8');
206
+ const flags = menuFlags(text);
207
+ const menuPresent = flags.userInvocable !== false && flags.disableModel !== true;
208
+ const re = referenceRe(name);
209
+ const cmdHits = commandText.filter(([, t]) => re.test(t)).map(([rel]) => rel);
210
+ const flowHits = flowDecl.filter(([, names]) => names.has(name)).map(([rel]) => rel);
211
+ rows.push({
212
+ name,
213
+ commandRefs: cmdHits.length,
214
+ flowRefs: flowHits.length,
215
+ menuPresent,
216
+ hasTriggerCorpus: fs.existsSync(path.join(skillsDir, name, 'evals', 'triggers.json')),
217
+ entryPath: classify(cmdHits.length, flowHits.length, menuPresent),
218
+ firstRef: cmdHits[0] ?? flowHits[0] ?? '',
219
+ });
220
+ }
221
+ return rows;
222
+ }
223
+
224
+ export function tally(rows: readonly SkillRow[]): Record<EntryPath, number> {
225
+ const t: Record<EntryPath, number> = {
226
+ both: 0,
227
+ 'command-only': 0,
228
+ 'flow-only': 0,
229
+ 'model-routed': 0,
230
+ orphan: 0,
231
+ };
232
+ for (const r of rows) t[r.entryPath] += 1;
233
+ return t;
234
+ }
235
+
236
+ export function renderArtifact(rows: readonly SkillRow[], pin: string): string {
237
+ const t = tally(rows);
238
+ const candidates = t['command-only'] + t['flow-only'];
239
+ const L: string[] = [];
240
+ L.push('<!-- evidence-type: analysis -->');
241
+ L.push('# Skill menu census — every skill, and what can reach it');
242
+ L.push('');
243
+ L.push(`Pinned to commit \`${pin}\`. Generated by \`./scripts-run src/scripts/report_skill_menu_census --emit --pin ${pin}\`;`);
244
+ L.push('re-running it at the same pin over the same tree is byte-identical, which is what');
245
+ L.push('`road-to-skill-menu-economy` step 1.1 asks to be checkable.');
246
+ L.push('');
247
+ L.push('## The rule this census applied');
248
+ L.push('');
249
+ L.push('**Commands.** A `src/domains/**/command.md` file counts as reaching a skill when it');
250
+ L.push('references it in one of three disambiguating shapes — `skill:<name>`, a `skills/<name>`');
251
+ L.push('path, or the bare name in backticks. A plain word-boundary match is NOT used: names');
252
+ L.push('like `security`, `database` and `docker` are ordinary English and would hit on every');
253
+ L.push('prose mention. The `skill:<name>` shape is closed with a negative lookahead rather');
254
+ L.push('than a word boundary: `\\b` sits between `w` and `-`, so `skill:code-review-lens` was a');
255
+ L.push('hit for `code-review` and a skill would have inherited a longer sibling\'s references.');
256
+ L.push('Tightening it moved no figure in this corpus, which is recorded rather than assumed.');
257
+ L.push('');
258
+ L.push('**Flows.** A `src/flows/*.yaml` file counts when its own `skills:` list names the skill.');
259
+ L.push('Flows carry YAML name lists rather than the shapes above, so applying the command regex');
260
+ L.push('to them returned `flow_refs = 0` for all 299 skills on the first pass. That was a false');
261
+ L.push('zero, corrected before publication; a bare name is safe here precisely because the');
262
+ L.push('`skills:` key bounds where it may be read.');
263
+ L.push('');
264
+ L.push('**Menu.** `menu_present` is true unless the frontmatter carries `user-invocable: false`');
265
+ L.push('or `disable-model-invocation: true`.');
266
+ L.push('');
267
+ L.push('| Class | Meaning |');
268
+ L.push('|---|---|');
269
+ L.push('| `both` | reachable from a command AND from a flow |');
270
+ L.push('| `command-only` | a command references it; no flow does |');
271
+ L.push('| `flow-only` | a flow references it; no command does |');
272
+ L.push('| `model-routed` | neither, and it is on the menu — the menu is the only entry path this tree can point at |');
273
+ L.push('| `orphan` | neither, and it is off the menu: nothing here reaches it |');
274
+ L.push('');
275
+ L.push('## What this census does NOT establish');
276
+ L.push('');
277
+ L.push('It does not decide **sole entry path**. A command reference proves a command CAN');
278
+ L.push('reach the skill; nothing here proves the menu never does. `command-only` and');
279
+ L.push('`flow-only` are therefore CANDIDATES for `user-invocable: false` and never the');
280
+ L.push('marking decision — the roadmap\'s Risk 1 is exactly this misclassification, and its');
281
+ L.push('stated mitigation is a successful command invocation per marked skill, which is a');
282
+ L.push('step 1.2 obligation and not something a static scan can discharge.');
283
+ L.push('');
284
+ L.push('An `orphan` row is a question, not a deletion list (K3).');
285
+ L.push('');
286
+ L.push('## Totals');
287
+ L.push('');
288
+ L.push('| Class | Count |');
289
+ L.push('|---|---:|');
290
+ for (const k of ['both', 'command-only', 'flow-only', 'model-routed', 'orphan'] as EntryPath[]) {
291
+ L.push(`| \`${k}\` | ${String(t[k])} |`);
292
+ }
293
+ L.push(`| **total** | **${String(rows.length)}** |`);
294
+ L.push('');
295
+ L.push(`Menu-removal candidates (\`command-only\` + \`flow-only\`): **${String(candidates)}**.`);
296
+ L.push('');
297
+ L.push('## Rows');
298
+ L.push('');
299
+ L.push('| Skill | Entry path | cmd refs | flow refs | on menu | triggers.json | first reference |');
300
+ L.push('|---|---|---:|---:|:-:|:-:|---|');
301
+ for (const r of rows) {
302
+ L.push(
303
+ `| \`${r.name}\` | \`${r.entryPath}\` | ${String(r.commandRefs)} | ${String(r.flowRefs)} | ` +
304
+ `${r.menuPresent ? 'yes' : 'no'} | ${r.hasTriggerCorpus ? 'yes' : 'no'} | ` +
305
+ `${r.firstRef === '' ? '—' : `\`${r.firstRef}\``} |`,
306
+ );
307
+ }
308
+ L.push('');
309
+ return L.join('\n');
310
+ }
311
+
312
+ function headSha(root: string): string {
313
+ return execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf-8' }).trim();
314
+ }
315
+
316
+ export function main(argv: readonly string[]): number {
317
+ const root = REPO_ROOT;
318
+ const emit = argv.includes('--emit');
319
+ const pinIdx = argv.indexOf('--pin');
320
+ const pin = pinIdx !== -1 && pinIdx + 1 < argv.length ? (argv[pinIdx + 1] as string) : headSha(root);
321
+ const rows = census(root);
322
+ const t = tally(rows);
323
+ if (emit) {
324
+ const out = path.join(root, ARTIFACT_REL);
325
+ fs.mkdirSync(path.dirname(out), { recursive: true });
326
+ fs.writeFileSync(out, renderArtifact(rows, pin), 'utf-8');
327
+ process.stdout.write(`wrote ${ARTIFACT_REL} · ${String(rows.length)} row(s) · pin ${pin}\n`);
328
+ }
329
+ process.stdout.write(`scanned: ${String(rows.length)} skill(s)\n`);
330
+ for (const k of ['both', 'command-only', 'flow-only', 'model-routed', 'orphan'] as EntryPath[]) {
331
+ process.stdout.write(` ${k.padEnd(14)} ${String(t[k]).padStart(4)}\n`);
332
+ }
333
+ process.stdout.write(
334
+ ` menu-removal candidates (command-only + flow-only): ${String(t['command-only'] + t['flow-only'])}\n`,
335
+ );
336
+ return 0;
337
+ }
338
+
339
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
340
+ process.exit(main(process.argv.slice(2)));
341
+ }
@@ -66,9 +66,16 @@ export const ROOT = path.resolve(path.dirname(_HERE), '..', '..', '..');
66
66
  * start ranking different trees. `null` when no candidate root exists, and the
67
67
  * CLI turns that into a distinct exit code rather than an empty list.
68
68
  */
69
- import { DEFAULT_CATALOGUE_ROOTS, resolveSkillsRoot } from '../_lib/skill_catalogue.js';
69
+ import { DEFAULT_CATALOGUE_ROOTS, resolveSkillCatalogueRoots } from '../_lib/skill_catalogue.js';
70
70
 
71
- export const DEFAULT_SKILLS_DIR: string | null = resolveSkillsRoot(ROOT);
71
+ /**
72
+ * Every readable catalogue root, in precedence order — possibly empty.
73
+ *
74
+ * The default the CLI and the hook rank across. `DEFAULT_SKILLS_DIR` below is
75
+ * the first of them and stays exported for the surfaces that name one directory.
76
+ */
77
+ export const DEFAULT_SKILLS_ROOTS: readonly string[] = resolveSkillCatalogueRoots(ROOT);
78
+ export const DEFAULT_SKILLS_DIR: string | null = DEFAULT_SKILLS_ROOTS[0] ?? null;
72
79
 
73
80
  /** Mirror Python len(str) — count Unicode code points, not UTF-16 units. */
74
81
  function pyLen(s: string): number {
@@ -164,6 +171,27 @@ export interface Skill {
164
171
  triggerText: string[];
165
172
  }
166
173
 
174
+ /**
175
+ * Load one root, or several with the FIRST occurrence of a name winning.
176
+ *
177
+ * Precedence by name, never by root: every root is read, and a collision
178
+ * resolves to the earliest root that carries it — `resolveSkillCatalogueRoots`
179
+ * owns what that order means. Reading only the first root is the defect this
180
+ * replaced; silently ranking one name twice would be the next one.
181
+ */
182
+ function _load_skills_across(roots: readonly string[]): Skill[] {
183
+ const out: Skill[] = [];
184
+ const seen = new Set<string>();
185
+ for (const root of roots) {
186
+ for (const s of _load_skills(root)) {
187
+ if (seen.has(s.name)) continue;
188
+ seen.add(s.name);
189
+ out.push(s);
190
+ }
191
+ }
192
+ return out;
193
+ }
194
+
167
195
  function _load_skills(skillsDir: string): Skill[] {
168
196
  const skills: Skill[] = [];
169
197
  for (const skillMd of _globSkillMd(skillsDir)) {
@@ -246,9 +274,16 @@ function _score(taskTerms: Set<string>, skill: Skill, opts: RankOptions = {}): n
246
274
 
247
275
  export type RankRow = [string, number, string[]];
248
276
 
249
- export function rank(task: string, skillsDir: string, opts: RankOptions = {}): RankRow[] {
277
+ /**
278
+ * @param skillsDir one root, or several to rank across (see `_load_skills_across`).
279
+ */
280
+ export function rank(
281
+ task: string,
282
+ skillsDir: string | readonly string[],
283
+ opts: RankOptions = {},
284
+ ): RankRow[] {
250
285
  const taskTerms = _tokenize(task);
251
- const skills = _load_skills(skillsDir);
286
+ const skills = _load_skills_across(typeof skillsDir === 'string' ? [skillsDir] : skillsDir);
252
287
  const rows: RankRow[] = [];
253
288
  for (const s of skills) {
254
289
  const score = _score(taskTerms, s, opts);
@@ -384,6 +419,15 @@ interface Args {
384
419
  task: string;
385
420
  /** `null` when no catalogue root exists — a DIFFERENT answer from an empty ranking. */
386
421
  skills_dir: string | null;
422
+ /**
423
+ * Did the operator NAME the directory?
424
+ *
425
+ * An explicit `--skills-dir` means one root and only that root: an operator
426
+ * pointing at a tree is asking about that tree, and quietly unioning the
427
+ * host catalogue into their answer would make the flag mean something else.
428
+ * Absent, the default ranks across every readable root.
429
+ */
430
+ skills_dir_explicit: boolean;
387
431
  top: number;
388
432
  json: boolean;
389
433
  sample: boolean;
@@ -407,6 +451,7 @@ export function parse_args(argv: string[]): Args {
407
451
  const args: Args = {
408
452
  task: '',
409
453
  skills_dir: DEFAULT_SKILLS_DIR,
454
+ skills_dir_explicit: false,
410
455
  top: 0,
411
456
  json: false,
412
457
  sample: false,
@@ -431,8 +476,10 @@ export function parse_args(argv: string[]): Args {
431
476
  _argError('argument --skills-dir: expected one argument');
432
477
  }
433
478
  args.skills_dir = v;
479
+ args.skills_dir_explicit = true;
434
480
  } else if (a.startsWith('--skills-dir=')) {
435
481
  args.skills_dir = a.slice('--skills-dir='.length);
482
+ args.skills_dir_explicit = true;
436
483
  } else if (a === '--top') {
437
484
  const v = argv[++i];
438
485
  if (v === undefined) {
@@ -497,7 +544,24 @@ export function main(argv: string[] | null = null): number {
497
544
  // operator who passes a wrong path is in exactly the position the silent
498
545
  // failure was about, and answering them with "(no relevant skills found)"
499
546
  // is the same wrong answer with a different cause.
500
- const unreadable = catalogueDeficit(args.skills_dir);
547
+ // One root when the operator named it; otherwise every readable root, so the
548
+ // ranking covers what the session can actually reach.
549
+ const roots: readonly string[] = args.skills_dir_explicit
550
+ ? args.skills_dir === null
551
+ ? []
552
+ : [args.skills_dir]
553
+ : DEFAULT_SKILLS_ROOTS;
554
+ // An EXPLICIT directory is always deficit-checked, even when it resolved to a
555
+ // one-element list: a wrong path must not degrade to "(no relevant skills
556
+ // found)", which is the silent failure the check exists for and which the
557
+ // first draft of this branch reintroduced. For the DEFAULT, the deficit is
558
+ // "not one readable root anywhere" — a single empty candidate is no longer a
559
+ // deficit, because the others may still carry the catalogue.
560
+ const unreadable = args.skills_dir_explicit
561
+ ? catalogueDeficit(args.skills_dir)
562
+ : roots.length === 0
563
+ ? catalogueDeficit(null)
564
+ : null;
501
565
  if (unreadable !== null) {
502
566
  if (args.json) {
503
567
  process.stdout.write(
@@ -520,7 +584,7 @@ export function main(argv: string[] | null = null): number {
520
584
  }
521
585
  // Non-null past the deficit check above, which returns before this line
522
586
  // whenever the root cannot be ranked.
523
- const rows = rank(task, args.skills_dir as string);
587
+ const rows = rank(task, roots);
524
588
  if (args.json) {
525
589
  const sliced = args.top ? rows.slice(0, args.top) : rows;
526
590
  const payload = sliced.map(([n, s, p]) => ({ name: n, score: s, personas: p }));
@@ -32,7 +32,7 @@ import { audit, type PersonaRow } from './audit_persona_coverage.js';
32
32
  /** Sentinel for "no catalogue root resolved" — never a readable path. */
33
33
  const NO_CATALOGUE = '<no-skills-catalogue>';
34
34
 
35
- import { DEFAULT_SKILLS_DIR, rank } from './score_skill_relevance.js';
35
+ import { DEFAULT_SKILLS_DIR, DEFAULT_SKILLS_ROOTS, rank } from './score_skill_relevance.js';
36
36
 
37
37
  const _HERE = fileURLToPath(import.meta.url);
38
38
  // src/scripts/skill_tools/suggest_skill_for_task.ts → parents[3] of the .py
@@ -78,13 +78,27 @@ export interface Combo {
78
78
  why: string;
79
79
  }
80
80
 
81
+ /**
82
+ * @param skillsDir the root the PERSONA audit reads — one directory by necessity.
83
+ * @param rankRoots roots to RANK across; defaults to `[skillsDir]`.
84
+ *
85
+ * The two parameters are separate because the two questions are. Ranking wants
86
+ * every catalogue the session can reach (`resolveSkillCatalogueRoots`); the
87
+ * persona-coverage audit walks one tree and pairs it with one persona directory.
88
+ * Collapsing them was the state a neutral review found on 2026-09-07: the MCP
89
+ * handler ranked the union while this CLI ranked the first root, so a consumer
90
+ * carrying its own project skills got a different answer from each — exactly the
91
+ * two-resolvers-over-one-catalogue split the resolver's docstring warns about,
92
+ * reproduced by the change that claimed to fix it.
93
+ */
81
94
  export function suggest(
82
95
  task: string,
83
96
  skillsDir: string,
84
97
  personasDir: string,
85
98
  top = 3,
99
+ rankRoots: readonly string[] = [skillsDir],
86
100
  ): Combo[] {
87
- const ranked = rank(task, skillsDir).slice(0, top);
101
+ const ranked = rank(task, rankRoots).slice(0, top);
88
102
  const personaRows = audit(skillsDir, personasDir);
89
103
  const status = _persona_status(personaRows);
90
104
  return ranked.map(([name, score, personas]) => ({
@@ -193,6 +207,8 @@ const PROG = 'suggest_skill_for_task.py';
193
207
  interface Args {
194
208
  task: string;
195
209
  skills_dir: string;
210
+ /** Did the operator NAME the directory? One tree then, and only that tree. */
211
+ skills_dir_explicit: boolean;
196
212
  personas_dir: string;
197
213
  top: number;
198
214
  json: boolean;
@@ -219,6 +235,7 @@ export function parse_args(argv: string[]): Args {
219
235
  // DEFAULT_SKILLS_DIR removed. When no catalogue resolves, this reads
220
236
  // as the literal marker below and the caller's own check reports it.
221
237
  skills_dir: DEFAULT_SKILLS_DIR ?? NO_CATALOGUE,
238
+ skills_dir_explicit: false,
222
239
  personas_dir: DEFAULT_PERSONAS,
223
240
  top: 3,
224
241
  json: false,
@@ -244,8 +261,10 @@ export function parse_args(argv: string[]): Args {
244
261
  _argError('argument --skills-dir: expected one argument');
245
262
  }
246
263
  args.skills_dir = v;
264
+ args.skills_dir_explicit = true;
247
265
  } else if (a.startsWith('--skills-dir=')) {
248
266
  args.skills_dir = a.slice('--skills-dir='.length);
267
+ args.skills_dir_explicit = true;
249
268
  } else if (a === '--personas-dir') {
250
269
  const v = argv[++i];
251
270
  if (v === undefined) {
@@ -279,7 +298,22 @@ export function main(argv: string[] | null = null): number {
279
298
  if (!task) {
280
299
  _argError('--task is required (or pass --sample)');
281
300
  }
282
- const combos = suggest(task, args.skills_dir, args.personas_dir, args.top);
301
+ // Rank across every readable root; audit personas against the first. An
302
+ // explicit --skills-dir names one tree and is honoured as one tree.
303
+ //
304
+ // Keyed on the FLAG, not on value-equality against the default. Corrected
305
+ // after a second neutral review: comparing paths meant
306
+ // `--skills-dir <the resolved default>` silently ranked the union including
307
+ // `~/.claude/skills`, so the comment above was false for exactly the operator
308
+ // who spelled the default out. And when no catalogue resolves at all,
309
+ // `skills_dir` is the `NO_CATALOGUE` sentinel while `DEFAULT_SKILLS_DIR` is
310
+ // null, so the old comparison was false there too — the branch was dead in
311
+ // the state it was written for.
312
+ const rankRoots =
313
+ !args.skills_dir_explicit && DEFAULT_SKILLS_ROOTS.length > 0
314
+ ? DEFAULT_SKILLS_ROOTS
315
+ : [args.skills_dir];
316
+ const combos = suggest(task, args.skills_dir, args.personas_dir, args.top, rankRoots);
283
317
  if (args.json) {
284
318
  process.stdout.write(pyJsonDumpsIndent2({ task, suggestions: combos }));
285
319
  process.stdout.write('\n');
@@ -0,0 +1,61 @@
1
+ # Vendored tree-sitter grammars
2
+
3
+ Compiled WebAssembly grammars for the native code-graph engine
4
+ (`src/scripts/code_graph/`). They are vendored — carried in this repository and
5
+ published inside the npm tarball — so that a consumer who runs
6
+ `npm install @event4u/agent-config` can build a code graph on the next command
7
+ with no manual parser install (ADR-259).
8
+
9
+ ## Why vendored rather than a dependency
10
+
11
+ `tree-sitter-wasms` carries 36 grammars (49 MiB on disk). Depending on it at
12
+ runtime would hand every consumer all 36, which is the option ADR-259 §
13
+ Alternatives rejects on exactly that measurement. Only the three grammars below
14
+ are loadable by the engine — `GRAMMAR_WASM` in `src/scripts/code_graph/types.ts`
15
+ has three entries — so the other 33 would be payload with no reader.
16
+
17
+ **No grammar is ever fetched at runtime** (Kill register K1). These files are in
18
+ the tarball; the loader reads them from disk.
19
+
20
+ ## The set
21
+
22
+ | Language | File | SHA-256 |
23
+ |---|---|---|
24
+ | php | `tree-sitter-php.wasm` | `55bb617b6f01e14bab997861f0b20a2420cf6ba3199ffeb295b9ec398966d8a3` |
25
+ | typescript (also serves `.tsx`) | `tree-sitter-typescript.wasm` | `8515404dceed38e1ed86aa34b09fcf3379fff1b4ff9dd3967bcd6d1eb5ac3d8f` |
26
+ | javascript | `tree-sitter-javascript.wasm` | `63812b9e275d26851264734868d27a1656bd44a2ef6eb3e85e6b03728c595ab5` |
27
+
28
+ All three are built against **grammar ABI 14** (`EXPECTED_GRAMMAR_ABI` in
29
+ `src/scripts/code_graph/types.ts`). The loader asserts the ABI on every load, so
30
+ a drifted grammar fails loudly rather than parsing wrongly.
31
+
32
+ ## Provenance
33
+
34
+ Copied byte-for-byte from `tree-sitter-wasms@0.1.13`
35
+ (<https://github.com/Gregoor/tree-sitter-wasms>, Unlicense), which compiles them
36
+ from the upstream `tree-sitter/tree-sitter-{php,typescript,javascript}` grammars
37
+ (MIT). The compiled artifacts are derivatives of the MIT grammar sources, so
38
+ they are annotated MIT in `REUSE.toml` and credited in `CREDITS.md`.
39
+
40
+ `tree-sitter-wasms` remains a **devDependency**: it is the source these copies
41
+ are refreshed from, and the ABI smoke test in `tests/scripts/code_graph.test.ts`
42
+ runs against it. It is deliberately not a runtime dependency.
43
+
44
+ ## Refreshing
45
+
46
+ The pair is ABI-coupled and must move together:
47
+
48
+ ```bash
49
+ npm i -D web-tree-sitter@<v> tree-sitter-wasms@<v>
50
+ for g in php typescript javascript; do
51
+ cp node_modules/tree-sitter-wasms/out/tree-sitter-$g.wasm src/vendor/grammars/
52
+ done
53
+ shasum -a 256 src/vendor/grammars/*.wasm # update the table above
54
+ ```
55
+
56
+ Then update, in the same change:
57
+
58
+ - the three `BINARY_PAYLOAD_EXCEPTIONS` entries in `src/scripts/check_pack_size.ts`
59
+ (they are path-and-size-bound, so any size change invalidates them by design);
60
+ - `EXPECTED_GRAMMAR_ABI` in `src/scripts/code_graph/types.ts` if the ABI moved;
61
+ - the install-size line in `MIGRATION.md`.