vigiles 5.0.1 → 5.2.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/README.md +15 -9
- package/dist/adapters/claude-code/adapter.js +1 -0
- package/dist/adapters/claude-code/agent-runtime.d.ts +30 -6
- package/dist/adapters/claude-code/agent-runtime.js +66 -37
- package/dist/adapters/claude-code/dialect.js +37 -2
- package/dist/adapters/claude-code/effect-region.d.ts +9 -0
- package/dist/adapters/claude-code/effect-region.js +45 -0
- package/dist/adapters/claude-code/layout.js +3 -0
- package/dist/adapters/claude-code/skill-runtime.d.ts +25 -0
- package/dist/adapters/claude-code/skill-runtime.js +48 -0
- package/dist/adapters/codex/adapter.js +3 -0
- package/dist/adapters/codex/eval.d.ts +94 -0
- package/dist/adapters/codex/eval.js +227 -0
- package/dist/adapters/codex/layout.js +3 -0
- package/dist/adapters/opencode/adapter.js +1 -0
- package/dist/adapters/opencode/layout.js +3 -0
- package/dist/check.d.ts +8 -0
- package/dist/check.js +27 -3
- package/dist/cli.js +712 -21
- package/dist/codex.d.ts +1 -0
- package/dist/codex.js +3 -0
- package/dist/core/adapter.d.ts +10 -0
- package/dist/core/bash-effects.d.ts +41 -0
- package/dist/core/bash-effects.js +405 -0
- package/dist/core/compile.d.ts +3 -1
- package/dist/core/compile.js +169 -74
- package/dist/core/description-overlap.d.ts +27 -0
- package/dist/core/description-overlap.js +53 -0
- package/dist/core/dialect.d.ts +18 -0
- package/dist/core/effects.d.ts +172 -0
- package/dist/core/effects.js +245 -0
- package/dist/core/frontmatter-read.d.ts +25 -0
- package/dist/core/frontmatter-read.js +138 -0
- package/dist/core/hook-events.d.ts +34 -0
- package/dist/core/hook-events.js +48 -0
- package/dist/core/layout.d.ts +6 -0
- package/dist/core/mcp-config.d.ts +20 -0
- package/dist/core/mcp-config.js +40 -0
- package/dist/core/mcp-hook.d.ts +35 -0
- package/dist/core/mcp-hook.js +70 -0
- package/dist/core/mcp-tool.d.ts +50 -0
- package/dist/core/mcp-tool.js +61 -0
- package/dist/core/orphans.js +21 -0
- package/dist/core/spec.d.ts +142 -3
- package/dist/core/spec.js +48 -0
- package/dist/core/tool-contract.d.ts +68 -0
- package/dist/core/tool-contract.js +113 -0
- package/dist/core/types.d.ts +91 -2
- package/dist/core/validate.js +23 -1
- package/dist/eval.d.ts +69 -13
- package/dist/eval.js +106 -51
- package/dist/harness-test.d.ts +7 -0
- package/dist/harness-test.js +19 -7
- package/dist/leaderboard.d.ts +2 -0
- package/dist/leaderboard.js +63 -3
- package/dist/optimize.d.ts +74 -0
- package/dist/optimize.js +94 -0
- package/dist/plugin-loader.d.ts +1 -0
- package/dist/plugin-loader.js +71 -18
- package/dist/scaffold-test.d.ts +30 -0
- package/dist/scaffold-test.js +158 -0
- package/dist/scan-behavioral.d.ts +73 -0
- package/dist/scan-behavioral.js +150 -0
- package/dist/scan.d.ts +166 -1
- package/dist/scan.js +622 -55
- package/dist/score-explainer.d.ts +69 -0
- package/dist/score-explainer.js +169 -0
- package/dist/test-coverage.d.ts +7 -0
- package/dist/test-coverage.js +39 -24
- package/package.json +2 -1
- package/skills/{migrate-to-spec → adopt-spec}/SKILL.md +4 -6
- package/skills/edit-spec/SKILL.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -19,10 +19,14 @@ const cli_flags_js_1 = require("./cli-flags.js");
|
|
|
19
19
|
const setup_plan_js_1 = require("./setup-plan.js");
|
|
20
20
|
const types_js_1 = require("./core/types.js");
|
|
21
21
|
const test_coverage_js_1 = require("./test-coverage.js");
|
|
22
|
+
const scaffold_test_js_1 = require("./scaffold-test.js");
|
|
22
23
|
const scan_js_1 = require("./scan.js");
|
|
24
|
+
const score_explainer_js_1 = require("./score-explainer.js");
|
|
25
|
+
const scan_behavioral_js_1 = require("./scan-behavioral.js");
|
|
23
26
|
const adapter_registry_js_1 = require("./adapter-registry.js");
|
|
24
27
|
const skill_harness_js_1 = require("./skill-harness.js");
|
|
25
28
|
const leaderboard_js_1 = require("./leaderboard.js");
|
|
29
|
+
const optimize_js_1 = require("./optimize.js");
|
|
26
30
|
const compile_js_1 = require("./core/compile.js");
|
|
27
31
|
const proofs_js_1 = require("./core/proofs.js");
|
|
28
32
|
const inline_js_1 = require("./core/inline.js");
|
|
@@ -32,6 +36,7 @@ const compose_js_1 = require("./core/compose.js");
|
|
|
32
36
|
const compile_generator_js_1 = require("./core/compile-generator.js");
|
|
33
37
|
const action_gate_js_1 = require("./action-gate.js");
|
|
34
38
|
const agent_runtime_js_1 = require("./adapters/claude-code/agent-runtime.js");
|
|
39
|
+
const effect_region_js_1 = require("./adapters/claude-code/effect-region.js");
|
|
35
40
|
const tool_intercept_js_1 = require("./tool-intercept.js");
|
|
36
41
|
const refs_js_1 = require("./core/refs.js");
|
|
37
42
|
const mcp_js_1 = require("./core/mcp.js");
|
|
@@ -633,6 +638,17 @@ function lintExitCode(report) {
|
|
|
633
638
|
report.integrityErrors > 0 ||
|
|
634
639
|
report.coverageErrors > 0 ||
|
|
635
640
|
report.untestedErrors > 0 ||
|
|
641
|
+
report.toolContractErrors > 0 ||
|
|
642
|
+
report.hookEventErrors > 0 ||
|
|
643
|
+
report.frontmatterSchemaErrors > 0 ||
|
|
644
|
+
report.mcpConfigErrors > 0 ||
|
|
645
|
+
report.skillFrontmatterErrors > 0 ||
|
|
646
|
+
report.mcpToolErrors > 0 ||
|
|
647
|
+
report.hookScriptErrors > 0 ||
|
|
648
|
+
report.disallowedToolErrors > 0 ||
|
|
649
|
+
report.descriptionOverlapErrors > 0 ||
|
|
650
|
+
report.frontmatterValidErrors > 0 ||
|
|
651
|
+
report.mcpHookErrors > 0 ||
|
|
636
652
|
report.symbolRefErrors > 0 ||
|
|
637
653
|
report.mcpRefErrors > 0)
|
|
638
654
|
return 2;
|
|
@@ -854,6 +870,17 @@ async function runLint(restArgs, flags, config) {
|
|
|
854
870
|
const json = flags.includes("--json");
|
|
855
871
|
const silent = summary || json;
|
|
856
872
|
const files = findInstructionFiles(restArgs);
|
|
873
|
+
// Resolve the active harness ONCE so the harness-specific checks below run
|
|
874
|
+
// against the right adapter's dialect (tool/event catalogs) and surfaces —
|
|
875
|
+
// not a hard-coded Claude Code default. A subagent-surface rule reports n/a
|
|
876
|
+
// on a harness without subagents (Codex) rather than scanning nothing.
|
|
877
|
+
const harnessFlag = harnessFlagFrom(flags);
|
|
878
|
+
const lintSelection = (0, adapter_registry_js_1.resolveHarnessSelection)({
|
|
879
|
+
root: process.cwd(),
|
|
880
|
+
flag: harnessFlag,
|
|
881
|
+
configHarness: (0, adapter_registry_js_1.normalizeHarnessList)(config?.harness),
|
|
882
|
+
});
|
|
883
|
+
const adapter = lintSelection.adapter;
|
|
857
884
|
// 1. Verify hashes and structure
|
|
858
885
|
if (!silent) {
|
|
859
886
|
if (files.length > 0) {
|
|
@@ -912,7 +939,41 @@ async function runLint(restArgs, flags, config) {
|
|
|
912
939
|
// 7b. Untested-surface check — skills/agents/hooks shipping without a test or
|
|
913
940
|
// eval. Warning by default (a nudge, exit 0); set rules.untested-{skill,agent,
|
|
914
941
|
// hook} to "error" to gate CI. See src/test-coverage.ts and docs/rules/.
|
|
915
|
-
const untested = checkUntestedSurfaces(config, silent);
|
|
942
|
+
const untested = checkUntestedSurfaces(config, silent, adapter);
|
|
943
|
+
// 7c. Subagent tool-contract check — cross-reference each subagent's `tools:`
|
|
944
|
+
// rail against the harness catalog (the moat). n/a on a harness with no
|
|
945
|
+
// subagents. Off by default unless a severity is configured; warning surfaces
|
|
946
|
+
// a typo/never-available tool, error gates CI.
|
|
947
|
+
const toolContract = checkSubagentToolContracts(config, silent, adapter);
|
|
948
|
+
// 7d. Hook-event check — a hook registered under an event the harness doesn't
|
|
949
|
+
// define never fires. High-precision (close typos only). Off unless configured.
|
|
950
|
+
const hookEvents = checkHookEvents(config, silent, adapter);
|
|
951
|
+
// 7e. Subagent-frontmatter check — a subagent missing required frontmatter
|
|
952
|
+
// (name + description) won't register. n/a on a harness with no subagents.
|
|
953
|
+
const frontmatter = checkFrontmatterSchema(config, silent, adapter);
|
|
954
|
+
// 7f. MCP-config check — a declared MCP server with no command/url can't start.
|
|
955
|
+
const mcpConfig = checkMcpConfig(config, silent, adapter);
|
|
956
|
+
// 7g. Skill-frontmatter — RECOMMEND explicit name/description on skills (a
|
|
957
|
+
// reliable trigger surface). Best-practice nudge; skills load without it.
|
|
958
|
+
const skillFm = checkSkillFrontmatter(config, silent, adapter);
|
|
959
|
+
// 7h. MCP tool-resolution — an `mcp__server__tool` in a contract whose server
|
|
960
|
+
// the plugin doesn't declare can't resolve (the MCP half of the tool moat).
|
|
961
|
+
const mcpToolResolves = checkMcpToolResolves(config, silent, adapter);
|
|
962
|
+
// 7i. Hook-script existence — a hook command referencing a missing script file
|
|
963
|
+
// never runs (matches Anthropic's own `claude plugin validate`).
|
|
964
|
+
const hookScripts = checkHookScriptExists(config, silent, adapter);
|
|
965
|
+
// 7j. Disallowed-tools — a `disallowedTools:` block-list typo blocks nothing
|
|
966
|
+
// (the deny-side mirror of subagent-tool-contract; close-typo only).
|
|
967
|
+
const disallowedTools = checkDisallowedTools(config, silent, adapter);
|
|
968
|
+
// 7k. Description-overlap — two model-invocable skills with near-identical
|
|
969
|
+
// descriptions collide in the selector (deterministic NCD precision proxy).
|
|
970
|
+
const descriptionOverlap = checkDescriptionOverlap(config, silent, adapter);
|
|
971
|
+
// 7l. Frontmatter-valid — a `---` block that isn't valid YAML (warn; js-yaml is
|
|
972
|
+
// stricter than some loaders, so verify before enforcing).
|
|
973
|
+
const frontmatterValid = checkFrontmatterValid(config, silent, adapter);
|
|
974
|
+
// 7m. MCP hook-target — a `type: mcp_tool` hook action that's incomplete or
|
|
975
|
+
// targets an undeclared server (the moat applied to the hook surface).
|
|
976
|
+
const mcpHookTargets = checkMcpHookTargets(config, silent, adapter);
|
|
916
977
|
// 8. Validate vigiles builder calls inside markdown code blocks. Default
|
|
917
978
|
// is to validate every ref; illustrative blocks opt out via
|
|
918
979
|
// `<!-- vigiles:ignore -->` (single block) or
|
|
@@ -956,6 +1017,28 @@ async function runLint(restArgs, flags, config) {
|
|
|
956
1017
|
orphanCount: orphanReport.orphans.length,
|
|
957
1018
|
untestedSurfaces: untested.untested,
|
|
958
1019
|
untestedErrors: untested.errors,
|
|
1020
|
+
toolContractIssues: toolContract.issues,
|
|
1021
|
+
toolContractErrors: toolContract.errors,
|
|
1022
|
+
hookEventIssues: hookEvents.issues,
|
|
1023
|
+
hookEventErrors: hookEvents.errors,
|
|
1024
|
+
frontmatterSchemaIssues: frontmatter.issues,
|
|
1025
|
+
frontmatterSchemaErrors: frontmatter.errors,
|
|
1026
|
+
mcpConfigIssues: mcpConfig.issues,
|
|
1027
|
+
mcpConfigErrors: mcpConfig.errors,
|
|
1028
|
+
skillFrontmatterIssues: skillFm.issues,
|
|
1029
|
+
skillFrontmatterErrors: skillFm.errors,
|
|
1030
|
+
mcpToolIssues: mcpToolResolves.issues,
|
|
1031
|
+
mcpToolErrors: mcpToolResolves.errors,
|
|
1032
|
+
hookScriptIssues: hookScripts.issues,
|
|
1033
|
+
hookScriptErrors: hookScripts.errors,
|
|
1034
|
+
disallowedToolIssues: disallowedTools.issues,
|
|
1035
|
+
disallowedToolErrors: disallowedTools.errors,
|
|
1036
|
+
descriptionOverlapIssues: descriptionOverlap.issues,
|
|
1037
|
+
descriptionOverlapErrors: descriptionOverlap.errors,
|
|
1038
|
+
frontmatterValidIssues: frontmatterValid.issues,
|
|
1039
|
+
frontmatterValidErrors: frontmatterValid.errors,
|
|
1040
|
+
mcpHookIssues: mcpHookTargets.issues,
|
|
1041
|
+
mcpHookErrors: mcpHookTargets.errors,
|
|
959
1042
|
docRefErrors: docRefReport.errors.length,
|
|
960
1043
|
symbolRefErrors,
|
|
961
1044
|
mcpRefErrors,
|
|
@@ -1524,7 +1607,7 @@ function redirectSyncToolTargets(cwd, targets) {
|
|
|
1524
1607
|
}
|
|
1525
1608
|
/** Pillar 1 — specs + types + schema + compile. Scaffolds a spec for every
|
|
1526
1609
|
* instruction file (so `--lint` always delivers a spec), but never compiles
|
|
1527
|
-
* OVER a hand-written file — that is left to the
|
|
1610
|
+
* OVER a hand-written file — that is left to the adopt-spec skill. */
|
|
1528
1611
|
async function setupPillar1(detected, targetValue, harnesses) {
|
|
1529
1612
|
const cwd = process.cwd();
|
|
1530
1613
|
const written = [];
|
|
@@ -1549,7 +1632,7 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
1549
1632
|
}
|
|
1550
1633
|
if (targetExists && !targetHasHash((0, node_path_1.resolve)(cwd, target))) {
|
|
1551
1634
|
needsMigration.push(target);
|
|
1552
|
-
console.log(` ${target} already has content —
|
|
1635
|
+
console.log(` ${target} already has content — adopt it into a spec with the adopt-spec skill, then \`vigiles compile\`.`);
|
|
1553
1636
|
}
|
|
1554
1637
|
}
|
|
1555
1638
|
// Generate types + schema.
|
|
@@ -1741,7 +1824,7 @@ function printSetupSummary(opts) {
|
|
|
1741
1824
|
console.log("\n---\nSetup complete.\n");
|
|
1742
1825
|
const nextSteps = [];
|
|
1743
1826
|
if (needsMigration.length > 0) {
|
|
1744
|
-
nextSteps.push(`
|
|
1827
|
+
nextSteps.push(`Adopt ${needsMigration.join(", ")} into a spec with the adopt-spec skill, then \`npx vigiles compile\``);
|
|
1745
1828
|
}
|
|
1746
1829
|
else if (specPathsList.length > 0) {
|
|
1747
1830
|
nextSteps.push(`Edit ${specPathsList.join(", ")} — add your conventions, then \`/strengthen\``);
|
|
@@ -1888,16 +1971,16 @@ function checkIntegrityForFiles(files, severity, silent) {
|
|
|
1888
1971
|
return severity === "error" ? errorCount : 0;
|
|
1889
1972
|
}
|
|
1890
1973
|
/**
|
|
1891
|
-
* Apply the per-kind `untested-skill` / `untested-
|
|
1974
|
+
* Apply the per-kind `untested-skill` / `untested-subagent` / `untested-hook` rules:
|
|
1892
1975
|
* find skills/agents/hooks with no test or eval (see src/test-coverage.ts). Each
|
|
1893
1976
|
* kind is gated by its OWN rule severity — a kind set to `false` is not scanned;
|
|
1894
1977
|
* "warn" prints but never fails CI; "error" fails (exit 2). Returns the raw
|
|
1895
1978
|
* untested count plus the severity-gated error count.
|
|
1896
1979
|
*/
|
|
1897
|
-
function checkUntestedSurfaces(config, silent) {
|
|
1980
|
+
function checkUntestedSurfaces(config, silent, adapter) {
|
|
1898
1981
|
const rules = config?.rules;
|
|
1899
1982
|
const skillSev = (0, types_js_1.ruleSeverity)(rules?.["untested-skill"]);
|
|
1900
|
-
const agentSev = (0, types_js_1.ruleSeverity)(rules?.["untested-
|
|
1983
|
+
const agentSev = (0, types_js_1.ruleSeverity)(rules?.["untested-subagent"]);
|
|
1901
1984
|
const hookSev = (0, types_js_1.ruleSeverity)(rules?.["untested-hook"]);
|
|
1902
1985
|
if (!skillSev && !agentSev && !hookSev)
|
|
1903
1986
|
return { untested: 0, errors: 0 };
|
|
@@ -1906,11 +1989,12 @@ function checkUntestedSurfaces(config, silent) {
|
|
|
1906
1989
|
// whichever of the three rules carries them.
|
|
1907
1990
|
const opts = {
|
|
1908
1991
|
...(0, types_js_1.ruleOptions)(rules?.["untested-skill"]),
|
|
1909
|
-
...(0, types_js_1.ruleOptions)(rules?.["untested-
|
|
1992
|
+
...(0, types_js_1.ruleOptions)(rules?.["untested-subagent"]),
|
|
1910
1993
|
...(0, types_js_1.ruleOptions)(rules?.["untested-hook"]),
|
|
1911
1994
|
};
|
|
1912
1995
|
const report = (0, test_coverage_js_1.findUntestedSurfaces)({
|
|
1913
1996
|
basePath: process.cwd(),
|
|
1997
|
+
layout: adapter.layout,
|
|
1914
1998
|
skills: skillSev !== false,
|
|
1915
1999
|
agents: agentSev !== false,
|
|
1916
2000
|
hooks: hookSev !== false,
|
|
@@ -1931,6 +2015,363 @@ function checkUntestedSurfaces(config, silent) {
|
|
|
1931
2015
|
errors: report.untested.filter((s) => sevFor(s.kind) === "error").length,
|
|
1932
2016
|
};
|
|
1933
2017
|
}
|
|
2018
|
+
/**
|
|
2019
|
+
* A surface-scoped rule (subagent / shell-hook) is configured, but the active
|
|
2020
|
+
* harness doesn't have that surface. Report it as **n/a** — loud, not silent (the
|
|
2021
|
+
* no-silent-skips ethos): the rule isn't failing and isn't passing, it simply
|
|
2022
|
+
* doesn't apply to this harness. Never counts toward issues/errors.
|
|
2023
|
+
*/
|
|
2024
|
+
function reportNotApplicable(check, surface, adapter, silent) {
|
|
2025
|
+
if (silent)
|
|
2026
|
+
return;
|
|
2027
|
+
console.log(`\n${check}:\n`);
|
|
2028
|
+
console.log(` – n/a — ${adapter.name} has no ${surface}`);
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Apply the `subagent-tool-contract` rule: cross-reference every subagent's `tools:`
|
|
2032
|
+
* rail against the harness tool catalog (the moat — "valid is not true"). Flags
|
|
2033
|
+
* only the HIGH-CONFIDENCE issues (a never-available tool, or a close typo) via
|
|
2034
|
+
* the shared `confidentToolIssues` detector — the same code `scan` and
|
|
2035
|
+
* `compileAgent` use (one-detector-no-drift), so a bare unrecognized tool
|
|
2036
|
+
* (plugin/MCP-provided) is never a false alarm. Warning by default; set
|
|
2037
|
+
* `subagent-tool-contract: "error"` to gate CI. Returns the issue + error counts.
|
|
2038
|
+
*/
|
|
2039
|
+
function checkSubagentToolContracts(config, silent, adapter) {
|
|
2040
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-tool-contract"]);
|
|
2041
|
+
if (!sev)
|
|
2042
|
+
return { issues: 0, errors: 0 };
|
|
2043
|
+
if (!adapter.capabilities.subagents) {
|
|
2044
|
+
reportNotApplicable("Subagent tool-contract check", "subagents", adapter, silent);
|
|
2045
|
+
return { issues: 0, errors: 0 };
|
|
2046
|
+
}
|
|
2047
|
+
// Reuse the loader's already-resolved, layout+dialect-driven agents (the same
|
|
2048
|
+
// `scan` detector — one-detector-no-drift) instead of re-globbing a hard-coded
|
|
2049
|
+
// `agents/` path, so a harness with a different subagent dir Just Works.
|
|
2050
|
+
let agents;
|
|
2051
|
+
try {
|
|
2052
|
+
agents = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).agents;
|
|
2053
|
+
}
|
|
2054
|
+
catch {
|
|
2055
|
+
return { issues: 0, errors: 0 };
|
|
2056
|
+
}
|
|
2057
|
+
let issues = 0;
|
|
2058
|
+
let printedHeader = false;
|
|
2059
|
+
for (const agent of agents) {
|
|
2060
|
+
if (agent.toolIssues.length === 0)
|
|
2061
|
+
continue;
|
|
2062
|
+
issues += agent.toolIssues.length;
|
|
2063
|
+
if (!silent) {
|
|
2064
|
+
if (!printedHeader) {
|
|
2065
|
+
console.log("\nSubagent tool-contract check:\n");
|
|
2066
|
+
printedHeader = true;
|
|
2067
|
+
}
|
|
2068
|
+
for (const issue of agent.toolIssues) {
|
|
2069
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${agent.path}: ${issue.message}`);
|
|
2070
|
+
ghAnnotate(sev === "error" ? "error" : "warning", issue.message, agent.path);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
return { issues, errors: sev === "error" ? issues : 0 };
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Apply the `hook-events` rule: flag a hook registered under an event name the
|
|
2078
|
+
* harness doesn't define (a typo → the hook never fires). Reuses `scanPlugin`'s
|
|
2079
|
+
* `hookEventIssues` (the shared detector, high-precision: close typos only, never
|
|
2080
|
+
* a framework/custom event). Warning by default; "error" gates CI.
|
|
2081
|
+
*/
|
|
2082
|
+
function checkHookEvents(config, silent, adapter) {
|
|
2083
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-events"]);
|
|
2084
|
+
if (!sev)
|
|
2085
|
+
return { issues: 0, errors: 0 };
|
|
2086
|
+
if (!adapter.capabilities.shellHooks) {
|
|
2087
|
+
reportNotApplicable("Hook-event check", "shell hooks", adapter, silent);
|
|
2088
|
+
return { issues: 0, errors: 0 };
|
|
2089
|
+
}
|
|
2090
|
+
let found;
|
|
2091
|
+
try {
|
|
2092
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hookEventIssues;
|
|
2093
|
+
}
|
|
2094
|
+
catch {
|
|
2095
|
+
return { issues: 0, errors: 0 };
|
|
2096
|
+
}
|
|
2097
|
+
if (found.length > 0 && !silent) {
|
|
2098
|
+
console.log("\nHook-event check:\n");
|
|
2099
|
+
for (const issue of found) {
|
|
2100
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
2101
|
+
ghAnnotate(sev === "error" ? "error" : "warning", issue.message);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2105
|
+
}
|
|
2106
|
+
/**
|
|
2107
|
+
* Apply the `subagent-frontmatter` rule. Two kinds of subagent-frontmatter defect, one
|
|
2108
|
+
* rule: (1) a subagent MISSING a required field (`name`/`description`) — it won't
|
|
2109
|
+
* register; (2) a subagent with an INVALID `model:`/`color:` value (a close typo
|
|
2110
|
+
* of a real one) — it silently falls back / is ignored. Reuses `scanPlugin`'s
|
|
2111
|
+
* `frontmatterIssues` + `frontmatterValueIssues`. Warning by default; "error" gates CI.
|
|
2112
|
+
*/
|
|
2113
|
+
function checkFrontmatterSchema(config, silent, adapter) {
|
|
2114
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-frontmatter"]);
|
|
2115
|
+
if (!sev)
|
|
2116
|
+
return { issues: 0, errors: 0 };
|
|
2117
|
+
if (!adapter.capabilities.subagents) {
|
|
2118
|
+
reportNotApplicable("Subagent-frontmatter check", "subagents", adapter, silent);
|
|
2119
|
+
return { issues: 0, errors: 0 };
|
|
2120
|
+
}
|
|
2121
|
+
let found;
|
|
2122
|
+
try {
|
|
2123
|
+
const r = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect);
|
|
2124
|
+
found = [...r.frontmatterIssues, ...r.frontmatterValueIssues];
|
|
2125
|
+
}
|
|
2126
|
+
catch {
|
|
2127
|
+
return { issues: 0, errors: 0 };
|
|
2128
|
+
}
|
|
2129
|
+
if (found.length > 0 && !silent) {
|
|
2130
|
+
console.log("\nFrontmatter-schema check:\n");
|
|
2131
|
+
for (const issue of found) {
|
|
2132
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
2133
|
+
ghAnnotate(sev === "error" ? "error" : "warning", issue.message, issue.path);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Apply the `skill-frontmatter` rule: RECOMMEND (not require) that a SKILL.md
|
|
2140
|
+
* declares an explicit `name` + `description` rather than relying on the
|
|
2141
|
+
* dir-name / first-paragraph fallbacks — a more reliable trigger surface. The
|
|
2142
|
+
* skill still LOADS without them, so this is a best-practice nudge: warn by
|
|
2143
|
+
* default; set "error" to enforce it on your own skills. Reuses `scanPlugin`'s
|
|
2144
|
+
* `skillMetaIssues`.
|
|
2145
|
+
*/
|
|
2146
|
+
function checkSkillFrontmatter(config, silent, adapter) {
|
|
2147
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-frontmatter"]);
|
|
2148
|
+
if (!sev)
|
|
2149
|
+
return { issues: 0, errors: 0 };
|
|
2150
|
+
let found;
|
|
2151
|
+
try {
|
|
2152
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).skillMetaIssues;
|
|
2153
|
+
}
|
|
2154
|
+
catch {
|
|
2155
|
+
return { issues: 0, errors: 0 };
|
|
2156
|
+
}
|
|
2157
|
+
if (found.length > 0 && !silent) {
|
|
2158
|
+
console.log("\nSkill-frontmatter check:\n");
|
|
2159
|
+
for (const issue of found) {
|
|
2160
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
2161
|
+
ghAnnotate(sev === "error" ? "error" : "warning", issue.message, issue.path);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Apply the `mcp-config` rule: a declared MCP server with neither a `command`
|
|
2168
|
+
* (stdio) nor a `url` (http/sse) can't start. Reuses `scanPlugin`'s `mcpIssues`.
|
|
2169
|
+
* Warning by default; "error" gates CI.
|
|
2170
|
+
*/
|
|
2171
|
+
function checkMcpConfig(config, silent, adapter) {
|
|
2172
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-config"]);
|
|
2173
|
+
if (!sev)
|
|
2174
|
+
return { issues: 0, errors: 0 };
|
|
2175
|
+
let found;
|
|
2176
|
+
try {
|
|
2177
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).mcpIssues;
|
|
2178
|
+
}
|
|
2179
|
+
catch {
|
|
2180
|
+
return { issues: 0, errors: 0 };
|
|
2181
|
+
}
|
|
2182
|
+
if (found.length > 0 && !silent) {
|
|
2183
|
+
console.log("\nMCP-config check:\n");
|
|
2184
|
+
for (const issue of found) {
|
|
2185
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
2186
|
+
ghAnnotate(sev === "error" ? "error" : "warning", issue.message);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Apply the `disallowed-tools-contract` rule: a subagent's `disallowedTools:`
|
|
2193
|
+
* block-list entry that's a close typo of a real tool blocks NOTHING — the tool
|
|
2194
|
+
* it was meant to deny stays available, silently. Reuses `scanPlugin`'s per-agent
|
|
2195
|
+
* `disallowedToolIssues` (close-typo only — high-precision). Warning by default;
|
|
2196
|
+
* "error" gates CI.
|
|
2197
|
+
*/
|
|
2198
|
+
function checkDisallowedTools(config, silent, adapter) {
|
|
2199
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["disallowed-tools-contract"]);
|
|
2200
|
+
if (!sev)
|
|
2201
|
+
return { issues: 0, errors: 0 };
|
|
2202
|
+
if (!adapter.capabilities.subagents) {
|
|
2203
|
+
reportNotApplicable("Disallowed-tools check", "subagents", adapter, silent);
|
|
2204
|
+
return { issues: 0, errors: 0 };
|
|
2205
|
+
}
|
|
2206
|
+
let found;
|
|
2207
|
+
try {
|
|
2208
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).agents.flatMap((a) => a.disallowedToolIssues.map((i) => ({ message: i.message, path: a.path })));
|
|
2209
|
+
}
|
|
2210
|
+
catch {
|
|
2211
|
+
return { issues: 0, errors: 0 };
|
|
2212
|
+
}
|
|
2213
|
+
if (found.length > 0 && !silent) {
|
|
2214
|
+
console.log("\nDisallowed-tools check:\n");
|
|
2215
|
+
for (const issue of found) {
|
|
2216
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.path}: ${issue.message}`);
|
|
2217
|
+
ghAnnotate(sev === "error" ? "error" : "warning", issue.message, issue.path);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2221
|
+
}
|
|
2222
|
+
/**
|
|
2223
|
+
* Apply the `frontmatter-valid` rule: a skill/agent `---` block that EXISTS but
|
|
2224
|
+
* isn't valid YAML — fields may not parse as intended. Reuses `scanPlugin`'s
|
|
2225
|
+
* `malformedFrontmatter`. HONEST caveat (see docs/rules/frontmatter-valid.md):
|
|
2226
|
+
* js-yaml is stricter than some loaders, so a one-line `description:` with a
|
|
2227
|
+
* colon / `<example>` is flagged though it may still load — hence WARN by default
|
|
2228
|
+
* (verify before setting "error").
|
|
2229
|
+
*/
|
|
2230
|
+
function checkFrontmatterValid(config, silent, adapter) {
|
|
2231
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["frontmatter-valid"]);
|
|
2232
|
+
if (!sev)
|
|
2233
|
+
return { issues: 0, errors: 0 };
|
|
2234
|
+
let found;
|
|
2235
|
+
try {
|
|
2236
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).malformedFrontmatter;
|
|
2237
|
+
}
|
|
2238
|
+
catch {
|
|
2239
|
+
return { issues: 0, errors: 0 };
|
|
2240
|
+
}
|
|
2241
|
+
if (found.length > 0 && !silent) {
|
|
2242
|
+
console.log("\nFrontmatter-validity check:\n");
|
|
2243
|
+
for (const issue of found) {
|
|
2244
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
2245
|
+
ghAnnotate(sev === "error" ? "error" : "warning", issue.message, issue.path);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Apply the `description-overlap` rule: two model-invocable skills with
|
|
2252
|
+
* near-identical descriptions collide in the selector — the wrong one fires. A
|
|
2253
|
+
* deterministic NCD proxy for a `--trigger`-class precision bug. Reuses
|
|
2254
|
+
* `scanPlugin`'s `descriptionOverlaps` (calibrated FP-safe: only basically
|
|
2255
|
+
* identical text). Warning by default; "error" gates CI.
|
|
2256
|
+
*/
|
|
2257
|
+
function checkDescriptionOverlap(config, silent, adapter) {
|
|
2258
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["description-overlap"]);
|
|
2259
|
+
if (!sev)
|
|
2260
|
+
return { issues: 0, errors: 0 };
|
|
2261
|
+
let found;
|
|
2262
|
+
try {
|
|
2263
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).descriptionOverlaps;
|
|
2264
|
+
}
|
|
2265
|
+
catch {
|
|
2266
|
+
return { issues: 0, errors: 0 };
|
|
2267
|
+
}
|
|
2268
|
+
if (found.length > 0 && !silent) {
|
|
2269
|
+
console.log("\nDescription-overlap check:\n");
|
|
2270
|
+
for (const issue of found) {
|
|
2271
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
2272
|
+
ghAnnotate(sev === "error" ? "error" : "warning", issue.message);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2276
|
+
}
|
|
2277
|
+
/**
|
|
2278
|
+
* Apply the `mcp-hook-target-resolves` rule: a `type: "mcp_tool"` hook action
|
|
2279
|
+
* that's incomplete (no `server`/`tool`) or targets a server the plugin doesn't
|
|
2280
|
+
* declare — the hook silently never dispatches. Reuses `scanPlugin`'s
|
|
2281
|
+
* `mcpHookIssues` (high-precision: declared-set gated, built-ins allowlisted).
|
|
2282
|
+
* Warning by default; "error" gates CI.
|
|
2283
|
+
*/
|
|
2284
|
+
function checkMcpHookTargets(config, silent, adapter) {
|
|
2285
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-hook-target-resolves"]);
|
|
2286
|
+
if (!sev)
|
|
2287
|
+
return { issues: 0, errors: 0 };
|
|
2288
|
+
if (!adapter.capabilities.shellHooks) {
|
|
2289
|
+
reportNotApplicable("MCP hook-target check", "shell hooks", adapter, silent);
|
|
2290
|
+
return { issues: 0, errors: 0 };
|
|
2291
|
+
}
|
|
2292
|
+
let found;
|
|
2293
|
+
try {
|
|
2294
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).mcpHookIssues;
|
|
2295
|
+
}
|
|
2296
|
+
catch {
|
|
2297
|
+
return { issues: 0, errors: 0 };
|
|
2298
|
+
}
|
|
2299
|
+
if (found.length > 0 && !silent) {
|
|
2300
|
+
console.log("\nMCP hook-target check:\n");
|
|
2301
|
+
for (const issue of found) {
|
|
2302
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.message}`);
|
|
2303
|
+
ghAnnotate(sev === "error" ? "error" : "warning", issue.message);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2307
|
+
}
|
|
2308
|
+
/**
|
|
2309
|
+
* Apply the `hook-script-exists` rule: a hook command references a script file
|
|
2310
|
+
* that doesn't exist on disk (with `${CLAUDE_PLUGIN_ROOT}` resolved) → the hook
|
|
2311
|
+
* silently never runs. Reuses `scanPlugin`'s `hooks` (status "missing"); the
|
|
2312
|
+
* shared resolver already excludes the FP-prone cases (unresolved vars,
|
|
2313
|
+
* existence-guarded one-liners, inline commands). Matches Anthropic's own
|
|
2314
|
+
* `claude plugin validate`. Warning by default; "error" gates CI.
|
|
2315
|
+
*/
|
|
2316
|
+
function checkHookScriptExists(config, silent, adapter) {
|
|
2317
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-script-exists"]);
|
|
2318
|
+
if (!sev)
|
|
2319
|
+
return { issues: 0, errors: 0 };
|
|
2320
|
+
if (!adapter.capabilities.shellHooks) {
|
|
2321
|
+
reportNotApplicable("Hook-script existence check", "shell hooks", adapter, silent);
|
|
2322
|
+
return { issues: 0, errors: 0 };
|
|
2323
|
+
}
|
|
2324
|
+
let missing;
|
|
2325
|
+
try {
|
|
2326
|
+
missing = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hooks.filter((h) => h.status === "missing");
|
|
2327
|
+
}
|
|
2328
|
+
catch {
|
|
2329
|
+
return { issues: 0, errors: 0 };
|
|
2330
|
+
}
|
|
2331
|
+
if (missing.length > 0 && !silent) {
|
|
2332
|
+
console.log("\nHook-script existence check:\n");
|
|
2333
|
+
for (const h of missing) {
|
|
2334
|
+
const msg = `hook script "${h.script}" is referenced but missing — the hook never runs.`;
|
|
2335
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${msg}`);
|
|
2336
|
+
ghAnnotate(sev === "error" ? "error" : "warning", msg);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
return {
|
|
2340
|
+
issues: missing.length,
|
|
2341
|
+
errors: sev === "error" ? missing.length : 0,
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
/**
|
|
2345
|
+
* Apply the `mcp-tool-resolves` rule: an `mcp__server__tool` in a subagent's
|
|
2346
|
+
* contract whose server isn't in the plugin's declared `mcpServers` can't resolve
|
|
2347
|
+
* (the MCP half of the tool moat). Reuses `scanPlugin`'s per-agent `mcpToolIssues`
|
|
2348
|
+
* — high-precision (gated on a declared set, built-ins allowlisted, the
|
|
2349
|
+
* plugin-namespaced form skipped). Warning by default; "error" gates CI.
|
|
2350
|
+
*/
|
|
2351
|
+
function checkMcpToolResolves(config, silent, adapter) {
|
|
2352
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-tool-resolves"]);
|
|
2353
|
+
if (!sev)
|
|
2354
|
+
return { issues: 0, errors: 0 };
|
|
2355
|
+
if (!adapter.capabilities.subagents) {
|
|
2356
|
+
reportNotApplicable("MCP tool-resolution check", "subagents", adapter, silent);
|
|
2357
|
+
return { issues: 0, errors: 0 };
|
|
2358
|
+
}
|
|
2359
|
+
let found;
|
|
2360
|
+
try {
|
|
2361
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).agents.flatMap((a) => a.mcpToolIssues.map((i) => ({ message: i.message, path: a.path })));
|
|
2362
|
+
}
|
|
2363
|
+
catch {
|
|
2364
|
+
return { issues: 0, errors: 0 };
|
|
2365
|
+
}
|
|
2366
|
+
if (found.length > 0 && !silent) {
|
|
2367
|
+
console.log("\nMCP tool-resolution check:\n");
|
|
2368
|
+
for (const issue of found) {
|
|
2369
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${issue.path}: ${issue.message}`);
|
|
2370
|
+
ghAnnotate(sev === "error" ? "error" : "warning", issue.message, issue.path);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2374
|
+
}
|
|
1934
2375
|
/**
|
|
1935
2376
|
* Apply the configured coverage thresholds. Returns the number of failing
|
|
1936
2377
|
* thresholds (so the lint can fail CI when severity is "error").
|
|
@@ -2008,6 +2449,43 @@ function findInstructionFiles(restArgs) {
|
|
|
2008
2449
|
}
|
|
2009
2450
|
return files;
|
|
2010
2451
|
}
|
|
2452
|
+
/** Value of a `--flag=value` arg (the `=` form, so it never collides with a positional). */
|
|
2453
|
+
function flagValue(args, name) {
|
|
2454
|
+
return args.find((a) => a.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
2455
|
+
}
|
|
2456
|
+
/**
|
|
2457
|
+
* The `scan --trigger` behavioral column: load the author-supplied per-skill
|
|
2458
|
+
* prompt sets, probe the plugin's model-invocable skills, print the column.
|
|
2459
|
+
* Model-gated and opt-in — the structural scan above stays deterministic.
|
|
2460
|
+
*/
|
|
2461
|
+
async function handleScanTrigger(root, args, json, harness) {
|
|
2462
|
+
const promptsPath = flagValue(args, "--prompts");
|
|
2463
|
+
if (!promptsPath) {
|
|
2464
|
+
console.error("scan --trigger needs --prompts=<file.json> (a map of skill name → { prompts, irrelevant }).");
|
|
2465
|
+
process.exitCode = 2;
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
let promptSet;
|
|
2469
|
+
try {
|
|
2470
|
+
promptSet = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(promptsPath), "utf-8"));
|
|
2471
|
+
}
|
|
2472
|
+
catch (e) {
|
|
2473
|
+
console.error(`scan --trigger: could not read --prompts file "${promptsPath}": ${e instanceof Error ? e.message : String(e)}`);
|
|
2474
|
+
process.exitCode = 2;
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
const concurrencyRaw = flagValue(args, "--concurrency");
|
|
2478
|
+
const minPromptsRaw = flagValue(args, "--min-prompts");
|
|
2479
|
+
const report = await (0, scan_behavioral_js_1.probePluginTriggers)(root, promptSet, {
|
|
2480
|
+
concurrency: concurrencyRaw ? Number(concurrencyRaw) : undefined,
|
|
2481
|
+
minPrompts: minPromptsRaw ? Number(minPromptsRaw) : undefined,
|
|
2482
|
+
model: flagValue(args, "--model"),
|
|
2483
|
+
harness,
|
|
2484
|
+
});
|
|
2485
|
+
console.log(json
|
|
2486
|
+
? JSON.stringify(report, null, 2)
|
|
2487
|
+
: `\n${(0, scan_behavioral_js_1.formatBehavioralReport)(report)}`);
|
|
2488
|
+
}
|
|
2011
2489
|
function handleGenerateTypes(args, restArgs) {
|
|
2012
2490
|
const checkOnly = args.includes("--check");
|
|
2013
2491
|
const outPath = restArgs[0] ?? ".vigiles/generated.d.ts";
|
|
@@ -2154,6 +2632,129 @@ function handleRunScripts(kind, args, restArgs) {
|
|
|
2154
2632
|
process.exit(1);
|
|
2155
2633
|
}
|
|
2156
2634
|
}
|
|
2635
|
+
/** Parse the `--harness=<name>` override out of an argv list (the one definition). */
|
|
2636
|
+
function harnessFlagFrom(argv) {
|
|
2637
|
+
return argv
|
|
2638
|
+
.find((a) => a.startsWith("--harness="))
|
|
2639
|
+
?.slice("--harness=".length);
|
|
2640
|
+
}
|
|
2641
|
+
/**
|
|
2642
|
+
* `vigiles explain <dir> [name]` — the deterministic WHY behind a low score (C4):
|
|
2643
|
+
* scan a plugin and surface the structural CAUSE of a behavioral symptom + the
|
|
2644
|
+
* one-line fix. No model — it reads the same `ScanReport` `scan` computes. An
|
|
2645
|
+
* optional surface name narrows to one underperforming skill/agent (the
|
|
2646
|
+
* optimizer's call). `--json` for the agent-consumable shape, `--harness=` to
|
|
2647
|
+
* override detection.
|
|
2648
|
+
*/
|
|
2649
|
+
function handleExplain(restArgs, args) {
|
|
2650
|
+
const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
|
|
2651
|
+
const surface = restArgs[1];
|
|
2652
|
+
const json = args.includes("--json");
|
|
2653
|
+
const harnessFlag = harnessFlagFrom(args);
|
|
2654
|
+
const adapter = harnessFlag
|
|
2655
|
+
? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
|
|
2656
|
+
: (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
|
|
2657
|
+
const report = (0, scan_js_1.scanPlugin)(dir, adapter.layout, adapter.dialect);
|
|
2658
|
+
const exps = surface ? (0, score_explainer_js_1.explainSurface)(report, surface) : (0, score_explainer_js_1.explainScore)(report);
|
|
2659
|
+
if (json) {
|
|
2660
|
+
console.log(JSON.stringify(exps, null, 2));
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
if (surface)
|
|
2664
|
+
console.log(`Explaining "${surface}":\n`);
|
|
2665
|
+
console.log((0, score_explainer_js_1.formatExplanations)(exps));
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* The plugin's declared name for the namespaced skill id, read from the layout's
|
|
2669
|
+
* manifest (adapter-aware path, not a hardcoded `.claude-plugin/`), falling back to
|
|
2670
|
+
* the dir basename. JSON manifests only for now (a TOML/Codex manifest → basename).
|
|
2671
|
+
*/
|
|
2672
|
+
function pluginNameFor(dir, manifestPath) {
|
|
2673
|
+
try {
|
|
2674
|
+
const manifest = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(dir, manifestPath), "utf-8"));
|
|
2675
|
+
if (typeof manifest.name === "string" && manifest.name)
|
|
2676
|
+
return manifest.name;
|
|
2677
|
+
}
|
|
2678
|
+
catch {
|
|
2679
|
+
/* missing / non-JSON manifest → fall back */
|
|
2680
|
+
}
|
|
2681
|
+
return (0, node_path_1.basename)(dir);
|
|
2682
|
+
}
|
|
2683
|
+
/** Enrich an untested Surface with the metadata the right template needs. */
|
|
2684
|
+
function scaffoldInputFor(s, report, pluginName) {
|
|
2685
|
+
const base = { kind: s.kind, name: s.name, path: s.path };
|
|
2686
|
+
switch (s.kind) {
|
|
2687
|
+
case "skill": {
|
|
2688
|
+
const sk = report.skills.find((x) => x.name === s.name);
|
|
2689
|
+
return { ...base, pluginName, userInvoked: sk?.userInvoked };
|
|
2690
|
+
}
|
|
2691
|
+
case "agent": {
|
|
2692
|
+
const ag = report.agents.find((x) => x.name === s.name);
|
|
2693
|
+
return { ...base, tools: ag?.tools ?? null };
|
|
2694
|
+
}
|
|
2695
|
+
case "hook":
|
|
2696
|
+
return { ...base, hookCommand: `bash ${s.path}` };
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
/**
|
|
2700
|
+
* `vigiles scaffold-test [dir]` — generate a runnable STARTER test for each
|
|
2701
|
+
* untested skill/agent/hook (B1, test-gen from free-form). Reuses the
|
|
2702
|
+
* untested-surface detector for the list + `scan` for the metadata, then emits the
|
|
2703
|
+
* cheapest meaningful tier per kind (hook → `runHook`, skill → `measureTriggerRate`,
|
|
2704
|
+
* subagent → `runHarnessTest`) at the surface's suggested test path. Dry-run by
|
|
2705
|
+
* default (prints the scaffolds); `--write` creates the files (never clobbering an
|
|
2706
|
+
* existing one); `--json` for the agent-consumable `{ path, content }[]`.
|
|
2707
|
+
*/
|
|
2708
|
+
function handleScaffoldTest(restArgs, args) {
|
|
2709
|
+
const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
|
|
2710
|
+
const write = args.includes("--write");
|
|
2711
|
+
const json = args.includes("--json");
|
|
2712
|
+
const harnessFlag = harnessFlagFrom(args);
|
|
2713
|
+
const adapter = harnessFlag
|
|
2714
|
+
? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
|
|
2715
|
+
: (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
|
|
2716
|
+
const { untested } = (0, test_coverage_js_1.findUntestedSurfaces)({
|
|
2717
|
+
basePath: dir,
|
|
2718
|
+
layout: adapter.layout,
|
|
2719
|
+
});
|
|
2720
|
+
const report = (0, scan_js_1.scanPlugin)(dir, adapter.layout, adapter.dialect);
|
|
2721
|
+
const pluginName = pluginNameFor(dir, adapter.layout.manifestPath);
|
|
2722
|
+
const scaffolds = untested.map((s) => (0, scaffold_test_js_1.scaffoldTest)(scaffoldInputFor(s, report, pluginName)));
|
|
2723
|
+
if (json) {
|
|
2724
|
+
console.log(JSON.stringify(scaffolds, null, 2));
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
if (!write) {
|
|
2728
|
+
console.log((0, scaffold_test_js_1.formatScaffolds)(scaffolds));
|
|
2729
|
+
for (const s of scaffolds) {
|
|
2730
|
+
console.log(`\n# ${s.path}\n`);
|
|
2731
|
+
console.log(s.content);
|
|
2732
|
+
}
|
|
2733
|
+
if (scaffolds.length > 0) {
|
|
2734
|
+
console.log("Re-run with --write to create these files.");
|
|
2735
|
+
}
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
const written = [];
|
|
2739
|
+
const skipped = [];
|
|
2740
|
+
for (const s of scaffolds) {
|
|
2741
|
+
const target = (0, node_path_1.resolve)(dir, s.path);
|
|
2742
|
+
if ((0, node_fs_1.existsSync)(target)) {
|
|
2743
|
+
skipped.push(s.path);
|
|
2744
|
+
continue;
|
|
2745
|
+
}
|
|
2746
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(target), { recursive: true });
|
|
2747
|
+
(0, node_fs_1.writeFileSync)(target, s.content);
|
|
2748
|
+
written.push(s.path);
|
|
2749
|
+
}
|
|
2750
|
+
for (const p of written)
|
|
2751
|
+
console.log(`✓ wrote ${p}`);
|
|
2752
|
+
for (const p of skipped)
|
|
2753
|
+
console.log(`⊘ skipped ${p} (already exists)`);
|
|
2754
|
+
if (written.length === 0 && skipped.length === 0) {
|
|
2755
|
+
console.log("Nothing to scaffold — every surface already has a test.");
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2157
2758
|
function printUsage(command) {
|
|
2158
2759
|
console.log("vigiles — compile typed specs to instruction files");
|
|
2159
2760
|
console.log("");
|
|
@@ -2163,6 +2764,8 @@ function printUsage(command) {
|
|
|
2163
2764
|
console.log(" vigiles lint [files...] Verify references, find gaps in instruction files");
|
|
2164
2765
|
console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
|
|
2165
2766
|
console.log(" vigiles eval [files...] Run *.eval.mjs real-model harness evals (--trials=N, --min=N, --no-skip)");
|
|
2767
|
+
console.log(" vigiles explain <dir> [name] Deterministic WHY a skill/agent underperforms + the fix (--json, --harness=)");
|
|
2768
|
+
console.log(" vigiles scaffold-test [dir] Generate a starter test for each untested skill/agent/hook (--write, --json)");
|
|
2166
2769
|
console.log("");
|
|
2167
2770
|
console.log("Examples:");
|
|
2168
2771
|
console.log(" vigiles init Auto-detect project, create specs, wire CI");
|
|
@@ -2272,6 +2875,42 @@ function skillStartCommand(target) {
|
|
|
2272
2875
|
(0, skill_runtime_js_1.setActiveSkill)(process.cwd(), target);
|
|
2273
2876
|
console.log(`Active skill: ${target}`);
|
|
2274
2877
|
}
|
|
2878
|
+
/**
|
|
2879
|
+
* PreToolUse-hook entrypoint: enforce the active skill's declared purity floor.
|
|
2880
|
+
* Reads the tool event on stdin, parses the `vigiles:purity:` marker from the
|
|
2881
|
+
* active skill's compiled SKILL.md, and blocks (exit 2 + reason on stderr) any
|
|
2882
|
+
* tool call that violates the declared floor — refining `Bash` by the live
|
|
2883
|
+
* command via `isReadOnlyBash`. Skills have no tools-allowlist rail; this gate
|
|
2884
|
+
* is purity-only. Mirrors `agentHookCommand` for skills.
|
|
2885
|
+
*/
|
|
2886
|
+
function skillToolHookCommand() {
|
|
2887
|
+
let raw = "";
|
|
2888
|
+
try {
|
|
2889
|
+
raw = (0, node_fs_1.readFileSync)(0, "utf-8");
|
|
2890
|
+
}
|
|
2891
|
+
catch {
|
|
2892
|
+
/* no stdin */
|
|
2893
|
+
}
|
|
2894
|
+
let tool = "";
|
|
2895
|
+
let command;
|
|
2896
|
+
try {
|
|
2897
|
+
const parsed = JSON.parse(raw);
|
|
2898
|
+
tool = parsed.tool_name ?? "";
|
|
2899
|
+
if (typeof parsed.tool_input?.command === "string") {
|
|
2900
|
+
command = parsed.tool_input.command;
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
catch {
|
|
2904
|
+
/* malformed input → no tool, allow */
|
|
2905
|
+
}
|
|
2906
|
+
if (!tool)
|
|
2907
|
+
return;
|
|
2908
|
+
const decision = (0, skill_runtime_js_1.evaluateSkillPreToolUse)(process.cwd(), tool, command);
|
|
2909
|
+
if (!decision.allow) {
|
|
2910
|
+
console.error(decision.message);
|
|
2911
|
+
process.exit(2);
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2275
2914
|
/**
|
|
2276
2915
|
* PreToolUse-hook entrypoint: enforce the active subagent's allowed-tools
|
|
2277
2916
|
* contract. Reads the tool event on stdin, parses the active agent's compiled
|
|
@@ -2287,15 +2926,20 @@ function agentHookCommand() {
|
|
|
2287
2926
|
/* no stdin */
|
|
2288
2927
|
}
|
|
2289
2928
|
let tool = "";
|
|
2929
|
+
let command;
|
|
2290
2930
|
try {
|
|
2291
|
-
|
|
2931
|
+
const parsed = JSON.parse(raw);
|
|
2932
|
+
tool = parsed.tool_name ?? "";
|
|
2933
|
+
if (typeof parsed.tool_input?.command === "string") {
|
|
2934
|
+
command = parsed.tool_input.command;
|
|
2935
|
+
}
|
|
2292
2936
|
}
|
|
2293
2937
|
catch {
|
|
2294
2938
|
/* malformed input → no tool, allow */
|
|
2295
2939
|
}
|
|
2296
2940
|
if (!tool)
|
|
2297
2941
|
return;
|
|
2298
|
-
const decision = (0, agent_runtime_js_1.evaluatePreToolUse)(process.cwd(), tool);
|
|
2942
|
+
const decision = (0, agent_runtime_js_1.evaluatePreToolUse)(process.cwd(), tool, command);
|
|
2299
2943
|
if (!decision.allow) {
|
|
2300
2944
|
console.error(decision.message);
|
|
2301
2945
|
process.exit(2);
|
|
@@ -2349,6 +2993,9 @@ function handleSkillCommand(command, restArgs) {
|
|
|
2349
2993
|
case "skill-hook":
|
|
2350
2994
|
skillHookCommand();
|
|
2351
2995
|
return true;
|
|
2996
|
+
case "skill-tool-hook":
|
|
2997
|
+
skillToolHookCommand();
|
|
2998
|
+
return true;
|
|
2352
2999
|
case "agent-start":
|
|
2353
3000
|
agentStartCommand(restArgs[0]);
|
|
2354
3001
|
return true;
|
|
@@ -2370,6 +3017,13 @@ function handleSkillCommand(command, restArgs) {
|
|
|
2370
3017
|
case "refs-hook":
|
|
2371
3018
|
refsHookCommand();
|
|
2372
3019
|
return true;
|
|
3020
|
+
case "effect-enter":
|
|
3021
|
+
(0, effect_region_js_1.setEffectActive)(process.cwd());
|
|
3022
|
+
console.log("Effect boundary entered.");
|
|
3023
|
+
return true;
|
|
3024
|
+
case "effect-exit":
|
|
3025
|
+
(0, effect_region_js_1.clearEffectActive)(process.cwd());
|
|
3026
|
+
return true;
|
|
2373
3027
|
default:
|
|
2374
3028
|
return false;
|
|
2375
3029
|
}
|
|
@@ -2540,9 +3194,7 @@ async function main() {
|
|
|
2540
3194
|
console.log("Run `vigiles init` to create one.");
|
|
2541
3195
|
process.exit(0);
|
|
2542
3196
|
}
|
|
2543
|
-
const harnessFlag = args
|
|
2544
|
-
.find((a) => a.startsWith("--harness="))
|
|
2545
|
-
?.slice("--harness=".length);
|
|
3197
|
+
const harnessFlag = harnessFlagFrom(args);
|
|
2546
3198
|
const valid = await compile(specs, config, { harnessFlag });
|
|
2547
3199
|
console.log("");
|
|
2548
3200
|
if (valid) {
|
|
@@ -2574,21 +3226,42 @@ async function main() {
|
|
|
2574
3226
|
case "scan": {
|
|
2575
3227
|
const dirs = restArgs.length > 0 ? restArgs : ["."];
|
|
2576
3228
|
const json = args.includes("--json");
|
|
2577
|
-
|
|
3229
|
+
// A single dir that's a marketplace (e.g. wshobson/agents' 80+ plugins
|
|
3230
|
+
// under one marketplace.json) expands into its members and ranks them.
|
|
3231
|
+
const market = dirs.length === 1 ? (0, scan_js_1.inspectMarketplace)((0, node_path_1.resolve)(dirs[0])) : null;
|
|
3232
|
+
const targets = market && market.onDisk.length > 0 ? [...market.onDisk] : dirs;
|
|
3233
|
+
const wantTrigger = args.includes("--trigger");
|
|
3234
|
+
if (market && market.onDisk.length === 0 && market.total > 0) {
|
|
3235
|
+
// A CURATED marketplace — every member is an external git/url plugin, so
|
|
3236
|
+
// there's nothing on disk to scan. Say so honestly instead of falling
|
|
3237
|
+
// through to a misleading "empty machine / no structural issues" report
|
|
3238
|
+
// (obra/superpowers-marketplace, anthropics/claude-plugins-community).
|
|
3239
|
+
if (json) {
|
|
3240
|
+
console.log(JSON.stringify(market, null, 2));
|
|
3241
|
+
}
|
|
3242
|
+
else {
|
|
3243
|
+
console.log(`Marketplace "${market.name}": ${String(market.total)} plugin(s), all external ` +
|
|
3244
|
+
`(url/git sources, not on disk).\n` +
|
|
3245
|
+
`Nothing to scan here — clone a member plugin and scan that, or scan a ` +
|
|
3246
|
+
`marketplace that vendors its plugins in-tree.`);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
else if (targets.length > 1) {
|
|
2578
3250
|
// Multiple targets → rank them (the leaderboard engine).
|
|
2579
|
-
const scores = (0, leaderboard_js_1.rankPlugins)(
|
|
3251
|
+
const scores = (0, leaderboard_js_1.rankPlugins)(targets);
|
|
2580
3252
|
console.log(json ? JSON.stringify(scores, null, 2) : (0, leaderboard_js_1.formatLeaderboard)(scores));
|
|
3253
|
+
if (wantTrigger) {
|
|
3254
|
+
console.log("\n⚠ --trigger (behavioral column) runs per single plugin; not yet wired into the leaderboard. Scan one plugin dir to probe it.");
|
|
3255
|
+
}
|
|
2581
3256
|
}
|
|
2582
3257
|
else {
|
|
2583
|
-
const root = (0, node_path_1.resolve)(
|
|
2584
|
-
const harnessFlag = args
|
|
2585
|
-
.find((a) => a.startsWith("--harness="))
|
|
2586
|
-
?.slice("--harness=".length);
|
|
3258
|
+
const root = (0, node_path_1.resolve)(targets[0]);
|
|
3259
|
+
const harnessFlag = harnessFlagFrom(args);
|
|
2587
3260
|
const det = (0, adapter_registry_js_1.detectAdapterResult)(root);
|
|
2588
3261
|
const adapter = harnessFlag
|
|
2589
3262
|
? (0, adapter_registry_js_1.resolveAdapter)(root, harnessFlag)
|
|
2590
3263
|
: det.adapter;
|
|
2591
|
-
const report = (0, scan_js_1.scanPlugin)(
|
|
3264
|
+
const report = (0, scan_js_1.scanPlugin)(targets[0], adapter.layout, adapter.dialect);
|
|
2592
3265
|
if (!json) {
|
|
2593
3266
|
console.log(`Detected harness: ${adapter.name}`);
|
|
2594
3267
|
if (!harnessFlag && det.ambiguousWith.length > 0) {
|
|
@@ -2596,10 +3269,28 @@ async function main() {
|
|
|
2596
3269
|
}
|
|
2597
3270
|
console.log("");
|
|
2598
3271
|
}
|
|
2599
|
-
|
|
3272
|
+
if (args.includes("--fix-plan")) {
|
|
3273
|
+
// The deterministic optimization lens on the SAME report: health score
|
|
3274
|
+
// + the ranked free fixes to clear before measuring (the A2 spine).
|
|
3275
|
+
const plan = (0, optimize_js_1.optimize)(report);
|
|
3276
|
+
console.log(json ? JSON.stringify(plan, null, 2) : (0, optimize_js_1.formatOptimize)(plan));
|
|
3277
|
+
}
|
|
3278
|
+
else {
|
|
3279
|
+
console.log(json ? JSON.stringify(report, null, 2) : (0, scan_js_1.formatScanReport)(report));
|
|
3280
|
+
}
|
|
3281
|
+
if (wantTrigger) {
|
|
3282
|
+
const harness = adapter.name === "codex" ? "codex" : "claude-code";
|
|
3283
|
+
await handleScanTrigger(root, args, json, harness);
|
|
3284
|
+
}
|
|
2600
3285
|
}
|
|
2601
3286
|
break;
|
|
2602
3287
|
}
|
|
3288
|
+
case "explain":
|
|
3289
|
+
handleExplain(restArgs, args);
|
|
3290
|
+
break;
|
|
3291
|
+
case "scaffold-test":
|
|
3292
|
+
handleScaffoldTest(restArgs, args);
|
|
3293
|
+
break;
|
|
2603
3294
|
// --- Plumbing ---
|
|
2604
3295
|
case "generate-types":
|
|
2605
3296
|
handleGenerateTypes(args, restArgs);
|