@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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +198 -0
- package/CREDITS.md +37 -0
- package/dist/agent-src/commands/analyze/inbox.md +380 -5
- package/dist/agent-src/commands/analyze/repo.md +4 -2
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
- package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
- package/dist/agent-src/rules/token-efficiency.md +26 -3
- package/dist/agent-src/scripts/_cli_entry.ts +51 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
- package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
- package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/dist/agent-src/scripts/stubs_due.ts +4 -18
- package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
- package/dist/agent-src/templates/agent-settings.md +0 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/_cli_entry.js +8 -0
- package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
- package/dist/cli-delegate/archival_sweep.js +8 -0
- package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
- package/dist/cli-delegate/check_update_banner.js +923 -0
- package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
- package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
- package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-BIXEDQPZ.js} +6 -0
- package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
- package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
- package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
- package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
- package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
- package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
- package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
- package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
- package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
- package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
- package/dist/cli-delegate/cmd_conformance.js +2 -2
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +26 -2
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/cli-delegate/dashboard_mode.js +16 -0
- package/dist/cli-delegate/gate_budget.js +24 -0
- package/dist/cli-delegate/gate_execute.js +25 -0
- package/dist/cli-delegate/guarded_baseline.js +18 -0
- package/dist/cli-delegate/resume_probe.js +22 -0
- package/dist/cli-delegate/roadmap_gates.js +638 -0
- package/dist/cli-delegate/roadmap_set_step.js +173 -0
- package/dist/cli-delegate/stub_queue.js +47 -0
- package/dist/cli-delegate/stubs_due.js +38 -0
- package/dist/cli-delegate/ticket_bundles.js +59 -0
- package/dist/cli-delegate/update_roadmap_progress.js +60 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +8 -8
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1193 -917
- package/dist/install/claudeLayerCarriage.js +237 -0
- package/dist/install/claudePathsPlan.js +79 -0
- package/dist/install/hostLayerFingerprint.js +1 -1
- package/dist/install/install.mjs +6 -0
- package/dist/install/partitionEligibility.js +231 -117
- package/dist/install/ruleLayerPartition.js +10 -7
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +39219 -38995
- package/dist/scripts/condense.js +32 -25
- package/docs/MIGRATION.md +49 -1
- package/docs/contracts/settings-classes.md +1 -1
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
- package/package.json +7 -4
- package/src/agent-src/scripts/_cli_entry.ts +51 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
- package/src/agent-src/scripts/roadmap_gates.ts +4 -20
- package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/src/agent-src/scripts/stubs_due.ts +4 -18
- package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/src/config/continuity-surface.json +509 -0
- package/src/config/estate-count-budget.json +1 -1
- package/src/config/gate-coverage.yml +29 -8
- package/src/config/gate-violation-baselines.json +8 -8
- package/src/config/gitignore-block.txt +19 -0
- package/src/config/host-injection-effect.json +73 -0
- package/src/config/packed-binary-manifest.json +33 -0
- package/src/config/preamble-payload-budget.json +112 -105
- package/src/config/publish-surface.json +4 -2
- package/src/install/claudeLayerCarriage.ts +273 -0
- package/src/install/claudePathsPlan.ts +76 -0
- package/src/install/hostLayerFingerprint.ts +1 -1
- package/src/install/partitionEligibility.ts +261 -127
- package/src/install/ruleLayerPartition.ts +19 -23
- package/src/scripts/_dispatch.bash +77 -27
- package/src/scripts/_lib/auto_dispatch.ts +2 -2
- package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
- package/src/scripts/_lib/continuity_surface.ts +296 -0
- package/src/scripts/_lib/corpus_manifest.ts +26 -9
- package/src/scripts/_lib/hook_settings.ts +1 -1
- package/src/scripts/_lib/injection_effect.ts +151 -0
- package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
- package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
- package/src/scripts/_lib/pin_resolver.ts +13 -0
- package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
- package/src/scripts/_lib/retired_status.ts +74 -0
- package/src/scripts/_lib/skill_catalogue.ts +156 -21
- package/src/scripts/_lib/subagent_capsule.ts +117 -8
- package/src/scripts/ai_council/api_transport.ts +5 -0
- package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
- package/src/scripts/ai_council/prompts.ts +9 -0
- package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
- package/src/scripts/capture_skill_catalogue.ts +24 -11
- package/src/scripts/check_candidate_lines.ts +499 -0
- package/src/scripts/check_continuity_surface.ts +475 -0
- package/src/scripts/check_dependency_floors.ts +13 -6
- package/src/scripts/check_estate_count.ts +40 -79
- package/src/scripts/check_pack_size.ts +262 -23
- package/src/scripts/check_roadmap_trackable.ts +19 -16
- package/src/scripts/check_rule_layer_partition.ts +54 -34
- package/src/scripts/check_single_delivery.ts +97 -31
- package/src/scripts/check_update_banner.ts +13 -0
- package/src/scripts/code_graph/build.ts +138 -15
- package/src/scripts/code_graph/cli.ts +76 -8
- package/src/scripts/code_graph/extract.ts +11 -0
- package/src/scripts/code_graph/grammar_probe.ts +43 -0
- package/src/scripts/code_graph/loader.ts +88 -14
- package/src/scripts/code_graph/query.ts +121 -11
- package/src/scripts/code_graph/resolution_tiers.ts +238 -0
- package/src/scripts/code_graph/sqlite_store.ts +247 -6
- package/src/scripts/code_graph/types.ts +66 -1
- package/src/scripts/code_graph/validate.ts +23 -0
- package/src/scripts/condense.ts +34 -22
- package/src/scripts/corpus_manifest.ts +5 -5
- package/src/scripts/council_cli.ts +11 -12
- package/src/scripts/count_intent_disagreement.ts +426 -0
- package/src/scripts/handoff_context_hook.ts +79 -0
- package/src/scripts/hook_effect_doctor.ts +82 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +64 -12
- package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
- package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
- package/src/scripts/hooks/concern_registry.ts +4 -2
- package/src/scripts/hooks/host_semantics.ts +1 -1
- package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
- package/src/scripts/hooks/rule_inject_hook.ts +1 -1
- package/src/scripts/hooks/skill_route_hook.ts +11 -4
- package/src/scripts/inbox_source_census.ts +322 -0
- package/src/scripts/install-hooks.sh +82 -0
- package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
- package/src/scripts/lint_plan_risk_register.ts +13 -17
- package/src/scripts/mcp_server/suggest_skill.ts +72 -0
- package/src/scripts/mcp_server/tools.ts +2 -56
- package/src/scripts/media/lib/adapter-contract.md +7 -2
- package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
- package/src/scripts/rdp_candidates_delta.ts +276 -0
- package/src/scripts/rdp_quality_eval.ts +217 -19
- package/src/scripts/release_mix_taxonomy.json +3 -2
- package/src/scripts/report_host_injection_effect.ts +189 -0
- package/src/scripts/report_layer_overlap.ts +41 -0
- package/src/scripts/report_skill_menu_census.ts +341 -0
- package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
- package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
- package/src/vendor/grammars/README.md +61 -0
- package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
- package/src/scripts/_lib/carrier_status.ts +0 -118
- package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
- 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,
|
|
69
|
+
import { DEFAULT_CATALOGUE_ROOTS, resolveSkillCatalogueRoots } from '../_lib/skill_catalogue.js';
|
|
70
70
|
|
|
71
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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`.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|