vigiles 5.1.0 → 6.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/README.md +59 -18
- package/dist/adapters/claude-code/adapter.js +1 -0
- package/dist/adapters/claude-code/agent-runtime.d.ts +45 -6
- package/dist/adapters/claude-code/agent-runtime.js +94 -8
- package/dist/adapters/claude-code/dialect.d.ts +34 -0
- package/dist/adapters/claude-code/dialect.js +51 -19
- 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 +40 -0
- package/dist/adapters/claude-code/typed-spec.d.ts +58 -0
- package/dist/adapters/claude-code/typed-spec.js +55 -0
- package/dist/adapters/codex/adapter.js +3 -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/claude-code.d.ts +1 -0
- package/dist/claude-code.js +8 -1
- package/dist/cli.js +469 -88
- 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 +176 -39
- package/dist/core/dialect.d.ts +10 -0
- package/dist/core/effects.d.ts +172 -0
- package/dist/core/effects.js +245 -0
- package/dist/core/generate-harness.d.ts +187 -0
- package/dist/core/generate-harness.js +337 -0
- package/dist/core/layout.d.ts +6 -0
- package/dist/core/mcp-tool.d.ts +1 -1
- package/dist/core/orphans.js +21 -0
- package/dist/core/spec.d.ts +432 -11
- package/dist/core/spec.js +166 -3
- package/dist/core/tool-contract.d.ts +1 -1
- package/dist/core/types.d.ts +6 -6
- package/dist/core/validate.js +4 -4
- 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 +2 -0
- package/dist/optimize.d.ts +74 -0
- package/dist/optimize.js +94 -0
- package/dist/scaffold-test.d.ts +58 -0
- package/dist/scaffold-test.js +263 -0
- package/dist/scan.d.ts +40 -0
- package/dist/scan.js +91 -43
- 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 -4
- package/skills/edit-spec/SKILL.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -14,19 +14,21 @@ const node_fs_1 = require("node:fs");
|
|
|
14
14
|
const node_path_1 = require("node:path");
|
|
15
15
|
const glob_1 = require("glob");
|
|
16
16
|
const generate_types_js_1 = require("./core/generate-types.js");
|
|
17
|
+
const generate_harness_js_1 = require("./core/generate-harness.js");
|
|
17
18
|
const validate_js_1 = require("./core/validate.js");
|
|
18
19
|
const cli_flags_js_1 = require("./cli-flags.js");
|
|
19
20
|
const setup_plan_js_1 = require("./setup-plan.js");
|
|
20
21
|
const types_js_1 = require("./core/types.js");
|
|
21
22
|
const test_coverage_js_1 = require("./test-coverage.js");
|
|
23
|
+
const scaffold_test_js_1 = require("./scaffold-test.js");
|
|
24
|
+
const effects_js_1 = require("./core/effects.js");
|
|
22
25
|
const scan_js_1 = require("./scan.js");
|
|
23
|
-
const
|
|
24
|
-
const agent_runtime_js_1 = require("./adapters/claude-code/agent-runtime.js");
|
|
25
|
-
const dialect_js_1 = require("./adapters/claude-code/dialect.js");
|
|
26
|
+
const score_explainer_js_1 = require("./score-explainer.js");
|
|
26
27
|
const scan_behavioral_js_1 = require("./scan-behavioral.js");
|
|
27
28
|
const adapter_registry_js_1 = require("./adapter-registry.js");
|
|
28
29
|
const skill_harness_js_1 = require("./skill-harness.js");
|
|
29
30
|
const leaderboard_js_1 = require("./leaderboard.js");
|
|
31
|
+
const optimize_js_1 = require("./optimize.js");
|
|
30
32
|
const compile_js_1 = require("./core/compile.js");
|
|
31
33
|
const proofs_js_1 = require("./core/proofs.js");
|
|
32
34
|
const inline_js_1 = require("./core/inline.js");
|
|
@@ -35,7 +37,8 @@ const generate_schema_js_1 = require("./core/generate-schema.js");
|
|
|
35
37
|
const compose_js_1 = require("./core/compose.js");
|
|
36
38
|
const compile_generator_js_1 = require("./core/compile-generator.js");
|
|
37
39
|
const action_gate_js_1 = require("./action-gate.js");
|
|
38
|
-
const
|
|
40
|
+
const agent_runtime_js_1 = require("./adapters/claude-code/agent-runtime.js");
|
|
41
|
+
const effect_region_js_1 = require("./adapters/claude-code/effect-region.js");
|
|
39
42
|
const tool_intercept_js_1 = require("./tool-intercept.js");
|
|
40
43
|
const refs_js_1 = require("./core/refs.js");
|
|
41
44
|
const mcp_js_1 = require("./core/mcp.js");
|
|
@@ -869,6 +872,17 @@ async function runLint(restArgs, flags, config) {
|
|
|
869
872
|
const json = flags.includes("--json");
|
|
870
873
|
const silent = summary || json;
|
|
871
874
|
const files = findInstructionFiles(restArgs);
|
|
875
|
+
// Resolve the active harness ONCE so the harness-specific checks below run
|
|
876
|
+
// against the right adapter's dialect (tool/event catalogs) and surfaces —
|
|
877
|
+
// not a hard-coded Claude Code default. A subagent-surface rule reports n/a
|
|
878
|
+
// on a harness without subagents (Codex) rather than scanning nothing.
|
|
879
|
+
const harnessFlag = harnessFlagFrom(flags);
|
|
880
|
+
const lintSelection = (0, adapter_registry_js_1.resolveHarnessSelection)({
|
|
881
|
+
root: process.cwd(),
|
|
882
|
+
flag: harnessFlag,
|
|
883
|
+
configHarness: (0, adapter_registry_js_1.normalizeHarnessList)(config?.harness),
|
|
884
|
+
});
|
|
885
|
+
const adapter = lintSelection.adapter;
|
|
872
886
|
// 1. Verify hashes and structure
|
|
873
887
|
if (!silent) {
|
|
874
888
|
if (files.length > 0) {
|
|
@@ -927,40 +941,41 @@ async function runLint(restArgs, flags, config) {
|
|
|
927
941
|
// 7b. Untested-surface check — skills/agents/hooks shipping without a test or
|
|
928
942
|
// eval. Warning by default (a nudge, exit 0); set rules.untested-{skill,agent,
|
|
929
943
|
// hook} to "error" to gate CI. See src/test-coverage.ts and docs/rules/.
|
|
930
|
-
const untested = checkUntestedSurfaces(config, silent);
|
|
931
|
-
// 7c.
|
|
932
|
-
// against the harness catalog (the moat).
|
|
933
|
-
//
|
|
934
|
-
|
|
944
|
+
const untested = checkUntestedSurfaces(config, silent, adapter);
|
|
945
|
+
// 7c. Subagent tool-contract check — cross-reference each subagent's `tools:`
|
|
946
|
+
// rail against the harness catalog (the moat). n/a on a harness with no
|
|
947
|
+
// subagents. Off by default unless a severity is configured; warning surfaces
|
|
948
|
+
// a typo/never-available tool, error gates CI.
|
|
949
|
+
const toolContract = checkSubagentToolContracts(config, silent, adapter);
|
|
935
950
|
// 7d. Hook-event check — a hook registered under an event the harness doesn't
|
|
936
951
|
// define never fires. High-precision (close typos only). Off unless configured.
|
|
937
|
-
const hookEvents = checkHookEvents(config, silent);
|
|
938
|
-
// 7e.
|
|
939
|
-
// (name
|
|
940
|
-
const frontmatter = checkFrontmatterSchema(config, silent);
|
|
952
|
+
const hookEvents = checkHookEvents(config, silent, adapter);
|
|
953
|
+
// 7e. Subagent-frontmatter check — a subagent missing required frontmatter
|
|
954
|
+
// (name + description) won't register. n/a on a harness with no subagents.
|
|
955
|
+
const frontmatter = checkFrontmatterSchema(config, silent, adapter);
|
|
941
956
|
// 7f. MCP-config check — a declared MCP server with no command/url can't start.
|
|
942
|
-
const mcpConfig = checkMcpConfig(config, silent);
|
|
957
|
+
const mcpConfig = checkMcpConfig(config, silent, adapter);
|
|
943
958
|
// 7g. Skill-frontmatter — RECOMMEND explicit name/description on skills (a
|
|
944
959
|
// reliable trigger surface). Best-practice nudge; skills load without it.
|
|
945
|
-
const skillFm = checkSkillFrontmatter(config, silent);
|
|
960
|
+
const skillFm = checkSkillFrontmatter(config, silent, adapter);
|
|
946
961
|
// 7h. MCP tool-resolution — an `mcp__server__tool` in a contract whose server
|
|
947
962
|
// the plugin doesn't declare can't resolve (the MCP half of the tool moat).
|
|
948
|
-
const mcpToolResolves = checkMcpToolResolves(config, silent);
|
|
963
|
+
const mcpToolResolves = checkMcpToolResolves(config, silent, adapter);
|
|
949
964
|
// 7i. Hook-script existence — a hook command referencing a missing script file
|
|
950
965
|
// never runs (matches Anthropic's own `claude plugin validate`).
|
|
951
|
-
const hookScripts = checkHookScriptExists(config, silent);
|
|
966
|
+
const hookScripts = checkHookScriptExists(config, silent, adapter);
|
|
952
967
|
// 7j. Disallowed-tools — a `disallowedTools:` block-list typo blocks nothing
|
|
953
|
-
// (the deny-side mirror of
|
|
954
|
-
const disallowedTools = checkDisallowedTools(config, silent);
|
|
968
|
+
// (the deny-side mirror of subagent-tool-contract; close-typo only).
|
|
969
|
+
const disallowedTools = checkDisallowedTools(config, silent, adapter);
|
|
955
970
|
// 7k. Description-overlap — two model-invocable skills with near-identical
|
|
956
971
|
// descriptions collide in the selector (deterministic NCD precision proxy).
|
|
957
|
-
const descriptionOverlap = checkDescriptionOverlap(config, silent);
|
|
972
|
+
const descriptionOverlap = checkDescriptionOverlap(config, silent, adapter);
|
|
958
973
|
// 7l. Frontmatter-valid — a `---` block that isn't valid YAML (warn; js-yaml is
|
|
959
974
|
// stricter than some loaders, so verify before enforcing).
|
|
960
|
-
const frontmatterValid = checkFrontmatterValid(config, silent);
|
|
975
|
+
const frontmatterValid = checkFrontmatterValid(config, silent, adapter);
|
|
961
976
|
// 7m. MCP hook-target — a `type: mcp_tool` hook action that's incomplete or
|
|
962
977
|
// targets an undeclared server (the moat applied to the hook surface).
|
|
963
|
-
const mcpHookTargets = checkMcpHookTargets(config, silent);
|
|
978
|
+
const mcpHookTargets = checkMcpHookTargets(config, silent, adapter);
|
|
964
979
|
// 8. Validate vigiles builder calls inside markdown code blocks. Default
|
|
965
980
|
// is to validate every ref; illustrative blocks opt out via
|
|
966
981
|
// `<!-- vigiles:ignore -->` (single block) or
|
|
@@ -1594,7 +1609,7 @@ function redirectSyncToolTargets(cwd, targets) {
|
|
|
1594
1609
|
}
|
|
1595
1610
|
/** Pillar 1 — specs + types + schema + compile. Scaffolds a spec for every
|
|
1596
1611
|
* instruction file (so `--lint` always delivers a spec), but never compiles
|
|
1597
|
-
* OVER a hand-written file — that is left to the
|
|
1612
|
+
* OVER a hand-written file — that is left to the adopt-spec skill. */
|
|
1598
1613
|
async function setupPillar1(detected, targetValue, harnesses) {
|
|
1599
1614
|
const cwd = process.cwd();
|
|
1600
1615
|
const written = [];
|
|
@@ -1619,7 +1634,7 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
1619
1634
|
}
|
|
1620
1635
|
if (targetExists && !targetHasHash((0, node_path_1.resolve)(cwd, target))) {
|
|
1621
1636
|
needsMigration.push(target);
|
|
1622
|
-
console.log(` ${target} already has content —
|
|
1637
|
+
console.log(` ${target} already has content — adopt it into a spec with the adopt-spec skill, then \`vigiles compile\`.`);
|
|
1623
1638
|
}
|
|
1624
1639
|
}
|
|
1625
1640
|
// Generate types + schema.
|
|
@@ -1811,7 +1826,7 @@ function printSetupSummary(opts) {
|
|
|
1811
1826
|
console.log("\n---\nSetup complete.\n");
|
|
1812
1827
|
const nextSteps = [];
|
|
1813
1828
|
if (needsMigration.length > 0) {
|
|
1814
|
-
nextSteps.push(`
|
|
1829
|
+
nextSteps.push(`Adopt ${needsMigration.join(", ")} into a spec with the adopt-spec skill, then \`npx vigiles compile\``);
|
|
1815
1830
|
}
|
|
1816
1831
|
else if (specPathsList.length > 0) {
|
|
1817
1832
|
nextSteps.push(`Edit ${specPathsList.join(", ")} — add your conventions, then \`/strengthen\``);
|
|
@@ -1958,16 +1973,16 @@ function checkIntegrityForFiles(files, severity, silent) {
|
|
|
1958
1973
|
return severity === "error" ? errorCount : 0;
|
|
1959
1974
|
}
|
|
1960
1975
|
/**
|
|
1961
|
-
* Apply the per-kind `untested-skill` / `untested-
|
|
1976
|
+
* Apply the per-kind `untested-skill` / `untested-subagent` / `untested-hook` rules:
|
|
1962
1977
|
* find skills/agents/hooks with no test or eval (see src/test-coverage.ts). Each
|
|
1963
1978
|
* kind is gated by its OWN rule severity — a kind set to `false` is not scanned;
|
|
1964
1979
|
* "warn" prints but never fails CI; "error" fails (exit 2). Returns the raw
|
|
1965
1980
|
* untested count plus the severity-gated error count.
|
|
1966
1981
|
*/
|
|
1967
|
-
function checkUntestedSurfaces(config, silent) {
|
|
1982
|
+
function checkUntestedSurfaces(config, silent, adapter) {
|
|
1968
1983
|
const rules = config?.rules;
|
|
1969
1984
|
const skillSev = (0, types_js_1.ruleSeverity)(rules?.["untested-skill"]);
|
|
1970
|
-
const agentSev = (0, types_js_1.ruleSeverity)(rules?.["untested-
|
|
1985
|
+
const agentSev = (0, types_js_1.ruleSeverity)(rules?.["untested-subagent"]);
|
|
1971
1986
|
const hookSev = (0, types_js_1.ruleSeverity)(rules?.["untested-hook"]);
|
|
1972
1987
|
if (!skillSev && !agentSev && !hookSev)
|
|
1973
1988
|
return { untested: 0, errors: 0 };
|
|
@@ -1976,11 +1991,12 @@ function checkUntestedSurfaces(config, silent) {
|
|
|
1976
1991
|
// whichever of the three rules carries them.
|
|
1977
1992
|
const opts = {
|
|
1978
1993
|
...(0, types_js_1.ruleOptions)(rules?.["untested-skill"]),
|
|
1979
|
-
...(0, types_js_1.ruleOptions)(rules?.["untested-
|
|
1994
|
+
...(0, types_js_1.ruleOptions)(rules?.["untested-subagent"]),
|
|
1980
1995
|
...(0, types_js_1.ruleOptions)(rules?.["untested-hook"]),
|
|
1981
1996
|
};
|
|
1982
1997
|
const report = (0, test_coverage_js_1.findUntestedSurfaces)({
|
|
1983
1998
|
basePath: process.cwd(),
|
|
1999
|
+
layout: adapter.layout,
|
|
1984
2000
|
skills: skillSev !== false,
|
|
1985
2001
|
agents: agentSev !== false,
|
|
1986
2002
|
hooks: hookSev !== false,
|
|
@@ -2002,47 +2018,58 @@ function checkUntestedSurfaces(config, silent) {
|
|
|
2002
2018
|
};
|
|
2003
2019
|
}
|
|
2004
2020
|
/**
|
|
2005
|
-
*
|
|
2021
|
+
* A surface-scoped rule (subagent / shell-hook) is configured, but the active
|
|
2022
|
+
* harness doesn't have that surface. Report it as **n/a** — loud, not silent (the
|
|
2023
|
+
* no-silent-skips ethos): the rule isn't failing and isn't passing, it simply
|
|
2024
|
+
* doesn't apply to this harness. Never counts toward issues/errors.
|
|
2025
|
+
*/
|
|
2026
|
+
function reportNotApplicable(check, surface, adapter, silent) {
|
|
2027
|
+
if (silent)
|
|
2028
|
+
return;
|
|
2029
|
+
console.log(`\n${check}:\n`);
|
|
2030
|
+
console.log(` – n/a — ${adapter.name} has no ${surface}`);
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Apply the `subagent-tool-contract` rule: cross-reference every subagent's `tools:`
|
|
2006
2034
|
* rail against the harness tool catalog (the moat — "valid is not true"). Flags
|
|
2007
2035
|
* only the HIGH-CONFIDENCE issues (a never-available tool, or a close typo) via
|
|
2008
2036
|
* the shared `confidentToolIssues` detector — the same code `scan` and
|
|
2009
2037
|
* `compileAgent` use (one-detector-no-drift), so a bare unrecognized tool
|
|
2010
2038
|
* (plugin/MCP-provided) is never a false alarm. Warning by default; set
|
|
2011
|
-
* `
|
|
2039
|
+
* `subagent-tool-contract: "error"` to gate CI. Returns the issue + error counts.
|
|
2012
2040
|
*/
|
|
2013
|
-
function
|
|
2014
|
-
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["
|
|
2041
|
+
function checkSubagentToolContracts(config, silent, adapter) {
|
|
2042
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-tool-contract"]);
|
|
2015
2043
|
if (!sev)
|
|
2016
2044
|
return { issues: 0, errors: 0 };
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
}
|
|
2045
|
+
if (!adapter.capabilities.subagents) {
|
|
2046
|
+
reportNotApplicable("Subagent tool-contract check", "subagents", adapter, silent);
|
|
2047
|
+
return { issues: 0, errors: 0 };
|
|
2048
|
+
}
|
|
2049
|
+
// Reuse the loader's already-resolved, layout+dialect-driven agents (the same
|
|
2050
|
+
// `scan` detector — one-detector-no-drift) instead of re-globbing a hard-coded
|
|
2051
|
+
// `agents/` path, so a harness with a different subagent dir Just Works.
|
|
2052
|
+
let agents;
|
|
2053
|
+
try {
|
|
2054
|
+
agents = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).agents;
|
|
2055
|
+
}
|
|
2056
|
+
catch {
|
|
2057
|
+
return { issues: 0, errors: 0 };
|
|
2058
|
+
}
|
|
2021
2059
|
let issues = 0;
|
|
2022
2060
|
let printedHeader = false;
|
|
2023
|
-
for (const
|
|
2024
|
-
|
|
2025
|
-
try {
|
|
2026
|
-
md = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), rel), "utf-8");
|
|
2027
|
-
}
|
|
2028
|
-
catch {
|
|
2061
|
+
for (const agent of agents) {
|
|
2062
|
+
if (agent.toolIssues.length === 0)
|
|
2029
2063
|
continue;
|
|
2030
|
-
|
|
2031
|
-
const tools = (0, agent_runtime_js_1.parseAgentTools)(md);
|
|
2032
|
-
if (tools === null)
|
|
2033
|
-
continue; // no contract → inherits all (a different rule)
|
|
2034
|
-
const found = (0, tool_contract_js_1.confidentToolIssues)((0, tool_contract_js_1.verifyToolContract)(tools, dialect_js_1.claudeCodeDialect));
|
|
2035
|
-
if (found.length === 0)
|
|
2036
|
-
continue;
|
|
2037
|
-
issues += found.length;
|
|
2064
|
+
issues += agent.toolIssues.length;
|
|
2038
2065
|
if (!silent) {
|
|
2039
2066
|
if (!printedHeader) {
|
|
2040
|
-
console.log("\
|
|
2067
|
+
console.log("\nSubagent tool-contract check:\n");
|
|
2041
2068
|
printedHeader = true;
|
|
2042
2069
|
}
|
|
2043
|
-
for (const issue of
|
|
2044
|
-
console.log(` ${sev === "error" ? "✗" : "⚠"} ${
|
|
2045
|
-
ghAnnotate(sev === "error" ? "error" : "warning", issue.message,
|
|
2070
|
+
for (const issue of agent.toolIssues) {
|
|
2071
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${agent.path}: ${issue.message}`);
|
|
2072
|
+
ghAnnotate(sev === "error" ? "error" : "warning", issue.message, agent.path);
|
|
2046
2073
|
}
|
|
2047
2074
|
}
|
|
2048
2075
|
}
|
|
@@ -2054,13 +2081,17 @@ function checkAgentToolContracts(config, silent) {
|
|
|
2054
2081
|
* `hookEventIssues` (the shared detector, high-precision: close typos only, never
|
|
2055
2082
|
* a framework/custom event). Warning by default; "error" gates CI.
|
|
2056
2083
|
*/
|
|
2057
|
-
function checkHookEvents(config, silent) {
|
|
2084
|
+
function checkHookEvents(config, silent, adapter) {
|
|
2058
2085
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-events"]);
|
|
2059
2086
|
if (!sev)
|
|
2060
2087
|
return { issues: 0, errors: 0 };
|
|
2088
|
+
if (!adapter.capabilities.shellHooks) {
|
|
2089
|
+
reportNotApplicable("Hook-event check", "shell hooks", adapter, silent);
|
|
2090
|
+
return { issues: 0, errors: 0 };
|
|
2091
|
+
}
|
|
2061
2092
|
let found;
|
|
2062
2093
|
try {
|
|
2063
|
-
found = (0, scan_js_1.scanPlugin)(process.cwd()).hookEventIssues;
|
|
2094
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hookEventIssues;
|
|
2064
2095
|
}
|
|
2065
2096
|
catch {
|
|
2066
2097
|
return { issues: 0, errors: 0 };
|
|
@@ -2075,19 +2106,23 @@ function checkHookEvents(config, silent) {
|
|
|
2075
2106
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2076
2107
|
}
|
|
2077
2108
|
/**
|
|
2078
|
-
* Apply the `
|
|
2109
|
+
* Apply the `subagent-frontmatter` rule. Two kinds of subagent-frontmatter defect, one
|
|
2079
2110
|
* rule: (1) a subagent MISSING a required field (`name`/`description`) — it won't
|
|
2080
2111
|
* register; (2) a subagent with an INVALID `model:`/`color:` value (a close typo
|
|
2081
2112
|
* of a real one) — it silently falls back / is ignored. Reuses `scanPlugin`'s
|
|
2082
2113
|
* `frontmatterIssues` + `frontmatterValueIssues`. Warning by default; "error" gates CI.
|
|
2083
2114
|
*/
|
|
2084
|
-
function checkFrontmatterSchema(config, silent) {
|
|
2085
|
-
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["
|
|
2115
|
+
function checkFrontmatterSchema(config, silent, adapter) {
|
|
2116
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-frontmatter"]);
|
|
2086
2117
|
if (!sev)
|
|
2087
2118
|
return { issues: 0, errors: 0 };
|
|
2119
|
+
if (!adapter.capabilities.subagents) {
|
|
2120
|
+
reportNotApplicable("Subagent-frontmatter check", "subagents", adapter, silent);
|
|
2121
|
+
return { issues: 0, errors: 0 };
|
|
2122
|
+
}
|
|
2088
2123
|
let found;
|
|
2089
2124
|
try {
|
|
2090
|
-
const r = (0, scan_js_1.scanPlugin)(process.cwd());
|
|
2125
|
+
const r = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect);
|
|
2091
2126
|
found = [...r.frontmatterIssues, ...r.frontmatterValueIssues];
|
|
2092
2127
|
}
|
|
2093
2128
|
catch {
|
|
@@ -2110,13 +2145,13 @@ function checkFrontmatterSchema(config, silent) {
|
|
|
2110
2145
|
* default; set "error" to enforce it on your own skills. Reuses `scanPlugin`'s
|
|
2111
2146
|
* `skillMetaIssues`.
|
|
2112
2147
|
*/
|
|
2113
|
-
function checkSkillFrontmatter(config, silent) {
|
|
2148
|
+
function checkSkillFrontmatter(config, silent, adapter) {
|
|
2114
2149
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-frontmatter"]);
|
|
2115
2150
|
if (!sev)
|
|
2116
2151
|
return { issues: 0, errors: 0 };
|
|
2117
2152
|
let found;
|
|
2118
2153
|
try {
|
|
2119
|
-
found = (0, scan_js_1.scanPlugin)(process.cwd()).skillMetaIssues;
|
|
2154
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).skillMetaIssues;
|
|
2120
2155
|
}
|
|
2121
2156
|
catch {
|
|
2122
2157
|
return { issues: 0, errors: 0 };
|
|
@@ -2135,13 +2170,13 @@ function checkSkillFrontmatter(config, silent) {
|
|
|
2135
2170
|
* (stdio) nor a `url` (http/sse) can't start. Reuses `scanPlugin`'s `mcpIssues`.
|
|
2136
2171
|
* Warning by default; "error" gates CI.
|
|
2137
2172
|
*/
|
|
2138
|
-
function checkMcpConfig(config, silent) {
|
|
2173
|
+
function checkMcpConfig(config, silent, adapter) {
|
|
2139
2174
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-config"]);
|
|
2140
2175
|
if (!sev)
|
|
2141
2176
|
return { issues: 0, errors: 0 };
|
|
2142
2177
|
let found;
|
|
2143
2178
|
try {
|
|
2144
|
-
found = (0, scan_js_1.scanPlugin)(process.cwd()).mcpIssues;
|
|
2179
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).mcpIssues;
|
|
2145
2180
|
}
|
|
2146
2181
|
catch {
|
|
2147
2182
|
return { issues: 0, errors: 0 };
|
|
@@ -2162,13 +2197,17 @@ function checkMcpConfig(config, silent) {
|
|
|
2162
2197
|
* `disallowedToolIssues` (close-typo only — high-precision). Warning by default;
|
|
2163
2198
|
* "error" gates CI.
|
|
2164
2199
|
*/
|
|
2165
|
-
function checkDisallowedTools(config, silent) {
|
|
2200
|
+
function checkDisallowedTools(config, silent, adapter) {
|
|
2166
2201
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["disallowed-tools-contract"]);
|
|
2167
2202
|
if (!sev)
|
|
2168
2203
|
return { issues: 0, errors: 0 };
|
|
2204
|
+
if (!adapter.capabilities.subagents) {
|
|
2205
|
+
reportNotApplicable("Disallowed-tools check", "subagents", adapter, silent);
|
|
2206
|
+
return { issues: 0, errors: 0 };
|
|
2207
|
+
}
|
|
2169
2208
|
let found;
|
|
2170
2209
|
try {
|
|
2171
|
-
found = (0, scan_js_1.scanPlugin)(process.cwd()).agents.flatMap((a) => a.disallowedToolIssues.map((i) => ({ message: i.message, path: a.path })));
|
|
2210
|
+
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 })));
|
|
2172
2211
|
}
|
|
2173
2212
|
catch {
|
|
2174
2213
|
return { issues: 0, errors: 0 };
|
|
@@ -2190,13 +2229,13 @@ function checkDisallowedTools(config, silent) {
|
|
|
2190
2229
|
* colon / `<example>` is flagged though it may still load — hence WARN by default
|
|
2191
2230
|
* (verify before setting "error").
|
|
2192
2231
|
*/
|
|
2193
|
-
function checkFrontmatterValid(config, silent) {
|
|
2232
|
+
function checkFrontmatterValid(config, silent, adapter) {
|
|
2194
2233
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["frontmatter-valid"]);
|
|
2195
2234
|
if (!sev)
|
|
2196
2235
|
return { issues: 0, errors: 0 };
|
|
2197
2236
|
let found;
|
|
2198
2237
|
try {
|
|
2199
|
-
found = (0, scan_js_1.scanPlugin)(process.cwd()).malformedFrontmatter;
|
|
2238
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).malformedFrontmatter;
|
|
2200
2239
|
}
|
|
2201
2240
|
catch {
|
|
2202
2241
|
return { issues: 0, errors: 0 };
|
|
@@ -2217,13 +2256,13 @@ function checkFrontmatterValid(config, silent) {
|
|
|
2217
2256
|
* `scanPlugin`'s `descriptionOverlaps` (calibrated FP-safe: only basically
|
|
2218
2257
|
* identical text). Warning by default; "error" gates CI.
|
|
2219
2258
|
*/
|
|
2220
|
-
function checkDescriptionOverlap(config, silent) {
|
|
2259
|
+
function checkDescriptionOverlap(config, silent, adapter) {
|
|
2221
2260
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["description-overlap"]);
|
|
2222
2261
|
if (!sev)
|
|
2223
2262
|
return { issues: 0, errors: 0 };
|
|
2224
2263
|
let found;
|
|
2225
2264
|
try {
|
|
2226
|
-
found = (0, scan_js_1.scanPlugin)(process.cwd()).descriptionOverlaps;
|
|
2265
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).descriptionOverlaps;
|
|
2227
2266
|
}
|
|
2228
2267
|
catch {
|
|
2229
2268
|
return { issues: 0, errors: 0 };
|
|
@@ -2244,13 +2283,17 @@ function checkDescriptionOverlap(config, silent) {
|
|
|
2244
2283
|
* `mcpHookIssues` (high-precision: declared-set gated, built-ins allowlisted).
|
|
2245
2284
|
* Warning by default; "error" gates CI.
|
|
2246
2285
|
*/
|
|
2247
|
-
function checkMcpHookTargets(config, silent) {
|
|
2286
|
+
function checkMcpHookTargets(config, silent, adapter) {
|
|
2248
2287
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-hook-target-resolves"]);
|
|
2249
2288
|
if (!sev)
|
|
2250
2289
|
return { issues: 0, errors: 0 };
|
|
2290
|
+
if (!adapter.capabilities.shellHooks) {
|
|
2291
|
+
reportNotApplicable("MCP hook-target check", "shell hooks", adapter, silent);
|
|
2292
|
+
return { issues: 0, errors: 0 };
|
|
2293
|
+
}
|
|
2251
2294
|
let found;
|
|
2252
2295
|
try {
|
|
2253
|
-
found = (0, scan_js_1.scanPlugin)(process.cwd()).mcpHookIssues;
|
|
2296
|
+
found = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).mcpHookIssues;
|
|
2254
2297
|
}
|
|
2255
2298
|
catch {
|
|
2256
2299
|
return { issues: 0, errors: 0 };
|
|
@@ -2272,13 +2315,17 @@ function checkMcpHookTargets(config, silent) {
|
|
|
2272
2315
|
* existence-guarded one-liners, inline commands). Matches Anthropic's own
|
|
2273
2316
|
* `claude plugin validate`. Warning by default; "error" gates CI.
|
|
2274
2317
|
*/
|
|
2275
|
-
function checkHookScriptExists(config, silent) {
|
|
2318
|
+
function checkHookScriptExists(config, silent, adapter) {
|
|
2276
2319
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-script-exists"]);
|
|
2277
2320
|
if (!sev)
|
|
2278
2321
|
return { issues: 0, errors: 0 };
|
|
2322
|
+
if (!adapter.capabilities.shellHooks) {
|
|
2323
|
+
reportNotApplicable("Hook-script existence check", "shell hooks", adapter, silent);
|
|
2324
|
+
return { issues: 0, errors: 0 };
|
|
2325
|
+
}
|
|
2279
2326
|
let missing;
|
|
2280
2327
|
try {
|
|
2281
|
-
missing = (0, scan_js_1.scanPlugin)(process.cwd()).hooks.filter((h) => h.status === "missing");
|
|
2328
|
+
missing = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).hooks.filter((h) => h.status === "missing");
|
|
2282
2329
|
}
|
|
2283
2330
|
catch {
|
|
2284
2331
|
return { issues: 0, errors: 0 };
|
|
@@ -2303,13 +2350,17 @@ function checkHookScriptExists(config, silent) {
|
|
|
2303
2350
|
* — high-precision (gated on a declared set, built-ins allowlisted, the
|
|
2304
2351
|
* plugin-namespaced form skipped). Warning by default; "error" gates CI.
|
|
2305
2352
|
*/
|
|
2306
|
-
function checkMcpToolResolves(config, silent) {
|
|
2353
|
+
function checkMcpToolResolves(config, silent, adapter) {
|
|
2307
2354
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-tool-resolves"]);
|
|
2308
2355
|
if (!sev)
|
|
2309
2356
|
return { issues: 0, errors: 0 };
|
|
2357
|
+
if (!adapter.capabilities.subagents) {
|
|
2358
|
+
reportNotApplicable("MCP tool-resolution check", "subagents", adapter, silent);
|
|
2359
|
+
return { issues: 0, errors: 0 };
|
|
2360
|
+
}
|
|
2310
2361
|
let found;
|
|
2311
2362
|
try {
|
|
2312
|
-
found = (0, scan_js_1.scanPlugin)(process.cwd()).agents.flatMap((a) => a.mcpToolIssues.map((i) => ({ message: i.message, path: a.path })));
|
|
2363
|
+
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 })));
|
|
2313
2364
|
}
|
|
2314
2365
|
catch {
|
|
2315
2366
|
return { issues: 0, errors: 0 };
|
|
@@ -2524,6 +2575,78 @@ function handleGenerateSchema(args, restArgs) {
|
|
|
2524
2575
|
console.log(" Add to your markdown frontmatter:\n" +
|
|
2525
2576
|
` # yaml-language-server: $schema=./${outPath}`);
|
|
2526
2577
|
}
|
|
2578
|
+
/**
|
|
2579
|
+
* `vigiles generate-harness [dir] [out]` — emit one typed registry over every
|
|
2580
|
+
* `*.spec.ts` under `dir`, so a single `tsc --noEmit` cross-checks the whole
|
|
2581
|
+
* harness (dangling delegates → a tsc error; duplicate names → this command
|
|
2582
|
+
* exits non-zero; the capability lattice → a computed export). The third
|
|
2583
|
+
* generated artifact beside `generate-types` / `generate-schema`. See
|
|
2584
|
+
* docs/cli.md and research/whole-harness-codegen.md.
|
|
2585
|
+
*/
|
|
2586
|
+
async function handleGenerateHarness(args, restArgs) {
|
|
2587
|
+
const checkOnly = args.includes("--check");
|
|
2588
|
+
const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
|
|
2589
|
+
const outPath = restArgs[1] ?? (0, node_path_1.resolve)(dir, generate_harness_js_1.HARNESS_GEN_FILENAME);
|
|
2590
|
+
const fullOut = (0, node_path_1.resolve)(process.cwd(), outPath);
|
|
2591
|
+
const specImport = args
|
|
2592
|
+
.filter((a) => a.startsWith("--spec-import="))
|
|
2593
|
+
.map((a) => a.split("=")[1])
|
|
2594
|
+
.filter(Boolean)[0] ?? undefined;
|
|
2595
|
+
// Resolve the harness ONCE (honour --harness / config / auto-detect) so the
|
|
2596
|
+
// capability lattice is computed against the right dialect — never defaulting
|
|
2597
|
+
// to Claude Code in core. The dialect is INJECTED into the generator.
|
|
2598
|
+
const harnessFlag = harnessFlagFrom(args);
|
|
2599
|
+
const adapter = harnessFlag
|
|
2600
|
+
? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
|
|
2601
|
+
: (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
|
|
2602
|
+
console.log(`Scanning ${(0, generate_harness_js_1.labelFor)(process.cwd(), dir)} for *.spec.ts...\n`);
|
|
2603
|
+
const model = await (0, generate_harness_js_1.loadHarnessModel)(dir, (abs) => loadSpec(abs));
|
|
2604
|
+
const result = (0, generate_harness_js_1.generateHarness)(model, {
|
|
2605
|
+
dialect: adapter.dialect,
|
|
2606
|
+
outDir: (0, node_path_1.dirname)(fullOut),
|
|
2607
|
+
specImport,
|
|
2608
|
+
});
|
|
2609
|
+
console.log(` ${String(result.agentCount)} agent(s), ${String(result.edgeCount)} delegate edge(s)` +
|
|
2610
|
+
(result.handoffCount > 0
|
|
2611
|
+
? `, ${String(result.handoffCount)} handoff check(s)`
|
|
2612
|
+
: ""));
|
|
2613
|
+
console.log(` capabilities: ${result.capabilities.purity} (` +
|
|
2614
|
+
`${String(result.capabilities.sideEffecting.length)} side-effecting, ` +
|
|
2615
|
+
`${String(result.capabilities.unknown.length)} unknown)`);
|
|
2616
|
+
// DUPLICATE NAME — the O(N) JS check (never a type). Exit non-zero, no write.
|
|
2617
|
+
if (result.duplicate) {
|
|
2618
|
+
console.log(`\n✗ ${result.duplicate.message}`);
|
|
2619
|
+
console.log(`::error::${result.duplicate.message}`);
|
|
2620
|
+
process.exit(2);
|
|
2621
|
+
}
|
|
2622
|
+
if (checkOnly) {
|
|
2623
|
+
if (!(0, node_fs_1.existsSync)(fullOut)) {
|
|
2624
|
+
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate-harness\` to create it.`);
|
|
2625
|
+
process.exit(1);
|
|
2626
|
+
}
|
|
2627
|
+
const existing = (0, node_fs_1.readFileSync)(fullOut, "utf-8");
|
|
2628
|
+
const normalize = (s) => s
|
|
2629
|
+
.split("\n")
|
|
2630
|
+
.map((l) => l.trimEnd())
|
|
2631
|
+
.join("\n")
|
|
2632
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
2633
|
+
.trim();
|
|
2634
|
+
if (normalize(existing) === normalize(result.gen)) {
|
|
2635
|
+
console.log(`\n✓ ${outPath} is up to date`);
|
|
2636
|
+
}
|
|
2637
|
+
else {
|
|
2638
|
+
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate-harness\` to update.`);
|
|
2639
|
+
process.exit(1);
|
|
2640
|
+
}
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
const outDir = (0, node_path_1.dirname)(fullOut);
|
|
2644
|
+
if (!(0, node_fs_1.existsSync)(outDir))
|
|
2645
|
+
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
2646
|
+
(0, node_fs_1.writeFileSync)(fullOut, result.gen);
|
|
2647
|
+
console.log(`\n✓ Generated ${(0, generate_harness_js_1.labelFor)(process.cwd(), fullOut)}`);
|
|
2648
|
+
console.log(" `tsc --noEmit` over this file now checks every delegate target resolves.");
|
|
2649
|
+
}
|
|
2527
2650
|
/**
|
|
2528
2651
|
* `vigiles test` / `vigiles eval` — discover and run the two-tier harness
|
|
2529
2652
|
* scripts (deterministic `*.harness.mjs` / real-model `*.eval.mjs`) as child
|
|
@@ -2583,6 +2706,167 @@ function handleRunScripts(kind, args, restArgs) {
|
|
|
2583
2706
|
process.exit(1);
|
|
2584
2707
|
}
|
|
2585
2708
|
}
|
|
2709
|
+
/** Parse the `--harness=<name>` override out of an argv list (the one definition). */
|
|
2710
|
+
function harnessFlagFrom(argv) {
|
|
2711
|
+
return argv
|
|
2712
|
+
.find((a) => a.startsWith("--harness="))
|
|
2713
|
+
?.slice("--harness=".length);
|
|
2714
|
+
}
|
|
2715
|
+
/**
|
|
2716
|
+
* `vigiles explain <dir> [name]` — the deterministic WHY behind a low score (C4):
|
|
2717
|
+
* scan a plugin and surface the structural CAUSE of a behavioral symptom + the
|
|
2718
|
+
* one-line fix. No model — it reads the same `ScanReport` `scan` computes. An
|
|
2719
|
+
* optional surface name narrows to one underperforming skill/agent (the
|
|
2720
|
+
* optimizer's call). `--json` for the agent-consumable shape, `--harness=` to
|
|
2721
|
+
* override detection.
|
|
2722
|
+
*/
|
|
2723
|
+
function handleExplain(restArgs, args) {
|
|
2724
|
+
const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
|
|
2725
|
+
const surface = restArgs[1];
|
|
2726
|
+
const json = args.includes("--json");
|
|
2727
|
+
const harnessFlag = harnessFlagFrom(args);
|
|
2728
|
+
const adapter = harnessFlag
|
|
2729
|
+
? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
|
|
2730
|
+
: (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
|
|
2731
|
+
const report = (0, scan_js_1.scanPlugin)(dir, adapter.layout, adapter.dialect);
|
|
2732
|
+
const exps = surface ? (0, score_explainer_js_1.explainSurface)(report, surface) : (0, score_explainer_js_1.explainScore)(report);
|
|
2733
|
+
if (json) {
|
|
2734
|
+
console.log(JSON.stringify(exps, null, 2));
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
if (surface)
|
|
2738
|
+
console.log(`Explaining "${surface}":\n`);
|
|
2739
|
+
console.log((0, score_explainer_js_1.formatExplanations)(exps));
|
|
2740
|
+
}
|
|
2741
|
+
/**
|
|
2742
|
+
* The plugin's declared name for the namespaced skill id, read from the layout's
|
|
2743
|
+
* manifest (adapter-aware path, not a hardcoded `.claude-plugin/`), falling back to
|
|
2744
|
+
* the dir basename. JSON manifests only for now (a TOML/Codex manifest → basename).
|
|
2745
|
+
*/
|
|
2746
|
+
function pluginNameFor(dir, manifestPath) {
|
|
2747
|
+
try {
|
|
2748
|
+
const manifest = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(dir, manifestPath), "utf-8"));
|
|
2749
|
+
if (typeof manifest.name === "string" && manifest.name)
|
|
2750
|
+
return manifest.name;
|
|
2751
|
+
}
|
|
2752
|
+
catch {
|
|
2753
|
+
/* missing / non-JSON manifest → fall back */
|
|
2754
|
+
}
|
|
2755
|
+
return (0, node_path_1.basename)(dir);
|
|
2756
|
+
}
|
|
2757
|
+
/** Enrich an untested Surface with the metadata the right template needs. */
|
|
2758
|
+
/** Extract `"name": type` fields from one rendered `vigiles:ok`/`err` shape block. */
|
|
2759
|
+
function parseContractFields(block) {
|
|
2760
|
+
const fields = [];
|
|
2761
|
+
const re = /"([^"]+)"\s*:\s*(string\[\]|string|number|boolean)/g;
|
|
2762
|
+
let m;
|
|
2763
|
+
while ((m = re.exec(block)) !== null) {
|
|
2764
|
+
fields.push({ name: m[1], type: m[2] });
|
|
2765
|
+
}
|
|
2766
|
+
return fields;
|
|
2767
|
+
}
|
|
2768
|
+
/**
|
|
2769
|
+
* Parse a subagent's compiled `## Output contract` (the `vigiles:ok` / `vigiles:err`
|
|
2770
|
+
* blocks the compiler emits) back into a typed `ResultContract`, so the generator
|
|
2771
|
+
* can write an `assertAgentOk` test against the real fields. Returns null when the
|
|
2772
|
+
* agent has no result() contract.
|
|
2773
|
+
*/
|
|
2774
|
+
function parseResultContract(md) {
|
|
2775
|
+
const ok = /```vigiles:ok\n([\s\S]*?)```/.exec(md);
|
|
2776
|
+
const err = /```vigiles:err\n([\s\S]*?)```/.exec(md);
|
|
2777
|
+
if (!ok && !err)
|
|
2778
|
+
return null;
|
|
2779
|
+
const okFields = ok ? parseContractFields(ok[1]) : [];
|
|
2780
|
+
const errFields = err ? parseContractFields(err[1]) : [];
|
|
2781
|
+
if (okFields.length === 0 && errFields.length === 0)
|
|
2782
|
+
return null;
|
|
2783
|
+
return { ok: okFields, err: errFields };
|
|
2784
|
+
}
|
|
2785
|
+
function scaffoldInputFor(s, report, pluginName, dir, dialect) {
|
|
2786
|
+
const base = { kind: s.kind, name: s.name, path: s.path };
|
|
2787
|
+
switch (s.kind) {
|
|
2788
|
+
case "skill": {
|
|
2789
|
+
const sk = report.skills.find((x) => x.name === s.name);
|
|
2790
|
+
return { ...base, pluginName, userInvoked: sk?.userInvoked };
|
|
2791
|
+
}
|
|
2792
|
+
case "agent": {
|
|
2793
|
+
const ag = report.agents.find((x) => x.name === s.name);
|
|
2794
|
+
const tools = ag?.tools ?? null;
|
|
2795
|
+
const sideEffectingTools = tools
|
|
2796
|
+
? (0, effects_js_1.effectSurface)(tools, dialect).sideEffecting
|
|
2797
|
+
: undefined;
|
|
2798
|
+
let resultContract = null;
|
|
2799
|
+
try {
|
|
2800
|
+
resultContract = parseResultContract((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(dir, s.path), "utf-8"));
|
|
2801
|
+
}
|
|
2802
|
+
catch {
|
|
2803
|
+
// agent .md unreadable → no contract to generate against
|
|
2804
|
+
}
|
|
2805
|
+
return { ...base, tools, sideEffectingTools, resultContract };
|
|
2806
|
+
}
|
|
2807
|
+
case "hook":
|
|
2808
|
+
return { ...base, hookCommand: `bash ${s.path}` };
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
/**
|
|
2812
|
+
* `vigiles scaffold-test [dir]` — generate a runnable STARTER test for each
|
|
2813
|
+
* untested skill/agent/hook (B1, test-gen from free-form). Reuses the
|
|
2814
|
+
* untested-surface detector for the list + `scan` for the metadata, then emits the
|
|
2815
|
+
* cheapest meaningful tier per kind (hook → `runHook`, skill → `measureTriggerRate`,
|
|
2816
|
+
* subagent → `runHarnessTest`) at the surface's suggested test path. Dry-run by
|
|
2817
|
+
* default (prints the scaffolds); `--write` creates the files (never clobbering an
|
|
2818
|
+
* existing one); `--json` for the agent-consumable `{ path, content }[]`.
|
|
2819
|
+
*/
|
|
2820
|
+
function handleScaffoldTest(restArgs, args) {
|
|
2821
|
+
const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
|
|
2822
|
+
const write = args.includes("--write");
|
|
2823
|
+
const json = args.includes("--json");
|
|
2824
|
+
const harnessFlag = harnessFlagFrom(args);
|
|
2825
|
+
const adapter = harnessFlag
|
|
2826
|
+
? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
|
|
2827
|
+
: (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
|
|
2828
|
+
const { untested } = (0, test_coverage_js_1.findUntestedSurfaces)({
|
|
2829
|
+
basePath: dir,
|
|
2830
|
+
layout: adapter.layout,
|
|
2831
|
+
});
|
|
2832
|
+
const report = (0, scan_js_1.scanPlugin)(dir, adapter.layout, adapter.dialect);
|
|
2833
|
+
const pluginName = pluginNameFor(dir, adapter.layout.manifestPath);
|
|
2834
|
+
const scaffolds = untested.map((s) => (0, scaffold_test_js_1.scaffoldTest)(scaffoldInputFor(s, report, pluginName, dir, adapter.dialect)));
|
|
2835
|
+
if (json) {
|
|
2836
|
+
console.log(JSON.stringify(scaffolds, null, 2));
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
if (!write) {
|
|
2840
|
+
console.log((0, scaffold_test_js_1.formatScaffolds)(scaffolds));
|
|
2841
|
+
for (const s of scaffolds) {
|
|
2842
|
+
console.log(`\n# ${s.path}\n`);
|
|
2843
|
+
console.log(s.content);
|
|
2844
|
+
}
|
|
2845
|
+
if (scaffolds.length > 0) {
|
|
2846
|
+
console.log("Re-run with --write to create these files.");
|
|
2847
|
+
}
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
const written = [];
|
|
2851
|
+
const skipped = [];
|
|
2852
|
+
for (const s of scaffolds) {
|
|
2853
|
+
const target = (0, node_path_1.resolve)(dir, s.path);
|
|
2854
|
+
if ((0, node_fs_1.existsSync)(target)) {
|
|
2855
|
+
skipped.push(s.path);
|
|
2856
|
+
continue;
|
|
2857
|
+
}
|
|
2858
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(target), { recursive: true });
|
|
2859
|
+
(0, node_fs_1.writeFileSync)(target, s.content);
|
|
2860
|
+
written.push(s.path);
|
|
2861
|
+
}
|
|
2862
|
+
for (const p of written)
|
|
2863
|
+
console.log(`✓ wrote ${p}`);
|
|
2864
|
+
for (const p of skipped)
|
|
2865
|
+
console.log(`⊘ skipped ${p} (already exists)`);
|
|
2866
|
+
if (written.length === 0 && skipped.length === 0) {
|
|
2867
|
+
console.log("Nothing to scaffold — every surface already has a test.");
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2586
2870
|
function printUsage(command) {
|
|
2587
2871
|
console.log("vigiles — compile typed specs to instruction files");
|
|
2588
2872
|
console.log("");
|
|
@@ -2592,6 +2876,8 @@ function printUsage(command) {
|
|
|
2592
2876
|
console.log(" vigiles lint [files...] Verify references, find gaps in instruction files");
|
|
2593
2877
|
console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
|
|
2594
2878
|
console.log(" vigiles eval [files...] Run *.eval.mjs real-model harness evals (--trials=N, --min=N, --no-skip)");
|
|
2879
|
+
console.log(" vigiles explain <dir> [name] Deterministic WHY a skill/agent underperforms + the fix (--json, --harness=)");
|
|
2880
|
+
console.log(" vigiles scaffold-test [dir] Generate a starter test for each untested skill/agent/hook (--write, --json)");
|
|
2595
2881
|
console.log("");
|
|
2596
2882
|
console.log("Examples:");
|
|
2597
2883
|
console.log(" vigiles init Auto-detect project, create specs, wire CI");
|
|
@@ -2603,6 +2889,8 @@ function printUsage(command) {
|
|
|
2603
2889
|
console.log(" vigiles generate-types --check Verify .d.ts is up to date");
|
|
2604
2890
|
console.log(" vigiles generate-schema [out] Emit JSON Schema for vigiles: frontmatter");
|
|
2605
2891
|
console.log(" vigiles generate-schema --check Verify schema.json is up to date");
|
|
2892
|
+
console.log(" vigiles generate-harness [dir] Emit harness.gen.ts — one typed registry");
|
|
2893
|
+
console.log(" vigiles generate-harness --check Verify harness.gen.ts is up to date");
|
|
2606
2894
|
console.log(" vigiles --version Print the version number");
|
|
2607
2895
|
if (command && command !== "--help") {
|
|
2608
2896
|
console.log(`\nUnknown command: "${command}"`);
|
|
@@ -2701,6 +2989,42 @@ function skillStartCommand(target) {
|
|
|
2701
2989
|
(0, skill_runtime_js_1.setActiveSkill)(process.cwd(), target);
|
|
2702
2990
|
console.log(`Active skill: ${target}`);
|
|
2703
2991
|
}
|
|
2992
|
+
/**
|
|
2993
|
+
* PreToolUse-hook entrypoint: enforce the active skill's declared purity floor.
|
|
2994
|
+
* Reads the tool event on stdin, parses the `vigiles:purity:` marker from the
|
|
2995
|
+
* active skill's compiled SKILL.md, and blocks (exit 2 + reason on stderr) any
|
|
2996
|
+
* tool call that violates the declared floor — refining `Bash` by the live
|
|
2997
|
+
* command via `isReadOnlyBash`. Skills have no tools-allowlist rail; this gate
|
|
2998
|
+
* is purity-only. Mirrors `agentHookCommand` for skills.
|
|
2999
|
+
*/
|
|
3000
|
+
function skillToolHookCommand() {
|
|
3001
|
+
let raw = "";
|
|
3002
|
+
try {
|
|
3003
|
+
raw = (0, node_fs_1.readFileSync)(0, "utf-8");
|
|
3004
|
+
}
|
|
3005
|
+
catch {
|
|
3006
|
+
/* no stdin */
|
|
3007
|
+
}
|
|
3008
|
+
let tool = "";
|
|
3009
|
+
let command;
|
|
3010
|
+
try {
|
|
3011
|
+
const parsed = JSON.parse(raw);
|
|
3012
|
+
tool = parsed.tool_name ?? "";
|
|
3013
|
+
if (typeof parsed.tool_input?.command === "string") {
|
|
3014
|
+
command = parsed.tool_input.command;
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
catch {
|
|
3018
|
+
/* malformed input → no tool, allow */
|
|
3019
|
+
}
|
|
3020
|
+
if (!tool)
|
|
3021
|
+
return;
|
|
3022
|
+
const decision = (0, skill_runtime_js_1.evaluateSkillPreToolUse)(process.cwd(), tool, command);
|
|
3023
|
+
if (!decision.allow) {
|
|
3024
|
+
console.error(decision.message);
|
|
3025
|
+
process.exit(2);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
2704
3028
|
/**
|
|
2705
3029
|
* PreToolUse-hook entrypoint: enforce the active subagent's allowed-tools
|
|
2706
3030
|
* contract. Reads the tool event on stdin, parses the active agent's compiled
|
|
@@ -2716,15 +3040,49 @@ function agentHookCommand() {
|
|
|
2716
3040
|
/* no stdin */
|
|
2717
3041
|
}
|
|
2718
3042
|
let tool = "";
|
|
3043
|
+
let command;
|
|
3044
|
+
let event = "";
|
|
3045
|
+
let toolInput;
|
|
2719
3046
|
try {
|
|
2720
|
-
|
|
3047
|
+
const parsed = JSON.parse(raw);
|
|
3048
|
+
event = parsed.hook_event_name ?? "";
|
|
3049
|
+
tool = parsed.tool_name ?? "";
|
|
3050
|
+
toolInput = parsed.tool_input;
|
|
3051
|
+
if (typeof parsed.tool_input?.command === "string") {
|
|
3052
|
+
command = parsed.tool_input.command;
|
|
3053
|
+
}
|
|
2721
3054
|
}
|
|
2722
3055
|
catch {
|
|
2723
3056
|
/* malformed input → no tool, allow */
|
|
2724
3057
|
}
|
|
3058
|
+
const cwd = process.cwd();
|
|
3059
|
+
// EXPERIMENTAL (parked P3, flat-only — do NOT auto-wire): the Task/SubagentStop
|
|
3060
|
+
// bracketing below assumes one active subagent at a time and is NOT nesting-safe
|
|
3061
|
+
// (CC v2.1.172 depth-5 nesting needs a stack + spawn-tool-name check). See
|
|
3062
|
+
// research/effect-boundary-design.md.
|
|
3063
|
+
// SubagentStop → CLOSE the window deterministically (no model `agent-done`):
|
|
3064
|
+
// the subagent returned, so its contract/purity no longer apply.
|
|
3065
|
+
if (event === "SubagentStop") {
|
|
3066
|
+
(0, agent_runtime_js_1.clearActiveAgent)(cwd);
|
|
3067
|
+
(0, effect_region_js_1.clearEffectActive)(cwd);
|
|
3068
|
+
return;
|
|
3069
|
+
}
|
|
3070
|
+
// PreToolUse(Task) → OPEN the window deterministically (no model `agent-start`
|
|
3071
|
+
// / `effect-enter`): the parent is dispatching a subagent, so activate that
|
|
3072
|
+
// subagent's compiled contract for the tool calls it is about to make. The
|
|
3073
|
+
// Task dispatch itself is the PARENT's action — don't gate it against the
|
|
3074
|
+
// subagent's contract; just open the window and allow.
|
|
3075
|
+
if (tool === "Task") {
|
|
3076
|
+
const agentPath = (0, agent_runtime_js_1.decideTaskDispatch)(toolInput, cwd, process.env.CLAUDE_PLUGIN_ROOT);
|
|
3077
|
+
if (agentPath) {
|
|
3078
|
+
(0, agent_runtime_js_1.setActiveAgent)(cwd, agentPath);
|
|
3079
|
+
(0, effect_region_js_1.setEffectActive)(cwd);
|
|
3080
|
+
}
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
2725
3083
|
if (!tool)
|
|
2726
3084
|
return;
|
|
2727
|
-
const decision = (0,
|
|
3085
|
+
const decision = (0, agent_runtime_js_1.evaluatePreToolUse)(cwd, tool, command);
|
|
2728
3086
|
if (!decision.allow) {
|
|
2729
3087
|
console.error(decision.message);
|
|
2730
3088
|
process.exit(2);
|
|
@@ -2760,7 +3118,7 @@ function agentStartCommand(target) {
|
|
|
2760
3118
|
console.error("Usage: vigiles agent-start <agents/<name>.md>");
|
|
2761
3119
|
process.exit(2);
|
|
2762
3120
|
}
|
|
2763
|
-
(0,
|
|
3121
|
+
(0, agent_runtime_js_1.setActiveAgent)(process.cwd(), target);
|
|
2764
3122
|
console.log(`Active agent: ${target}`);
|
|
2765
3123
|
}
|
|
2766
3124
|
/** Dispatch the skill-runtime subcommands. Returns false if unrecognized. */
|
|
@@ -2778,11 +3136,14 @@ function handleSkillCommand(command, restArgs) {
|
|
|
2778
3136
|
case "skill-hook":
|
|
2779
3137
|
skillHookCommand();
|
|
2780
3138
|
return true;
|
|
3139
|
+
case "skill-tool-hook":
|
|
3140
|
+
skillToolHookCommand();
|
|
3141
|
+
return true;
|
|
2781
3142
|
case "agent-start":
|
|
2782
3143
|
agentStartCommand(restArgs[0]);
|
|
2783
3144
|
return true;
|
|
2784
3145
|
case "agent-done":
|
|
2785
|
-
(0,
|
|
3146
|
+
(0, agent_runtime_js_1.clearActiveAgent)(process.cwd());
|
|
2786
3147
|
return true;
|
|
2787
3148
|
case "agent-hook":
|
|
2788
3149
|
agentHookCommand();
|
|
@@ -2799,6 +3160,13 @@ function handleSkillCommand(command, restArgs) {
|
|
|
2799
3160
|
case "refs-hook":
|
|
2800
3161
|
refsHookCommand();
|
|
2801
3162
|
return true;
|
|
3163
|
+
case "effect-enter":
|
|
3164
|
+
(0, effect_region_js_1.setEffectActive)(process.cwd());
|
|
3165
|
+
console.log("Effect boundary entered.");
|
|
3166
|
+
return true;
|
|
3167
|
+
case "effect-exit":
|
|
3168
|
+
(0, effect_region_js_1.clearEffectActive)(process.cwd());
|
|
3169
|
+
return true;
|
|
2802
3170
|
default:
|
|
2803
3171
|
return false;
|
|
2804
3172
|
}
|
|
@@ -2969,9 +3337,7 @@ async function main() {
|
|
|
2969
3337
|
console.log("Run `vigiles init` to create one.");
|
|
2970
3338
|
process.exit(0);
|
|
2971
3339
|
}
|
|
2972
|
-
const harnessFlag = args
|
|
2973
|
-
.find((a) => a.startsWith("--harness="))
|
|
2974
|
-
?.slice("--harness=".length);
|
|
3340
|
+
const harnessFlag = harnessFlagFrom(args);
|
|
2975
3341
|
const valid = await compile(specs, config, { harnessFlag });
|
|
2976
3342
|
console.log("");
|
|
2977
3343
|
if (valid) {
|
|
@@ -3033,9 +3399,7 @@ async function main() {
|
|
|
3033
3399
|
}
|
|
3034
3400
|
else {
|
|
3035
3401
|
const root = (0, node_path_1.resolve)(targets[0]);
|
|
3036
|
-
const harnessFlag = args
|
|
3037
|
-
.find((a) => a.startsWith("--harness="))
|
|
3038
|
-
?.slice("--harness=".length);
|
|
3402
|
+
const harnessFlag = harnessFlagFrom(args);
|
|
3039
3403
|
const det = (0, adapter_registry_js_1.detectAdapterResult)(root);
|
|
3040
3404
|
const adapter = harnessFlag
|
|
3041
3405
|
? (0, adapter_registry_js_1.resolveAdapter)(root, harnessFlag)
|
|
@@ -3048,7 +3412,15 @@ async function main() {
|
|
|
3048
3412
|
}
|
|
3049
3413
|
console.log("");
|
|
3050
3414
|
}
|
|
3051
|
-
|
|
3415
|
+
if (args.includes("--fix-plan")) {
|
|
3416
|
+
// The deterministic optimization lens on the SAME report: health score
|
|
3417
|
+
// + the ranked free fixes to clear before measuring (the A2 spine).
|
|
3418
|
+
const plan = (0, optimize_js_1.optimize)(report);
|
|
3419
|
+
console.log(json ? JSON.stringify(plan, null, 2) : (0, optimize_js_1.formatOptimize)(plan));
|
|
3420
|
+
}
|
|
3421
|
+
else {
|
|
3422
|
+
console.log(json ? JSON.stringify(report, null, 2) : (0, scan_js_1.formatScanReport)(report));
|
|
3423
|
+
}
|
|
3052
3424
|
if (wantTrigger) {
|
|
3053
3425
|
const harness = adapter.name === "codex" ? "codex" : "claude-code";
|
|
3054
3426
|
await handleScanTrigger(root, args, json, harness);
|
|
@@ -3056,6 +3428,12 @@ async function main() {
|
|
|
3056
3428
|
}
|
|
3057
3429
|
break;
|
|
3058
3430
|
}
|
|
3431
|
+
case "explain":
|
|
3432
|
+
handleExplain(restArgs, args);
|
|
3433
|
+
break;
|
|
3434
|
+
case "scaffold-test":
|
|
3435
|
+
handleScaffoldTest(restArgs, args);
|
|
3436
|
+
break;
|
|
3059
3437
|
// --- Plumbing ---
|
|
3060
3438
|
case "generate-types":
|
|
3061
3439
|
handleGenerateTypes(args, restArgs);
|
|
@@ -3063,6 +3441,9 @@ async function main() {
|
|
|
3063
3441
|
case "generate-schema":
|
|
3064
3442
|
handleGenerateSchema(args, restArgs);
|
|
3065
3443
|
break;
|
|
3444
|
+
case "generate-harness":
|
|
3445
|
+
await handleGenerateHarness(args, restArgs);
|
|
3446
|
+
break;
|
|
3066
3447
|
default:
|
|
3067
3448
|
if (!handleSkillCommand(command, restArgs))
|
|
3068
3449
|
printUsage(command);
|