@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
package/src/scripts/release.ts
CHANGED
|
@@ -879,14 +879,14 @@ function watch_pr_checks(branch: string): void {
|
|
|
879
879
|
*
|
|
880
880
|
* git's wording for this is stable across versions: the hint is `fetch first`
|
|
881
881
|
* / `Updates were rejected`, and the ref line carries `[rejected]` with
|
|
882
|
-
* `non-fast-forward` or `fetch first`.
|
|
882
|
+
* `non-fast-forward` or `fetch first`. A stale lease is a SECOND wording —
|
|
883
|
+
* `[remote rejected]` + `cannot lock ref … but expected`; pinned in the tests.
|
|
883
884
|
*/
|
|
884
885
|
export function _is_non_fast_forward(stderr: string, stdout: string): boolean {
|
|
885
886
|
const text = `${stderr}\n${stdout}`;
|
|
886
|
-
return
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
);
|
|
887
|
+
if (!/\[(?:remote )?rejected\]/i.test(text)) return false;
|
|
888
|
+
if (/cannot lock ref[^\n]*but expected/i.test(text)) return true;
|
|
889
|
+
return /(non-fast-forward|fetch first|behind its remote counterpart)/i.test(text);
|
|
890
890
|
}
|
|
891
891
|
|
|
892
892
|
function push_release_branch(branch: string): void {
|
|
@@ -63,7 +63,7 @@ import {
|
|
|
63
63
|
classifyTask,
|
|
64
64
|
type ActivationInputs,
|
|
65
65
|
} from "./_lib/auto_dispatch.js";
|
|
66
|
-
import { TIER_ORDER, readCooldowns } from "./_lib/tier_budget_routing.js";
|
|
66
|
+
import { COOLDOWN_FILE, TIER_ORDER, readCooldowns } from "./_lib/tier_budget_routing.js";
|
|
67
67
|
import {
|
|
68
68
|
instructionsLoadedRecord,
|
|
69
69
|
measureStandingDelivery,
|
|
@@ -111,6 +111,13 @@ export interface OrchestrationReport {
|
|
|
111
111
|
downshift: boolean;
|
|
112
112
|
/** cost.budgets.per_tier ceilings + live cool-down state per tier. */
|
|
113
113
|
tier_budgets: Record<string, { ceiling_usd: number | null; cooldown_until_ms: number }>;
|
|
114
|
+
/**
|
|
115
|
+
* Whether the cool-down store exists on disk. `readCooldowns` returns all
|
|
116
|
+
* zeros both when no tier is cooling AND when nothing has ever written the
|
|
117
|
+
* file, so a bare zero cannot distinguish "measured: live" from "no reading
|
|
118
|
+
* exists". This flag is what separates them.
|
|
119
|
+
*/
|
|
120
|
+
cooldown_store_present: boolean;
|
|
114
121
|
host_manifest: HostCapabilityManifest;
|
|
115
122
|
/**
|
|
116
123
|
* The host identifier the capability registry was keyed on, and whether it
|
|
@@ -158,14 +165,23 @@ export interface OrchestrationReport {
|
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
168
|
+
* Tier-decision coverage in the orchestration record.
|
|
169
|
+
*
|
|
170
|
+
* REPOINTED 2026-08-16. This used to check "delivery evidence" for budget-aware
|
|
171
|
+
* tier routing, on the reasoning that policy can be present while delivery is
|
|
172
|
+
* silently absent — the session-canary blind-spot class. That layer is now
|
|
173
|
+
* ARCHIVED (`docs/contracts/budget-routing.md`), so the old rationale no longer
|
|
174
|
+
* holds and the old warning would have been permanently true and un-actionable:
|
|
175
|
+
* nothing dispatches through a tier decision by design, so telling the operator
|
|
176
|
+
* "the relation may not be running" is a dead advisory of exactly the shape this
|
|
177
|
+
* repository keeps removing elsewhere.
|
|
178
|
+
*
|
|
179
|
+
* What it measures now is narrower and still worth having: how many recorded
|
|
180
|
+
* dispatches carry a `tier` at all. That is the SAME field the archived layer's
|
|
181
|
+
* revisit-if depends on — a saving can only be computed once both the chosen and
|
|
182
|
+
* the realized tier are recorded — so this is the instrument that would show the
|
|
183
|
+
* reopen condition arriving. It reports coverage rather than warning about it.
|
|
184
|
+
* Read-only over agents/runtime/state/audit/*.jsonl; no new state.
|
|
169
185
|
*/
|
|
170
186
|
export function check_budget_delivery(
|
|
171
187
|
workspace_root: string,
|
|
@@ -202,9 +218,10 @@ export function check_budget_delivery(
|
|
|
202
218
|
}
|
|
203
219
|
if (out.eligible_dispatches > 0 && out.budget_evidence_lines === 0) {
|
|
204
220
|
out.warning =
|
|
205
|
-
`
|
|
206
|
-
|
|
207
|
-
`the
|
|
221
|
+
`tier-decision coverage is 0 of ${out.eligible_dispatches} recorded dispatch(es) — ` +
|
|
222
|
+
`expected while budget routing stays archived (docs/contracts/budget-routing.md). ` +
|
|
223
|
+
`This is the counter its revisit-if reads: a non-zero value here is the signal ` +
|
|
224
|
+
`that per-tier dispatch has reappeared and the decision can be reopened`;
|
|
208
225
|
}
|
|
209
226
|
return out;
|
|
210
227
|
}
|
|
@@ -356,6 +373,7 @@ export function collect_orchestration(
|
|
|
356
373
|
const ledger_present = fs.existsSync(path.join(tracking_dir, "sessions.jsonl"));
|
|
357
374
|
const per_tier = (cost_budgets["per_tier"] ?? {}) as Record<string, unknown>;
|
|
358
375
|
const cooldowns = readCooldowns(tracking_dir);
|
|
376
|
+
const cooldown_store_present = fs.existsSync(path.join(tracking_dir, COOLDOWN_FILE));
|
|
359
377
|
const tier_budgets: OrchestrationReport["tier_budgets"] = {};
|
|
360
378
|
for (const t of TIER_ORDER) {
|
|
361
379
|
const v = per_tier[t];
|
|
@@ -382,6 +400,7 @@ export function collect_orchestration(
|
|
|
382
400
|
halted,
|
|
383
401
|
downshift,
|
|
384
402
|
tier_budgets,
|
|
403
|
+
cooldown_store_present,
|
|
385
404
|
host_manifest,
|
|
386
405
|
host_platform: platform,
|
|
387
406
|
host_platform_assumed: platform_assumed,
|
|
@@ -636,10 +655,28 @@ function _render(report: DoctorReport): string {
|
|
|
636
655
|
);
|
|
637
656
|
const tierBits = Object.entries(o.tier_budgets).map(([t, s]) => {
|
|
638
657
|
const cap = s.ceiling_usd === null ? "no cap" : `$${s.ceiling_usd}`;
|
|
639
|
-
|
|
658
|
+
// `> Date.now()`, not `> 0`. The purger went with the archived permit
|
|
659
|
+
// lifecycle (`tripCooldown` rewrote the map on every write), so a stale
|
|
660
|
+
// entry now has nothing to clear it and `> 0` would render COOLING for
|
|
661
|
+
// ever. Latent rather than live — there is no writer either — but the
|
|
662
|
+
// comparison should be the one that stays correct if a writer returns.
|
|
663
|
+
const cool = s.cooldown_until_ms > Date.now() ? " COOLING" : "";
|
|
640
664
|
return `${t}=${cap}${cool}`;
|
|
641
665
|
});
|
|
642
666
|
lines.push(` tier budgets (rolling-24h): ${tierBits.join(" · ")}`);
|
|
667
|
+
// `readCooldowns` returns all zeros both when no tier is cooling and when
|
|
668
|
+
// nothing has ever written the store, so a missing COOLING marker alone
|
|
669
|
+
// cannot be read as a measured "this tier is live". Report which of the two
|
|
670
|
+
// it is — the same unavailable-vs-false distinction the capability-
|
|
671
|
+
// provenance line above makes two lines earlier. Derived from the store's
|
|
672
|
+
// existence rather than from a caller count, so it stays true if the
|
|
673
|
+
// cool-down producer is ever wired.
|
|
674
|
+
if (!o.cooldown_store_present) {
|
|
675
|
+
lines.push(
|
|
676
|
+
` cool-down state: no reading on disk (${COOLDOWN_FILE} absent) — ` +
|
|
677
|
+
`absence of COOLING above is not evidence of a live tier`,
|
|
678
|
+
);
|
|
679
|
+
}
|
|
643
680
|
if (o.delivery.warning) {
|
|
644
681
|
lines.push(` ⚠️ ${o.delivery.warning}`);
|
|
645
682
|
} else if (o.delivery.eligible_dispatches > 0) {
|
|
@@ -254,7 +254,7 @@
|
|
|
254
254
|
},
|
|
255
255
|
"requires_confirmation": {
|
|
256
256
|
"type": "boolean",
|
|
257
|
-
"description": "Optional, additive: the command's action STAGES rather than executes, so an irreversible step waits for an explicit human confirmation naming the exact object (non-destructive-by-default § Never act while asking). Twin of the same key in skill.schema.json's execution block; commands carry no execution block, so it sits top-level. A declaration only — it names the action class, never the object, which exists at stage time and not in frontmatter. Mechanism: stageAction/confirmOnce in work_engine/hooks/builtin/staged_confirmation.ts; enumerate with `agent-config gates --pending`. Nothing BINDS on it yet (blocker confirmation-degraded-host-semantics:
|
|
257
|
+
"description": "Optional, additive: the command's action STAGES rather than executes, so an irreversible step waits for an explicit human confirmation naming the exact object (non-destructive-by-default § Never act while asking). Twin of the same key in skill.schema.json's execution block; commands carry no execution block, so it sits top-level. A declaration only — it names the action class, never the object, which exists at stage time and not in frontmatter. Mechanism: stageAction/confirmOnce in work_engine/hooks/builtin/staged_confirmation.ts; enumerate with `agent-config gates --pending`. Nothing BINDS on it yet (blocker confirmation-degraded-host-semantics: only claude both binds pre_tool_use and honours a deny — docs/contracts/hook-architecture-v1.md, section Which hosts carry pre_tool_use)."
|
|
258
258
|
},
|
|
259
259
|
"auto_detect": {
|
|
260
260
|
"type": "boolean",
|
|
@@ -276,7 +276,7 @@
|
|
|
276
276
|
},
|
|
277
277
|
"requires_confirmation": {
|
|
278
278
|
"type": "boolean",
|
|
279
|
-
"description": "Optional, additive: the skill's action STAGES rather than executes, so an irreversible step waits for an explicit human confirmation naming the exact object (non-destructive-by-default § Never act while asking). A declaration only — it names the action class, never the object, which exists at stage time and not in frontmatter. The mechanism is stageAction/confirmOnce in work_engine/hooks/builtin/staged_confirmation.ts; enumerate what is pending with `agent-config gates --pending`. Nothing BINDS on this flag yet:
|
|
279
|
+
"description": "Optional, additive: the skill's action STAGES rather than executes, so an irreversible step waits for an explicit human confirmation naming the exact object (non-destructive-by-default § Never act while asking). A declaration only — it names the action class, never the object, which exists at stage time and not in frontmatter. The mechanism is stageAction/confirmOnce in work_engine/hooks/builtin/staged_confirmation.ts; enumerate what is pending with `agent-config gates --pending`. Nothing BINDS on this flag yet: only claude both binds pre_tool_use and honours a deny (docs/contracts/hook-architecture-v1.md, section Which hosts carry pre_tool_use — 2 further hosts bind it and ignore the verdict, 3 alias it unbound, 2 have no pre-tool surface), so a guard would degrade to prose on 7 of 8 hosts while the flag reads like a control — that decision is deferred behind blocker confirmation-degraded-host-semantics."
|
|
280
280
|
},
|
|
281
281
|
"command": {
|
|
282
282
|
"type": "array",
|
|
@@ -287,6 +287,23 @@
|
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
289
|
},
|
|
290
|
+
"triggers": {
|
|
291
|
+
"type": "array",
|
|
292
|
+
"minItems": 1,
|
|
293
|
+
"items": {
|
|
294
|
+
"type": "object",
|
|
295
|
+
"additionalProperties": false,
|
|
296
|
+
"properties": {
|
|
297
|
+
"keyword": {"type": "string", "minLength": 1},
|
|
298
|
+
"phrase": {"type": "string", "minLength": 1},
|
|
299
|
+
"file_pattern": {"type": "string", "minLength": 1},
|
|
300
|
+
"path_prefix": {"type": "string", "minLength": 1},
|
|
301
|
+
"command": {"type": "string", "minLength": 1},
|
|
302
|
+
"reason": {"type": "string"}
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
"description": "Activation triggers for the SKILL catalogue, shaped after the rule-side field so the two stay legible together — same five match keys (keyword, phrase, file_pattern, path_prefix, command), same additionalProperties:false, and deliberately no `intent` key, which was removed rule-side for giving authors false confidence that an activation path existed. OPTIONAL and empty of adopters on the day it ships: the capability becomes declarable before anything declares it, so a seeded tranche is measurable against the router corpus before it is written. Read by scripts/trigger_coverage.ts --scope skill through the same router_match.ts matcher the rule scope uses — not a fork. Schema: docs/contracts/rule-router.md."
|
|
306
|
+
},
|
|
290
307
|
"workspaces": {
|
|
291
308
|
"type": "array",
|
|
292
309
|
"minItems": 1,
|
|
@@ -40,6 +40,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
40
40
|
import { parse as parseYaml } from 'yaml';
|
|
41
41
|
|
|
42
42
|
import { keyword_matches_anchored } from './router_telemetry.js';
|
|
43
|
+
import { match_prompt, type Router as MatchRouter } from './_lib/router_match.js';
|
|
43
44
|
|
|
44
45
|
const _HERE = fileURLToPath(import.meta.url);
|
|
45
46
|
|
|
@@ -48,6 +49,11 @@ export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
|
48
49
|
export const ROUTER = path.join(REPO_ROOT, 'dist', 'router.json');
|
|
49
50
|
export const CORPUS = path.join(REPO_ROOT, 'tests', 'eval', 'trigger-coverage.yaml');
|
|
50
51
|
|
|
52
|
+
/** Skill tree the `skill` scope reads `triggers:` frontmatter from. */
|
|
53
|
+
export const SKILLS_DIR = path.join(REPO_ROOT, 'src', 'skills');
|
|
54
|
+
/** The matrix corpus the unintended-activation census is defined over. */
|
|
55
|
+
export const MATRIX_DIR = path.join(REPO_ROOT, 'tests', 'eval', 'routing-matrix');
|
|
56
|
+
|
|
51
57
|
type Router = {
|
|
52
58
|
kernel?: string[];
|
|
53
59
|
tier_1?: Array<{ id: string; triggers?: Array<Record<string, string>> }>;
|
|
@@ -100,6 +106,168 @@ export function fired_rules(prompt: string, router: Router): Set<string> {
|
|
|
100
106
|
return fired;
|
|
101
107
|
}
|
|
102
108
|
|
|
109
|
+
/* ------------------------------------------------------------------ *
|
|
110
|
+
* The SKILL scope.
|
|
111
|
+
*
|
|
112
|
+
* It reuses `match_prompt` from `_lib/router_match.ts` — the same matcher the
|
|
113
|
+
* rule scope's census runs on — by handing it a router-SHAPED view of the
|
|
114
|
+
* skill catalogue. Forking the matcher would let the two scopes disagree about
|
|
115
|
+
* what "fires" means, which is the exact drift the shared helper exists to
|
|
116
|
+
* prevent.
|
|
117
|
+
*
|
|
118
|
+
* WHAT THIS SCOPE CANNOT REGRESS, stated because the roadmap's own risk
|
|
119
|
+
* register assumed otherwise. Risk 2 reads "the corpus counts activations
|
|
120
|
+
* across the whole router, so a skill-side tranche can regress a rule-side
|
|
121
|
+
* number". Verified against the tree: `compile_router.ts` contains zero
|
|
122
|
+
* references to skills, and `dist/router.json` carries exactly
|
|
123
|
+
* `kernel` + `tier_1` + `tier_2` rule entries. Skill triggers are compiled
|
|
124
|
+
* into nothing and are matched here over a SEPARATE catalogue, so the rule-side
|
|
125
|
+
* 433 census is structurally untouched by any skill tranche. The risk it names
|
|
126
|
+
* is real for a shared router and does not exist in this one — which makes the
|
|
127
|
+
* skill-side count below a NEW number needing its own ceiling, not a threat to
|
|
128
|
+
* the old one.
|
|
129
|
+
* ------------------------------------------------------------------ */
|
|
130
|
+
|
|
131
|
+
/** One skill that declares activation triggers. */
|
|
132
|
+
export interface SkillTriggerEntry {
|
|
133
|
+
id: string;
|
|
134
|
+
triggers: Array<Record<string, string>>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** `triggers:` list items from a SKILL.md frontmatter block. */
|
|
138
|
+
function _parseTriggerBlock(frontmatter: string): Array<Record<string, string>> {
|
|
139
|
+
const out: Array<Record<string, string>> = [];
|
|
140
|
+
const lines = frontmatter.split('\n');
|
|
141
|
+
let inside = false;
|
|
142
|
+
for (const line of lines) {
|
|
143
|
+
if (/^triggers:\s*$/.test(line)) {
|
|
144
|
+
inside = true;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (inside && /^\S/.test(line)) break; // next top-level key ends the block
|
|
148
|
+
if (!inside) continue;
|
|
149
|
+
const item = /^\s*-\s*(keyword|phrase|file_pattern|path_prefix|command):\s*(.+?)\s*$/.exec(line);
|
|
150
|
+
if (item) {
|
|
151
|
+
out.push({ [item[1]!]: item[2]!.replace(/^["']|["']$/g, '') });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Every skill under `skillsDir` that declares a non-empty `triggers:` list. */
|
|
158
|
+
export function loadSkillTriggers(skillsDir: string): SkillTriggerEntry[] {
|
|
159
|
+
let names: string[];
|
|
160
|
+
try {
|
|
161
|
+
names = fs.readdirSync(skillsDir);
|
|
162
|
+
} catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
const out: SkillTriggerEntry[] = [];
|
|
166
|
+
for (const name of names.sort()) {
|
|
167
|
+
const file = path.join(skillsDir, name, 'SKILL.md');
|
|
168
|
+
let content: string;
|
|
169
|
+
try {
|
|
170
|
+
content = fs.readFileSync(file, 'utf-8');
|
|
171
|
+
} catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (!content.startsWith('---')) continue;
|
|
175
|
+
const end = content.indexOf('\n---', 3);
|
|
176
|
+
if (end === -1) continue;
|
|
177
|
+
const triggers = _parseTriggerBlock(content.slice(3, end));
|
|
178
|
+
if (triggers.length > 0) out.push({ id: name, triggers });
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Every prompt in the matrix corpus, positives and near-misses alike.
|
|
185
|
+
*
|
|
186
|
+
* Parsed with the YAML reader this module already imports, not with a
|
|
187
|
+
* line regex. A regex anchored on `- prompt: "…"` silently drops any entry
|
|
188
|
+
* written unquoted or in single quotes, and a corpus that shrinks without
|
|
189
|
+
* saying so moves every rate computed against it — the denominator would
|
|
190
|
+
* change while the number kept its old name.
|
|
191
|
+
*/
|
|
192
|
+
export function loadMatrixPrompts(matrixDir: string): string[] {
|
|
193
|
+
let files: string[];
|
|
194
|
+
try {
|
|
195
|
+
files = fs.readdirSync(matrixDir).filter((n) => n.endsWith('.yaml')).sort();
|
|
196
|
+
} catch {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
const out: string[] = [];
|
|
200
|
+
for (const f of files) {
|
|
201
|
+
let doc: unknown;
|
|
202
|
+
try {
|
|
203
|
+
doc = parseYaml(fs.readFileSync(path.join(matrixDir, f), 'utf-8'), { version: '1.1' });
|
|
204
|
+
} catch {
|
|
205
|
+
continue; // an unparseable corpus file is skipped, never guessed at
|
|
206
|
+
}
|
|
207
|
+
if (typeof doc !== 'object' || doc === null) continue;
|
|
208
|
+
const record = doc as Record<string, unknown>;
|
|
209
|
+
for (const section of ['positives', 'near_misses']) {
|
|
210
|
+
const entries = record[section];
|
|
211
|
+
if (!Array.isArray(entries)) continue;
|
|
212
|
+
for (const raw of entries) {
|
|
213
|
+
const prompt = (raw as Record<string, unknown> | null)?.['prompt'];
|
|
214
|
+
if (typeof prompt === 'string' && prompt.trim() !== '') out.push(prompt);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface SkillScopeReport {
|
|
222
|
+
/** Skills declaring at least one trigger. Zero on the day the schema ships. */
|
|
223
|
+
skills_with_triggers: number;
|
|
224
|
+
prompts: number;
|
|
225
|
+
/** Total (prompt, skill) activations — the skill-side census. */
|
|
226
|
+
activations: number;
|
|
227
|
+
/** Prompts that activated at least one skill. */
|
|
228
|
+
prompts_activating: number;
|
|
229
|
+
/** Per-skill activation counts, descending then by id. */
|
|
230
|
+
histogram: Array<[string, number]>;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Run the skill catalogue over the matrix corpus with the shared matcher.
|
|
235
|
+
*
|
|
236
|
+
* `tier_2` is the carrier for the router-shaped view purely because
|
|
237
|
+
* `match_prompt` reads its rule lists from `tier_1`/`tier_2`; skills have no
|
|
238
|
+
* tier and none is implied by the placement.
|
|
239
|
+
*/
|
|
240
|
+
export function runSkillScope(
|
|
241
|
+
entries: readonly SkillTriggerEntry[],
|
|
242
|
+
prompts: readonly string[],
|
|
243
|
+
): SkillScopeReport {
|
|
244
|
+
// `kernel: []` is load-bearing: `match_prompt` appends every kernel id to
|
|
245
|
+
// `activated_rules` unconditionally, so a non-empty kernel here would count
|
|
246
|
+
// rule activations as skill ones on every prompt.
|
|
247
|
+
const view = { kernel: [], tier_1: [], tier_2: entries } as unknown as MatchRouter;
|
|
248
|
+
const histogram: Record<string, number> = {};
|
|
249
|
+
let activations = 0;
|
|
250
|
+
let promptsActivating = 0;
|
|
251
|
+
for (const prompt of prompts) {
|
|
252
|
+
const result = match_prompt(view, prompt, 'full', null, null);
|
|
253
|
+
const ids = result.activated_rules.map((a) => String(a.rule));
|
|
254
|
+
if (ids.length > 0) promptsActivating += 1;
|
|
255
|
+
for (const id of ids) {
|
|
256
|
+
histogram[id] = (histogram[id] ?? 0) + 1;
|
|
257
|
+
activations += 1;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
skills_with_triggers: entries.length,
|
|
262
|
+
prompts: prompts.length,
|
|
263
|
+
activations,
|
|
264
|
+
prompts_activating: promptsActivating,
|
|
265
|
+
histogram: Object.entries(histogram).sort((a, b) =>
|
|
266
|
+
b[1] !== a[1] ? b[1] - a[1] : a[0] < b[0] ? -1 : 1,
|
|
267
|
+
),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
103
271
|
export function run(corpus: Case[], router: Router): [CaseResult[], number] {
|
|
104
272
|
const results: CaseResult[] = [];
|
|
105
273
|
let misses = 0;
|
|
@@ -168,23 +336,76 @@ function _jsonDumps(obj: unknown): string {
|
|
|
168
336
|
|
|
169
337
|
interface ParsedArgs {
|
|
170
338
|
json: boolean;
|
|
339
|
+
/** `rule` keeps the pinned byte-identical contract; `skill` is additive. */
|
|
340
|
+
scope: 'rule' | 'skill';
|
|
171
341
|
}
|
|
172
342
|
|
|
173
343
|
function parse_args(argv: string[]): ParsedArgs {
|
|
174
|
-
const out: ParsedArgs = { json: false };
|
|
175
|
-
for (
|
|
344
|
+
const out: ParsedArgs = { json: false, scope: 'rule' };
|
|
345
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
346
|
+
const a = argv[i]!;
|
|
176
347
|
if (a === '--json') {
|
|
177
348
|
out.json = true;
|
|
349
|
+
} else if (a === '--scope') {
|
|
350
|
+
const v = argv[i + 1];
|
|
351
|
+
if (v !== 'rule' && v !== 'skill') {
|
|
352
|
+
process.stderr.write(`error: --scope must be rule or skill, got: ${v ?? '(missing)'}\n`);
|
|
353
|
+
process.exit(2);
|
|
354
|
+
}
|
|
355
|
+
out.scope = v;
|
|
356
|
+
i += 1;
|
|
178
357
|
} else if (a === '-h' || a === '--help') {
|
|
179
|
-
process.stdout.write('usage: trigger_coverage [-h] [--json]\n');
|
|
358
|
+
process.stdout.write('usage: trigger_coverage [-h] [--json] [--scope rule|skill]\n');
|
|
180
359
|
process.exit(0);
|
|
181
360
|
}
|
|
182
361
|
}
|
|
183
362
|
return out;
|
|
184
363
|
}
|
|
185
364
|
|
|
365
|
+
/**
|
|
366
|
+
* `--scope skill` — report only, never a gate.
|
|
367
|
+
*
|
|
368
|
+
* Exit 0 regardless of the count: no ceiling is committed here. The skill-side
|
|
369
|
+
* census is a NEW number (see the scope's header note on why the rule-side 433
|
|
370
|
+
* is structurally out of reach), and this package does not commit a threshold
|
|
371
|
+
* before it has a reading. A tranche is judged against a published baseline in
|
|
372
|
+
* its own change, not against a number invented on the day the instrument
|
|
373
|
+
* shipped.
|
|
374
|
+
*/
|
|
375
|
+
function runSkillScopeCli(json: boolean): number {
|
|
376
|
+
const report = runSkillScope(loadSkillTriggers(SKILLS_DIR), loadMatrixPrompts(MATRIX_DIR));
|
|
377
|
+
if (json) {
|
|
378
|
+
process.stdout.write(_jsonDumps(report) + '\n');
|
|
379
|
+
return 0;
|
|
380
|
+
}
|
|
381
|
+
process.stdout.write(
|
|
382
|
+
`skill trigger coverage over ${report.prompts} matrix prompts\n` +
|
|
383
|
+
` skills declaring triggers: ${report.skills_with_triggers}\n` +
|
|
384
|
+
` activations: ${report.activations}\n` +
|
|
385
|
+
` prompts activating ≥1: ${report.prompts_activating}\n`,
|
|
386
|
+
);
|
|
387
|
+
if (report.skills_with_triggers === 0) {
|
|
388
|
+
process.stdout.write(
|
|
389
|
+
'\nNo skill declares a trigger yet. That is the schema landing empty of\n' +
|
|
390
|
+
'adopters, not a failure: the capability is declarable before anything\n' +
|
|
391
|
+
'declares it, so a seeded tranche is measurable before it is written.\n',
|
|
392
|
+
);
|
|
393
|
+
return 0;
|
|
394
|
+
}
|
|
395
|
+
process.stdout.write('\nper skill:\n');
|
|
396
|
+
for (const [id, n] of report.histogram) {
|
|
397
|
+
process.stdout.write(` ${String(n).padStart(4)} ${id}\n`);
|
|
398
|
+
}
|
|
399
|
+
process.stdout.write(
|
|
400
|
+
'\nReport only — no ceiling is enforced here. The rule-side 433 census is a\n' +
|
|
401
|
+
'different catalogue and cannot move from this scope.\n',
|
|
402
|
+
);
|
|
403
|
+
return 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
186
406
|
export function main(argv: string[] | null = null): number {
|
|
187
407
|
const args = parse_args(argv ?? process.argv.slice(2));
|
|
408
|
+
if (args.scope === 'skill') return runSkillScopeCli(args.json);
|
|
188
409
|
|
|
189
410
|
let isFile = false;
|
|
190
411
|
try {
|
|
@@ -80,13 +80,13 @@ export const settingsSchema = z.object({
|
|
|
80
80
|
),
|
|
81
81
|
per_tier: z.object({
|
|
82
82
|
cheap: z.number().min(0).nullable().default(null).describe(
|
|
83
|
-
'USD ceiling for the cheap model tier
|
|
83
|
+
'USD ceiling for the cheap model tier. Budget-aware delegation was ARCHIVED 2026-08-16 (docs/contracts/budget-routing.md), so no code routes on this today; the cap is still summed and reported by `budget.mjs tier`. null = no separate tier cap; global ceilings still apply.',
|
|
84
84
|
),
|
|
85
85
|
medium: z.number().min(0).nullable().default(null).describe(
|
|
86
86
|
'USD ceiling for the medium model tier. null = no separate tier cap.',
|
|
87
87
|
),
|
|
88
88
|
strong: z.number().min(0).nullable().default(null).describe(
|
|
89
|
-
'USD ceiling for the strong model tier. null = no separate tier cap
|
|
89
|
+
'USD ceiling for the strong model tier. null = no separate tier cap. The never-block-to-save-money relation this used to describe went with the archived budget-routing layer (docs/contracts/budget-routing.md); nothing routes between tiers today.',
|
|
90
90
|
),
|
|
91
91
|
}).default({ cheap: null, medium: null, strong: null }),
|
|
92
92
|
}),
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$comment": "Single source of truth for the budget-routing reserve lifecycle (docs/contracts/budget-routing.md). Read by BOTH reserve readers — src/scripts/_lib/tier_budget_routing.ts (acquireBudgetPermit) and src/scripts/cost/budget.mjs (tier subcommand) — so the reserve window can never diverge between them. reserve_ttl_ms: reserves are RACE PROTECTION, not spend accounting — a pending entry older than this is dead and is dropped at the next compaction. lock_break_ms: an .lock file older than this is a crash leftover and is broken (unlink + one retry).",
|
|
3
|
-
"owner": "maintainer",
|
|
4
|
-
"review_by": "2027-08-03",
|
|
5
|
-
"reserve_ttl_ms": 600000,
|
|
6
|
-
"lock_break_ms": 30000
|
|
7
|
-
}
|