@event4u/agent-config 12.1.0 → 13.0.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +151 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/cost/report.md +23 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
- package/dist/agent-src/contexts/subagent-configuration.md +69 -7
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/design-fidelity.md +1 -1
- package/dist/agent-src/rules/design-review-after-ui-write.md +10 -5
- package/dist/agent-src/rules/evaluator-independence.md +27 -5
- package/dist/agent-src/rules/git-history-discipline.md +5 -1
- package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
- package/dist/agent-src/rules/ui-audit-gate.md +9 -4
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +17 -2
- package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
- package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
- package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +4 -4
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
- package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/{chunk-O52B4F5S.js → chunk-3B72LSZL.js} +1 -1
- package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
- package/dist/cli-delegate/{chunk-K34QTSD5.js → chunk-N7FEST6D.js} +17 -17
- package/dist/cli-delegate/{chunk-LKZ5DOYW.js → chunk-XURJ4AQC.js} +11 -3
- package/dist/cli-delegate/cmd_conformance.js +2 -2
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +43 -22
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +936 -346
- package/dist/install/install.mjs +12 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1192 -160
- package/dist/router.json +1 -1
- package/dist/server/schemas/settings.js +2 -2
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/guidelines/design-fidelity-mechanics.md +21 -0
- package/docs/guidelines/design-handover-extraction.md +87 -0
- package/docs/guidelines/prompt-templates.md +24 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +17 -2
- package/src/config/evaluator-budgets.json +5 -4
- package/src/config/gate-coverage.yml +33 -11
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/hook-token-budget.json +8 -1
- package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
- package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
- package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
- package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
- package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
- package/src/scripts/_lib/skill_catalogue.ts +258 -5
- package/src/scripts/_lib/tier_budget_routing.ts +31 -331
- package/src/scripts/ai_council/clients.ts +21 -21
- package/src/scripts/bench_ab_v2_complexity.ts +252 -0
- package/src/scripts/bench_ab_v2_run.ts +3 -42
- package/src/scripts/bench_ab_v2_safety.ts +296 -0
- package/src/scripts/bench_ab_v2_search.ts +343 -0
- package/src/scripts/bench_ab_v2_stats.ts +191 -0
- package/src/scripts/build_archive_index.ts +383 -0
- package/src/scripts/capture_skill_catalogue.ts +107 -2
- package/src/scripts/check_source_size_budget.ts +428 -0
- package/src/scripts/check_suppression_hygiene.ts +5 -1
- package/src/scripts/cost/backfill_rates.mjs +298 -0
- package/src/scripts/cost/budget.mjs +12 -32
- package/src/scripts/cost_summary.ts +25 -0
- package/src/scripts/external_sources_denylist.json +1 -0
- package/src/scripts/hook_manifest.yaml +28 -2
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/skill_route_hook.ts +226 -0
- package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
- package/src/scripts/lint_design_antipattern_parity.ts +40 -6
- package/src/scripts/lint_harvest_provenance.ts +35 -1
- package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
- package/src/scripts/lint_skill_router_head.ts +21 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
- package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
- package/src/scripts/mcp_server/tools.ts +87 -0
- package/src/scripts/release.ts +5 -5
- package/src/scripts/routing_doctor.ts +50 -13
- package/src/scripts/schemas/command.schema.json +1 -1
- package/src/scripts/schemas/skill.schema.json +18 -1
- package/src/scripts/trigger_coverage.ts +224 -3
- package/src/server/schemas/settings.ts +2 -2
- package/src/config/budget-routing.json +0 -7
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Skill route — `user_prompt_submit` concern
|
|
4
|
+
* (road-to-inbox-harvest-2026-08-d-runtime-skill-routing Phase 2).
|
|
5
|
+
*
|
|
6
|
+
* THE DEFECT THIS CLOSES. A deterministic task→skill ranker has shipped in
|
|
7
|
+
* this tree for months and is bound to nothing: `score_skill_relevance.rank`
|
|
8
|
+
* and its `suggest_skill_for_task` wrapper have no production caller, and
|
|
9
|
+
* `hook_manifest.yaml` had no concern that reached either. A capability that
|
|
10
|
+
* ships and never runs is indistinguishable from one that was never built —
|
|
11
|
+
* the same shape `delegation-nudge` was created to end for `classifyTask`, and
|
|
12
|
+
* this concern is deliberately its twin rather than a new mechanism.
|
|
13
|
+
*
|
|
14
|
+
* POINTERS, NEVER BODIES. The injected line names at most three skill IDs and
|
|
15
|
+
* their scores. It does not carry descriptions, excerpts, or file contents, and
|
|
16
|
+
* it never instructs the agent to load anything. The reason is the risk this
|
|
17
|
+
* roadmap's own register ranks first: a pointer line the agent trusts is worse
|
|
18
|
+
* than no line when the ranking is poor, so what is injected is the cheapest
|
|
19
|
+
* thing that can still be acted on — and the cheapest thing to ignore.
|
|
20
|
+
*
|
|
21
|
+
* SILENCE IS THE DEFAULT, AND BOTH FLOORS ARE MEASURED, NOT GUESSED. `rank`
|
|
22
|
+
* already drops every zero-scoring skill, but on real prompts it almost always
|
|
23
|
+
* returns SOMETHING, so "non-empty" is not a usable trigger. Measured over the
|
|
24
|
+
* routing-matrix corpus (496 prompt lines, `tests/eval/routing-matrix/*.yaml`,
|
|
25
|
+
* 2026-08-16): top-score median 18, p75 28, p90 30, max 70, and only 4 prompts
|
|
26
|
+
* score nothing at all.
|
|
27
|
+
*
|
|
28
|
+
* TWO floors are needed, because the score alone cannot express the failure at
|
|
29
|
+
* the short end. `MIN_TOP_SCORE` gates confidence; `MIN_TASK_TERMS` gates the
|
|
30
|
+
* denominator that confidence is computed over — the scorer divides by the task
|
|
31
|
+
* term count, so a one-term prompt scores 70/100 against whatever shares that
|
|
32
|
+
* term. Together they fire on 9.1 % of the corpus. A score floor at the median
|
|
33
|
+
* with no term floor would fire on half of all turns AND still hand 70/100 to
|
|
34
|
+
* `"fix it"` — the per-turn-reminder shape this estate has already measured
|
|
35
|
+
* failing at a 24/29 miss rate. Each constant carries its own derivation below.
|
|
36
|
+
*
|
|
37
|
+
* COST, MEASURED RATHER THAN ASSUMED. Ranking re-reads the skill catalogue per
|
|
38
|
+
* prompt, uncached. Measured 2026-08-16 on this tree: **12.3 ms warm** for a
|
|
39
|
+
* prompt that reaches the ranker, against the slot's 250 ms p95 budget — about
|
|
40
|
+
* 5 % of it. A prompt below `MIN_TASK_TERMS` costs **0 ms**, because the term
|
|
41
|
+
* floor is checked BEFORE the catalogue read; that ordering is deliberate and
|
|
42
|
+
* is what keeps conversational turns free. No cache is added: a cache would
|
|
43
|
+
* need an invalidation story for a tree that changes under the session, and
|
|
44
|
+
* 12.3 ms does not buy one.
|
|
45
|
+
*
|
|
46
|
+
* NO ADOPTION THRESHOLD IS COMMITTED HERE. Whether the line changes behaviour
|
|
47
|
+
* is a question for data, not for this header: `skill_route_pointer_rate` is
|
|
48
|
+
* registered in `hook-token-budget.json` under the same owner/review discipline
|
|
49
|
+
* as its sibling advisories, with the same kill standard — a nudge whose
|
|
50
|
+
* verdicts are measurably ignored gets its trigger tightened or its line
|
|
51
|
+
* removed. The score floor above is a TRIGGER calibration and is measured; the
|
|
52
|
+
* adoption threshold is an OUTCOME claim and is deliberately absent.
|
|
53
|
+
*
|
|
54
|
+
* DELIVERY PATH — the verified one, copied rather than re-derived. A
|
|
55
|
+
* `user_prompt_submit` concern returning `{decision:"allow", context:…}` at
|
|
56
|
+
* exit 0 produces zero stdout: `host_semantics.emitFor` drops the field for
|
|
57
|
+
* severity `"allow"`. The only shape that survives to
|
|
58
|
+
* `hookSpecificOutput.additionalContext` is `{decision:"warn", reason,
|
|
59
|
+
* additional_context}` at exit 2, which `emitFor` reduces to a real process
|
|
60
|
+
* exit of 0 — it never blocks the turn. `delegation_nudge_hook.ts` carries the
|
|
61
|
+
* full proof of that mechanism and this concern follows it exactly.
|
|
62
|
+
*
|
|
63
|
+
* PLATFORM SCOPE — `claude` only, for the same reason its twin gives:
|
|
64
|
+
* `host_semantics.VERIFIED_PLATFORMS` covers only `claude`, `cowork`'s
|
|
65
|
+
* trampoline discards dispatcher stdout unconditionally, and the other
|
|
66
|
+
* trampolines were never inspected for that property. Extending an exit-2
|
|
67
|
+
* delivery to an unverified propagation path is the speculative mapping
|
|
68
|
+
* `host_semantics.ts` exists to prevent.
|
|
69
|
+
*/
|
|
70
|
+
import fs from "node:fs";
|
|
71
|
+
import path from "node:path";
|
|
72
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
73
|
+
|
|
74
|
+
import { readHookStdin } from "./hook_stdin.js";
|
|
75
|
+
import { isSyntheticPrompt } from "../_lib/prompt_shape.js";
|
|
76
|
+
import { resolveSkillsRoot } from "../_lib/skill_catalogue.js";
|
|
77
|
+
import { _tokenize, rank, type RankRow } from "../skill_tools/score_skill_relevance.js";
|
|
78
|
+
|
|
79
|
+
const EXIT_ALLOW = 0;
|
|
80
|
+
/** See the header: reduced to a real exit of 0 by `emitFor`'s warn branch. */
|
|
81
|
+
const EXIT_WARN = 2;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Minimum top-1 ranker score before this concern speaks.
|
|
85
|
+
*
|
|
86
|
+
* **31, not the measured p90 of 30, and the extra point is load-bearing.** The
|
|
87
|
+
* scorer is `overlap * 70 + personaHit * 30`, so a skill whose PERSONA slug
|
|
88
|
+
* appears in the prompt scores exactly 30 with zero keyword overlap. A floor of
|
|
89
|
+
* 30 with a `>=` test therefore admits a pure persona coincidence as if it were
|
|
90
|
+
* a ranked match. 31 is the smallest floor strictly above that value; it moves
|
|
91
|
+
* the corpus fire rate from 13.9 % to 9.1 %.
|
|
92
|
+
*/
|
|
93
|
+
export const MIN_TOP_SCORE = 31;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Minimum distinct task terms before the score is trusted at all.
|
|
97
|
+
*
|
|
98
|
+
* The scorer divides by `|task_terms|`, so the score is inversely proportional
|
|
99
|
+
* to prompt length and degenerates at the short end: `"fix it"` tokenizes to
|
|
100
|
+
* ONE term and hands 70/100 to whichever skills happen to share it, which on
|
|
101
|
+
* this catalogue means the alphabetically first three. `MIN_TOP_SCORE` cannot
|
|
102
|
+
* fix that — the number is not too low, the denominator is too small.
|
|
103
|
+
*
|
|
104
|
+
* **3 is the routing-matrix corpus's own minimum**, so this floor excludes
|
|
105
|
+
* ZERO of the 496 prompt lines the score calibration rests on while removing
|
|
106
|
+
* every degenerate short prompt measured (`"fix it"` 1, `"mach das"` 2,
|
|
107
|
+
* `"was denkst du dazu"` 2, `"weiter"` 1). A floor of 4 would have cost 10 real
|
|
108
|
+
* corpus prompts for one tenth of a percentage point.
|
|
109
|
+
*/
|
|
110
|
+
export const MIN_TASK_TERMS = 3;
|
|
111
|
+
|
|
112
|
+
/** Pointers per line. Three is the ranker's own default `--top`. */
|
|
113
|
+
export const TOP_K = 3;
|
|
114
|
+
|
|
115
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
|
116
|
+
type JsonObject = { [k: string]: JsonValue };
|
|
117
|
+
|
|
118
|
+
function _isObject(v: unknown): v is JsonObject {
|
|
119
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Rank a prompt and return the pointers worth injecting, or `[]` for silence.
|
|
124
|
+
*
|
|
125
|
+
* Total by construction: a missing root, an unreadable catalogue, or a ranker
|
|
126
|
+
* error all resolve to `[]`. A routing advisory that can crash a turn is worse
|
|
127
|
+
* than one that says nothing.
|
|
128
|
+
*/
|
|
129
|
+
export function routePointers(prompt: string, skillsDir: string | null): RankRow[] {
|
|
130
|
+
if (skillsDir === null) return [];
|
|
131
|
+
// Denominator floor BEFORE the catalogue read: a prompt too short to score
|
|
132
|
+
// meaningfully cannot produce a pointer worth 337 file reads either.
|
|
133
|
+
try {
|
|
134
|
+
if (_tokenize(prompt).size < MIN_TASK_TERMS) return [];
|
|
135
|
+
} catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
let rows: RankRow[];
|
|
139
|
+
try {
|
|
140
|
+
rows = rank(prompt, skillsDir);
|
|
141
|
+
} catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
const top = rows[0];
|
|
145
|
+
if (top === undefined || top[1] < MIN_TOP_SCORE) return [];
|
|
146
|
+
return rows.slice(0, TOP_K);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The injected line. Names the floor it cleared, so a reader can tell a
|
|
151
|
+
* confident pointer from a lucky one without opening this file.
|
|
152
|
+
*/
|
|
153
|
+
export function buildRouteLine(rows: readonly RankRow[]): string {
|
|
154
|
+
const pointers = rows.map(([name, score]) => `${name} (${score})`).join(", ");
|
|
155
|
+
return (
|
|
156
|
+
`<skill-route>ranked skill pointers for this prompt: ${pointers}. ` +
|
|
157
|
+
`Deterministic keyword ranker, top-1 at or above ${MIN_TOP_SCORE}/100 — ` +
|
|
158
|
+
`pointers only, no bodies loaded. Open one if it fits; ignore the line if it does not.</skill-route>`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function _extractPrompt(payload: JsonObject): string {
|
|
163
|
+
for (const key of ["prompt", "userPrompt", "user_prompt", "message", "text"]) {
|
|
164
|
+
const v = payload[key];
|
|
165
|
+
if (typeof v === "string" && v.trim()) return v;
|
|
166
|
+
}
|
|
167
|
+
return "";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function _workspaceRoot(envelope: JsonObject): string {
|
|
171
|
+
const v = envelope["workspace_root"];
|
|
172
|
+
return typeof v === "string" && v ? v : process.cwd();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function main(): number {
|
|
176
|
+
let envelope: JsonValue = {};
|
|
177
|
+
try {
|
|
178
|
+
const raw = readHookStdin();
|
|
179
|
+
envelope = raw.trim() ? (JSON.parse(raw) as JsonValue) : {};
|
|
180
|
+
} catch {
|
|
181
|
+
return EXIT_ALLOW; // malformed envelope — never block
|
|
182
|
+
}
|
|
183
|
+
const env: JsonObject = _isObject(envelope) ? envelope : {};
|
|
184
|
+
|
|
185
|
+
const event = env["event"];
|
|
186
|
+
const slot = typeof event === "string" && event !== "" ? event : "user_prompt_submit";
|
|
187
|
+
if (slot !== "user_prompt_submit") return EXIT_ALLOW;
|
|
188
|
+
|
|
189
|
+
const payload = _isObject(env["payload"]) ? (env["payload"] as JsonObject) : env;
|
|
190
|
+
const prompt = _extractPrompt(payload);
|
|
191
|
+
if (!prompt) return EXIT_ALLOW;
|
|
192
|
+
if (isSyntheticPrompt(prompt)) return EXIT_ALLOW; // harness turn, not a task
|
|
193
|
+
|
|
194
|
+
const rows = routePointers(prompt, resolveSkillsRoot(_workspaceRoot(env)));
|
|
195
|
+
if (rows.length === 0) return EXIT_ALLOW; // silence is the default
|
|
196
|
+
|
|
197
|
+
process.stdout.write(
|
|
198
|
+
`${JSON.stringify({
|
|
199
|
+
decision: "warn",
|
|
200
|
+
reason: `skill-route: ${rows.length} pointer(s), top ${rows[0]![1]}/100`,
|
|
201
|
+
additional_context: buildRouteLine(rows),
|
|
202
|
+
})}\n`,
|
|
203
|
+
);
|
|
204
|
+
return EXIT_WARN;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Bundle-safety: never auto-run when inlined into an esbuild bundle, where
|
|
208
|
+
// every module shares the bundle's `import.meta.url`.
|
|
209
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
210
|
+
function _isCliEntry(): boolean {
|
|
211
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== "undefined" && __AGENT_CONFIG_BUNDLE__) return false;
|
|
212
|
+
if (process.argv[1] === undefined) return false;
|
|
213
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
214
|
+
if (import.meta.url === argvUrl) return true;
|
|
215
|
+
try {
|
|
216
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
217
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
218
|
+
return here === argv;
|
|
219
|
+
} catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (_isCliEntry()) {
|
|
225
|
+
process.exit(main());
|
|
226
|
+
}
|
|
@@ -30,9 +30,12 @@
|
|
|
30
30
|
* degradation valve `design_slop_hook` carries, for the same reason: an agent
|
|
31
31
|
* that has decided to proceed must not be re-prompted into a loop.
|
|
32
32
|
*
|
|
33
|
-
* HOST BOUNDARY, STATED RATHER THAN IMPLIED. `pre_tool_use`
|
|
34
|
-
* hosts. Everywhere else
|
|
35
|
-
*
|
|
33
|
+
* HOST BOUNDARY, STATED RATHER THAN IMPLIED. `pre_tool_use` is BOUND on three
|
|
34
|
+
* hosts. Everywhere else the obligation stays model-carried, exactly as the two
|
|
35
|
+
* rules already declare. Corrected 2026-08-17: this comment said the concern has
|
|
36
|
+
* "nowhere to bind" elsewhere, which the manifest's own `native_event_aliases`
|
|
37
|
+
* table refutes for cursor, cline and gemini — unbound, not unbindable. See
|
|
38
|
+
* `docs/contracts/hook-architecture-v1.md` § Which hosts carry pre_tool_use. Run
|
|
36
39
|
* `agent-config hooks:status` for the host you are actually on.
|
|
37
40
|
*
|
|
38
41
|
* Default-OFF: no-ops unless `hooks.ui_route_nudge.enabled: true`.
|
|
@@ -29,6 +29,7 @@ import * as fs from 'node:fs';
|
|
|
29
29
|
import * as path from 'node:path';
|
|
30
30
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
31
|
|
|
32
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
32
33
|
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
33
34
|
|
|
34
35
|
const QUIET = process.argv.includes('--quiet');
|
|
@@ -44,6 +45,14 @@ export type Status = (typeof STATUSES)[number];
|
|
|
44
45
|
export interface ParityFinding {
|
|
45
46
|
kind: string;
|
|
46
47
|
msg: string;
|
|
48
|
+
/**
|
|
49
|
+
* The catalog id this finding is about, when it is about one.
|
|
50
|
+
*
|
|
51
|
+
* Carried explicitly so the per-target ledger can attribute a failure
|
|
52
|
+
* without parsing it back out of `msg`. Absent on structural findings
|
|
53
|
+
* (a missing § Detector status section is about the document, not an id).
|
|
54
|
+
*/
|
|
55
|
+
id?: string;
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
const ENTRY_ROW = /^\|\s*((?:V|C|T|L|M|CP)\d+)\s*\|/gm;
|
|
@@ -89,7 +98,7 @@ export function parityFindings(catalogMd: string, registryTs: string): ParityFin
|
|
|
89
98
|
const entries = catalogEntries(catalogMd);
|
|
90
99
|
const seen = new Set<string>();
|
|
91
100
|
for (const id of entries) {
|
|
92
|
-
if (seen.has(id)) out.push({ kind: 'duplicate-entry', msg: `${id}: appears more than once in the catalog tables` });
|
|
101
|
+
if (seen.has(id)) out.push({ kind: 'duplicate-entry', id, msg: `${id}: appears more than once in the catalog tables` });
|
|
93
102
|
seen.add(id);
|
|
94
103
|
}
|
|
95
104
|
|
|
@@ -98,13 +107,13 @@ export function parityFindings(catalogMd: string, registryTs: string): ParityFin
|
|
|
98
107
|
// A — every catalog entry is classified.
|
|
99
108
|
for (const id of seen) {
|
|
100
109
|
if (!rows.has(id)) {
|
|
101
|
-
out.push({ kind: 'unclassified', msg: `${id}: catalog entry missing from § Detector status` });
|
|
110
|
+
out.push({ kind: 'unclassified', id, msg: `${id}: catalog entry missing from § Detector status` });
|
|
102
111
|
}
|
|
103
112
|
}
|
|
104
113
|
// B — every classified row names a real entry.
|
|
105
114
|
for (const id of rows.keys()) {
|
|
106
115
|
if (!seen.has(id)) {
|
|
107
|
-
out.push({ kind: 'orphan-status', msg: `${id}: § Detector status row names an entry that no catalog table defines` });
|
|
116
|
+
out.push({ kind: 'orphan-status', id, msg: `${id}: § Detector status row names an entry that no catalog table defines` });
|
|
108
117
|
}
|
|
109
118
|
}
|
|
110
119
|
// D + E — status vocabulary and reasons.
|
|
@@ -112,12 +121,13 @@ export function parityFindings(catalogMd: string, registryTs: string): ParityFin
|
|
|
112
121
|
if (!(STATUSES as readonly string[]).includes(row.status)) {
|
|
113
122
|
out.push({
|
|
114
123
|
kind: 'bad-status',
|
|
124
|
+
id,
|
|
115
125
|
msg: `${id}: status '${row.status || '(empty)'}' is not one of ${STATUSES.join(', ')}`,
|
|
116
126
|
});
|
|
117
127
|
continue;
|
|
118
128
|
}
|
|
119
129
|
if (row.status !== 'backed' && row.note === '') {
|
|
120
|
-
out.push({ kind: 'missing-reason', msg: `${id}: status '${row.status}' carries no reason` });
|
|
130
|
+
out.push({ kind: 'missing-reason', id, msg: `${id}: status '${row.status}' carries no reason` });
|
|
121
131
|
}
|
|
122
132
|
}
|
|
123
133
|
|
|
@@ -126,7 +136,7 @@ export function parityFindings(catalogMd: string, registryTs: string): ParityFin
|
|
|
126
136
|
const registry = new Set(registryCatalogIds(registryTs));
|
|
127
137
|
for (const id of backed) {
|
|
128
138
|
if (!registry.has(id)) {
|
|
129
|
-
out.push({ kind: 'backed-without-rule', msg: `${id}: marked backed but no registry rule declares that catalogId` });
|
|
139
|
+
out.push({ kind: 'backed-without-rule', id, msg: `${id}: marked backed but no registry rule declares that catalogId` });
|
|
130
140
|
}
|
|
131
141
|
}
|
|
132
142
|
for (const id of registry) {
|
|
@@ -134,6 +144,7 @@ export function parityFindings(catalogMd: string, registryTs: string): ParityFin
|
|
|
134
144
|
const known = rows.get(id);
|
|
135
145
|
out.push({
|
|
136
146
|
kind: 'rule-without-backed',
|
|
147
|
+
id,
|
|
137
148
|
msg: known
|
|
138
149
|
? `${id}: a registry rule declares this catalogId but § Detector status marks it '${known.status}'`
|
|
139
150
|
: `${id}: a registry rule declares this catalogId and no catalog entry carries it`,
|
|
@@ -167,8 +178,31 @@ export function main(): number {
|
|
|
167
178
|
const registryTs = fs.readFileSync(REGISTRY, 'utf-8');
|
|
168
179
|
const findings = parityFindings(catalogMd, registryTs);
|
|
169
180
|
|
|
181
|
+
const rows = statusRows(catalogMd);
|
|
170
182
|
const entries = catalogEntries(catalogMd).length;
|
|
171
|
-
const backed = [...
|
|
183
|
+
const backed = [...rows.values()].filter((r) => r.status === 'backed').length;
|
|
184
|
+
|
|
185
|
+
// Per-target completeness accounting. The target is a catalog ID, and the
|
|
186
|
+
// planned set is the UNION of the three places an id can appear — catalog
|
|
187
|
+
// tables, the § Detector status table, and the rule registry — because this
|
|
188
|
+
// gate's whole subject is parity BETWEEN those sets. Planning only the
|
|
189
|
+
// catalog would leave an orphan status row or a registry-only id
|
|
190
|
+
// unaccounted, which is the direction the gate exists to catch.
|
|
191
|
+
const ledger = new GateLedger('lint_design_antipattern_parity');
|
|
192
|
+
const targets = [
|
|
193
|
+
...new Set([
|
|
194
|
+
...catalogEntries(catalogMd),
|
|
195
|
+
...rows.keys(),
|
|
196
|
+
...registryCatalogIds(registryTs),
|
|
197
|
+
]),
|
|
198
|
+
].sort();
|
|
199
|
+
ledger.plan(targets);
|
|
200
|
+
const failedIds = new Set(findings.map((f) => f.id).filter((v): v is string => v !== undefined));
|
|
201
|
+
for (const id of targets) {
|
|
202
|
+
if (failedIds.has(id)) ledger.fail(id, 'parity finding');
|
|
203
|
+
else ledger.complete(id);
|
|
204
|
+
}
|
|
205
|
+
ledger.report();
|
|
172
206
|
|
|
173
207
|
if (findings.length === 0) {
|
|
174
208
|
_print(`✅ catalog ↔ rule parity OK — ${entries} entries classified, ${backed} detector-backed`);
|
|
@@ -49,6 +49,7 @@ import * as fs from 'node:fs';
|
|
|
49
49
|
import * as path from 'node:path';
|
|
50
50
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
51
51
|
|
|
52
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
52
53
|
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
53
54
|
|
|
54
55
|
const _FILE = fileURLToPath(import.meta.url);
|
|
@@ -432,7 +433,40 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
432
433
|
const text = fs.readFileSync(ledgerPath, 'utf-8');
|
|
433
434
|
const { records, findings } = lintLedgerText(text, REPO);
|
|
434
435
|
const citations = [...collectCitations(REPO), ...collectPersonaSources(REPO)];
|
|
435
|
-
const
|
|
436
|
+
const orphans = findOrphanCitations(citations, records);
|
|
437
|
+
const allFindings = [...findings, ...orphans];
|
|
438
|
+
|
|
439
|
+
// Per-target completeness accounting over the two populations this gate
|
|
440
|
+
// reasons about: the ledger's own rows, and the citations that point at
|
|
441
|
+
// them. Both are legitimately EMPTY today, and that is exactly why the
|
|
442
|
+
// accounting is worth having — a green line over zero rows and a green line
|
|
443
|
+
// over a hundred checked rows read identically without it, which is the
|
|
444
|
+
// failure the success message above already tries to talk its way out of in
|
|
445
|
+
// prose. The ledger states it structurally instead.
|
|
446
|
+
const ledger = new GateLedger('lint_harvest_provenance');
|
|
447
|
+
const known = new Set(records.map((r) => r.harvest_id));
|
|
448
|
+
// The row population is every PARSED ledger line, not `records`: a row that
|
|
449
|
+
// fails validation or uniqueness is `continue`d and never reaches `records`,
|
|
450
|
+
// so planning `records` would plan exactly the rows that passed and account
|
|
451
|
+
// for none of the ones that did not — an accounting that can only ever read
|
|
452
|
+
// 100 %. Lines are the stable key; a Finding already carries one.
|
|
453
|
+
const { parsed } = parseLedgerText(text);
|
|
454
|
+
const badLines = new Set(findings.filter((f) => f.line > 0).map((f) => f.line));
|
|
455
|
+
ledger.plan([
|
|
456
|
+
...parsed.map((r) => `row:${String(r.line)}`),
|
|
457
|
+
...citations.map((c) => `citation:${c.id}@${c.file}`),
|
|
458
|
+
]);
|
|
459
|
+
for (const r of parsed) {
|
|
460
|
+
const id = `row:${String(r.line)}`;
|
|
461
|
+
if (badLines.has(r.line)) ledger.fail(id, 'ledger-row finding');
|
|
462
|
+
else ledger.complete(id);
|
|
463
|
+
}
|
|
464
|
+
for (const c of citations) {
|
|
465
|
+
const id = `citation:${c.id}@${c.file}`;
|
|
466
|
+
if (known.has(c.id)) ledger.complete(id);
|
|
467
|
+
else ledger.fail(id, 'orphan citation — no ledger row carries this harvest id');
|
|
468
|
+
}
|
|
469
|
+
ledger.report();
|
|
436
470
|
|
|
437
471
|
if (allFindings.length > 0) {
|
|
438
472
|
for (const f of allFindings) {
|
|
@@ -66,6 +66,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
66
66
|
|
|
67
67
|
import { parse as parseYaml } from 'yaml';
|
|
68
68
|
|
|
69
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
69
70
|
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
70
71
|
|
|
71
72
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -404,6 +405,48 @@ function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
|
404
405
|
const errors = findings.filter((f) => f.kind === 'unreachable-route');
|
|
405
406
|
const advisories = findings.filter((f) => f.kind === 'unrouted-skill');
|
|
406
407
|
|
|
408
|
+
// Per-target completeness accounting over BOTH populations this gate
|
|
409
|
+
// reasons about, id-prefixed so they stay distinguishable in one tally.
|
|
410
|
+
// The skips are the point: check 1 walks past every unscoped rule and
|
|
411
|
+
// check 2 past every unscoped skill, because "ships everywhere" makes the
|
|
412
|
+
// reachability question inapplicable rather than satisfied. Without the
|
|
413
|
+
// ledger those walk-pasts were indistinguishable from checks that passed —
|
|
414
|
+
// the exact "checked 20 of 300, printed one green line" shape.
|
|
415
|
+
//
|
|
416
|
+
// This tally is deliberately NOT the `scanned:` line below. That line is
|
|
417
|
+
// the machine-read coverage contract and stays the rule count; this is the
|
|
418
|
+
// human-auditable breakdown beside it, per `_lib/gate_ledger` § report.
|
|
419
|
+
const ledger = new GateLedger('lint_rule_skill_pack_reach');
|
|
420
|
+
const routedSkills = [...new Set(rules.flatMap((r) => r.routesToSkills))]
|
|
421
|
+
.filter((sk) => skillPacks.has(sk))
|
|
422
|
+
.sort();
|
|
423
|
+
ledger.plan([
|
|
424
|
+
...rules.map((r) => `rule:${r.name}`),
|
|
425
|
+
...routedSkills.map((sk) => `skill:${sk}`),
|
|
426
|
+
]);
|
|
427
|
+
const failedSubjects = new Set(findings.map((f) => f.subject));
|
|
428
|
+
for (const rule of rules) {
|
|
429
|
+
const id = `rule:${rule.name}`;
|
|
430
|
+
if (rule.packs.length === 0) {
|
|
431
|
+
ledger.skip(id, 'not_applicable_kind');
|
|
432
|
+
} else if ([...failedSubjects].some((sub) => sub.startsWith(`${rule.name} → skill:`))) {
|
|
433
|
+
ledger.fail(id, 'unreachable-route');
|
|
434
|
+
} else {
|
|
435
|
+
ledger.complete(id);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
for (const sk of routedSkills) {
|
|
439
|
+
const id = `skill:${sk}`;
|
|
440
|
+
if ((skillPacks.get(sk) ?? []).length === 0) {
|
|
441
|
+
ledger.skip(id, 'not_applicable_kind');
|
|
442
|
+
} else if (failedSubjects.has(id)) {
|
|
443
|
+
ledger.fail(id, 'unrouted-skill');
|
|
444
|
+
} else {
|
|
445
|
+
ledger.complete(id);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
ledger.report();
|
|
449
|
+
|
|
407
450
|
// --quiet suppresses the per-finding lines but never the count line: this
|
|
408
451
|
// gate is expected to stay non-empty for a while, and a quiet run that
|
|
409
452
|
// printed nothing at all would read as clean.
|
|
@@ -38,6 +38,7 @@ import * as fs from 'node:fs';
|
|
|
38
38
|
import * as path from 'node:path';
|
|
39
39
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
40
40
|
|
|
41
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
41
42
|
import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
|
|
42
43
|
|
|
43
44
|
const _FILE = fileURLToPath(import.meta.url);
|
|
@@ -204,6 +205,26 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
204
205
|
const heads = collectSkillHeads(path.join(REPO, SKILLS_REL));
|
|
205
206
|
const { findings, staleAllowlist } = evaluate(heads);
|
|
206
207
|
|
|
208
|
+
// Per-target completeness accounting. The interesting outcome here is the
|
|
209
|
+
// SKIP: a grandfathered head is over the cap and deliberately unchecked, so
|
|
210
|
+
// without a ledger it was indistinguishable from a head that passed. The
|
|
211
|
+
// allowlist size is already printed, but only the ledger ties each skipped
|
|
212
|
+
// name to the reason it went unchecked.
|
|
213
|
+
const ledger = new GateLedger('lint_skill_router_head');
|
|
214
|
+
ledger.plan(heads.map((h) => h.name));
|
|
215
|
+
const failed = new Set(findings.map((f) => f.skill));
|
|
216
|
+
const allow = new Set(GRANDFATHERED);
|
|
217
|
+
for (const h of heads) {
|
|
218
|
+
if (failed.has(h.name)) {
|
|
219
|
+
ledger.fail(h.name, `${String(h.lines)} lines over the ${String(MAX_HEAD_LINES)}-line cap with no mode bodies`);
|
|
220
|
+
} else if (h.lines > MAX_HEAD_LINES && allow.has(h.name)) {
|
|
221
|
+
ledger.skip(h.name, 'declared_exemption');
|
|
222
|
+
} else {
|
|
223
|
+
ledger.complete(h.name);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
ledger.report();
|
|
227
|
+
|
|
207
228
|
for (const s of staleAllowlist) {
|
|
208
229
|
process.stderr.write(
|
|
209
230
|
`⚠️ lint_skill_router_head: '${s}' is grandfathered but no longer over the cap — remove it from GRANDFATHERED so the ratchet keeps its teeth\n`,
|
|
@@ -544,10 +544,33 @@
|
|
|
544
544
|
},
|
|
545
545
|
{
|
|
546
546
|
"name": "suggest_skill_for_task",
|
|
547
|
-
"description": "
|
|
547
|
+
"description": "Match a free-form task description to the most relevant skills, ranked by a deterministic keyword scorer over SKILL.md frontmatter. Use when a skill you need is not in the catalogue the host delivered — a measured host dropped 402 entries from its model-visible list — so asking by name is impossible while asking by task is not. Read-only: no shell, no writes, and no skill bodies are returned, only names, scores and declared personas.",
|
|
548
548
|
"side_effect": "ro",
|
|
549
|
-
"implemented_on": [
|
|
550
|
-
|
|
549
|
+
"implemented_on": [
|
|
550
|
+
"stdio"
|
|
551
|
+
],
|
|
552
|
+
"input_schema": {
|
|
553
|
+
"type": "object",
|
|
554
|
+
"required": [
|
|
555
|
+
"task"
|
|
556
|
+
],
|
|
557
|
+
"properties": {
|
|
558
|
+
"task": {
|
|
559
|
+
"type": "string",
|
|
560
|
+
"description": "Free-form description of the task to match skills against."
|
|
561
|
+
},
|
|
562
|
+
"limit": {
|
|
563
|
+
"type": "integer",
|
|
564
|
+
"minimum": 1,
|
|
565
|
+
"default": 5,
|
|
566
|
+
"description": "Maximum number of skills to return. Defaults to 5."
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
"additionalProperties": false
|
|
570
|
+
},
|
|
571
|
+
"annotations": {
|
|
572
|
+
"readOnlyHint": true
|
|
573
|
+
}
|
|
551
574
|
},
|
|
552
575
|
{
|
|
553
576
|
"name": "sync_agent_settings",
|
|
@@ -96,30 +96,12 @@ export const STUB_TOOLS: readonly StubToolSource[] = [
|
|
|
96
96
|
additionalProperties: false,
|
|
97
97
|
},
|
|
98
98
|
},
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
'Returns a ranked list of skills with name, description, and ' +
|
|
106
|
-
'match reason.',
|
|
107
|
-
side_effect: 'ro',
|
|
108
|
-
input_schema: {
|
|
109
|
-
type: 'object',
|
|
110
|
-
properties: {
|
|
111
|
-
task: { type: 'string', description: 'Free-form description of the task to find a skill for.' },
|
|
112
|
-
limit: {
|
|
113
|
-
type: 'integer',
|
|
114
|
-
minimum: 1,
|
|
115
|
-
default: 5,
|
|
116
|
-
description: 'Maximum number of skills to return. Defaults to 5.',
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
required: ['task'],
|
|
120
|
-
additionalProperties: false,
|
|
121
|
-
},
|
|
122
|
-
},
|
|
99
|
+
// `suggest_skill_for_task` moved to ALLOWLIST in mcp_server/tools.ts
|
|
100
|
+
// (road-to-inbox-harvest-2026-08-d-runtime-skill-routing 4.1) — it is
|
|
101
|
+
// implemented now, and `build_mcp_catalog` refuses a name registered in
|
|
102
|
+
// both places. Its published input schema (`task` + `limit`) was carried
|
|
103
|
+
// over verbatim rather than re-designed: the stub is a contract consumers
|
|
104
|
+
// may already have read.
|
|
123
105
|
{
|
|
124
106
|
name: 'mine_session',
|
|
125
107
|
description:
|
|
@@ -1210,7 +1210,94 @@ async function _readResourceBodyHandler(
|
|
|
1210
1210
|
// unlisted tool. Boot-time stderr log enumerates the registered set.
|
|
1211
1211
|
// ---------------------------------------------------------------------
|
|
1212
1212
|
|
|
1213
|
+
/**
|
|
1214
|
+
* `suggest_skill_for_task` — the recovery path for a truncated catalogue.
|
|
1215
|
+
*
|
|
1216
|
+
* Promoted from a discovery stub because the defect it answers is measured, not
|
|
1217
|
+
* hypothetical: on the one host that publishes its own truncation, a default
|
|
1218
|
+
* install had 402 entries dropped from the model-visible skills list — the
|
|
1219
|
+
* host's own count against its own denominator, not a subtraction over ours
|
|
1220
|
+
* (`agents/evidence/analysis/scoped-projection-host-delivery.md`). An agent
|
|
1221
|
+
* that cannot see a skill also cannot ask for it by name, so the only reachable
|
|
1222
|
+
* question is "what fits this task" — which is what the deterministic ranker
|
|
1223
|
+
* answers, over the TREE rather than over the host's catalogue.
|
|
1224
|
+
*
|
|
1225
|
+
* Read-only and shell-free by construction: it reads SKILL.md frontmatter and
|
|
1226
|
+
* returns names, scores and declared personas. No skill BODY is returned — the
|
|
1227
|
+
* caller opens what it picked, so a wrong rank costs one read rather than a
|
|
1228
|
+
* context payload.
|
|
1229
|
+
*/
|
|
1230
|
+
async function _suggestSkillForTaskHandler(
|
|
1231
|
+
args: Record<string, unknown>,
|
|
1232
|
+
consumerRoot: string,
|
|
1233
|
+
): Promise<Record<string, unknown>> {
|
|
1234
|
+
const task = typeof args['task'] === 'string' ? args['task'] : '';
|
|
1235
|
+
if (task.trim() === '') {
|
|
1236
|
+
return { status: 'error', error: 'task must be a non-empty string', suggestions: [] };
|
|
1237
|
+
}
|
|
1238
|
+
// `limit`, not `top` — the stub published `limit` and a consumer may have
|
|
1239
|
+
// read that contract already. Renaming it on promotion would break a
|
|
1240
|
+
// caller for cosmetic reasons.
|
|
1241
|
+
const limitRaw = args['limit'];
|
|
1242
|
+
const limit =
|
|
1243
|
+
typeof limitRaw === 'number' && Number.isFinite(limitRaw) ? Math.floor(limitRaw) : 5;
|
|
1244
|
+
|
|
1245
|
+
const catalogue = await import('../_lib/skill_catalogue.js');
|
|
1246
|
+
const skillsDir = catalogue.resolveSkillsRoot(consumerRoot);
|
|
1247
|
+
if (skillsDir === null) {
|
|
1248
|
+
// A missing catalogue is UNKNOWN, never "no skill fits this task". The
|
|
1249
|
+
// second reads as a ranked answer and would be a confident empty list —
|
|
1250
|
+
// the same zero-from-silence failure `capture_skill_catalogue` refuses.
|
|
1251
|
+
return {
|
|
1252
|
+
status: 'no_catalogue',
|
|
1253
|
+
searched: [...catalogue.DEFAULT_CATALOGUE_ROOTS],
|
|
1254
|
+
consumer_root: consumerRoot,
|
|
1255
|
+
suggestions: [],
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const { rank } = await import('../skill_tools/score_skill_relevance.js');
|
|
1260
|
+
const rows = rank(task, skillsDir).slice(0, Math.max(1, limit));
|
|
1261
|
+
return {
|
|
1262
|
+
status: 'ok',
|
|
1263
|
+
skills_root: skillsDir,
|
|
1264
|
+
suggestions: rows.map(([name, score, personas]) => ({ skill: name, score, personas })),
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1213
1268
|
export const ALLOWLIST: Record<string, BuiltinTool> = {
|
|
1269
|
+
suggest_skill_for_task: {
|
|
1270
|
+
name: 'suggest_skill_for_task',
|
|
1271
|
+
side_effect: 'ro',
|
|
1272
|
+
description:
|
|
1273
|
+
'Match a free-form task description to the most relevant skills, ' +
|
|
1274
|
+
'ranked by a deterministic keyword scorer over SKILL.md ' +
|
|
1275
|
+
'frontmatter. Use when a skill you need is not in the catalogue ' +
|
|
1276
|
+
'the host delivered — a measured host dropped 402 entries from ' +
|
|
1277
|
+
'its model-visible list — so asking by name is impossible while ' +
|
|
1278
|
+
'asking by task is not. ' +
|
|
1279
|
+
'Read-only: no shell, no writes, and no skill bodies are returned, ' +
|
|
1280
|
+
'only names, scores and declared personas.',
|
|
1281
|
+
input_schema: {
|
|
1282
|
+
type: 'object',
|
|
1283
|
+
required: ['task'],
|
|
1284
|
+
properties: {
|
|
1285
|
+
task: {
|
|
1286
|
+
type: 'string',
|
|
1287
|
+
description:
|
|
1288
|
+
'Free-form description of the task to match skills against.',
|
|
1289
|
+
},
|
|
1290
|
+
limit: {
|
|
1291
|
+
type: 'integer',
|
|
1292
|
+
minimum: 1,
|
|
1293
|
+
default: 5,
|
|
1294
|
+
description: 'Maximum number of skills to return. Defaults to 5.',
|
|
1295
|
+
},
|
|
1296
|
+
},
|
|
1297
|
+
additionalProperties: false,
|
|
1298
|
+
},
|
|
1299
|
+
handler: _suggestSkillForTaskHandler,
|
|
1300
|
+
},
|
|
1214
1301
|
lint_skills: {
|
|
1215
1302
|
name: 'lint_skills',
|
|
1216
1303
|
side_effect: 'ro',
|