vigiles 8.0.0 → 9.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 +63 -35
- package/dist/adoptability.d.ts +55 -0
- package/dist/adoptability.js +196 -0
- package/dist/audit-html.d.ts +20 -0
- package/dist/audit-html.js +61 -0
- package/dist/audit-prompts.d.ts +46 -0
- package/dist/audit-prompts.js +90 -0
- package/dist/audit-report.d.ts +70 -0
- package/dist/audit-report.js +51 -0
- package/dist/audit-report.template.html +110 -0
- package/dist/audit-score.d.ts +44 -0
- package/dist/audit-score.js +221 -0
- package/dist/cli-commands.d.ts +1 -1
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +340 -111
- package/dist/core/inline.js +11 -1
- package/dist/core/types.d.ts +12 -0
- package/dist/dialect-drift.js +1 -1
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +1 -1
- package/dist/optimize.d.ts +12 -5
- package/dist/optimize.js +27 -5
- package/dist/scan-behavioral.d.ts +8 -2
- package/dist/scan-behavioral.js +6 -4
- package/dist/scan-trigger-suggest.d.ts +68 -31
- package/dist/scan-trigger-suggest.js +66 -33
- package/dist/scan.d.ts +36 -11
- package/dist/scan.js +60 -14
- package/dist/score-explainer.d.ts +1 -1
- package/package.json +4 -2
- package/skills/test-harness/SKILL.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -26,12 +26,16 @@ const effects_js_1 = require("./core/effects.js");
|
|
|
26
26
|
const scan_js_1 = require("./scan.js");
|
|
27
27
|
const scan_trigger_suggest_js_1 = require("./scan-trigger-suggest.js");
|
|
28
28
|
const dialect_drift_js_1 = require("./dialect-drift.js");
|
|
29
|
-
const score_explainer_js_1 = require("./score-explainer.js");
|
|
30
29
|
const scan_behavioral_js_1 = require("./scan-behavioral.js");
|
|
31
30
|
const adapter_registry_js_1 = require("./adapter-registry.js");
|
|
32
31
|
const skill_harness_js_1 = require("./skill-harness.js");
|
|
33
32
|
const leaderboard_js_1 = require("./leaderboard.js");
|
|
34
33
|
const optimize_js_1 = require("./optimize.js");
|
|
34
|
+
const audit_score_js_1 = require("./audit-score.js");
|
|
35
|
+
const audit_prompts_js_1 = require("./audit-prompts.js");
|
|
36
|
+
const audit_html_js_1 = require("./audit-html.js");
|
|
37
|
+
const audit_report_js_1 = require("./audit-report.js");
|
|
38
|
+
const adoptability_js_1 = require("./adoptability.js");
|
|
35
39
|
const compile_js_1 = require("./core/compile.js");
|
|
36
40
|
const proofs_js_1 = require("./core/proofs.js");
|
|
37
41
|
const inline_js_1 = require("./core/inline.js");
|
|
@@ -1206,7 +1210,16 @@ function targetHasHash(absPath) {
|
|
|
1206
1210
|
return false;
|
|
1207
1211
|
}
|
|
1208
1212
|
}
|
|
1209
|
-
|
|
1213
|
+
/**
|
|
1214
|
+
* Single-target spec scaffolder — the small building block behind the `init`
|
|
1215
|
+
* verb, NOT the wizard. Creates exactly one sibling `<target>.spec.ts`: it
|
|
1216
|
+
* faithfully ADOPTS an existing hand-written instruction file (non-destructive —
|
|
1217
|
+
* never overwrites the markdown) or writes a blank starter for a greenfield
|
|
1218
|
+
* target. Called directly for `vigiles init --target=<file>`, and once per
|
|
1219
|
+
* target by `setupPillar1`. The full onboarding (both layers, deps, CI, plugin)
|
|
1220
|
+
* is `setup()`.
|
|
1221
|
+
*/
|
|
1222
|
+
function scaffoldSpec(args) {
|
|
1210
1223
|
const targetFlag = args.find((a) => a.startsWith("--target="));
|
|
1211
1224
|
const target = targetFlag ? targetFlag.split("=")[1] : "CLAUDE.md";
|
|
1212
1225
|
const specPath = `${target}.spec.ts`;
|
|
@@ -1454,7 +1467,7 @@ function workflowUsesStaleApi(content) {
|
|
|
1454
1467
|
return false; // uses the Action — fine
|
|
1455
1468
|
if (!/\bvigiles\b/.test(content))
|
|
1456
1469
|
return false; // not a vigiles workflow
|
|
1457
|
-
const hasModernCmd = /vigiles\s+(lint|test|eval|compile|
|
|
1470
|
+
const hasModernCmd = /vigiles\s+(lint|test|eval|compile|audit|generate-types|generate-schema|init)\b/.test(content);
|
|
1458
1471
|
return !hasModernCmd;
|
|
1459
1472
|
}
|
|
1460
1473
|
/**
|
|
@@ -1465,7 +1478,7 @@ function workflowUsesStaleApi(content) {
|
|
|
1465
1478
|
* the bare-API heuristic above, which an Action reference short-circuits.
|
|
1466
1479
|
*/
|
|
1467
1480
|
const REMOVED_SUBCOMMANDS = {
|
|
1468
|
-
|
|
1481
|
+
scan: "audit", // renamed: the Lighthouse report verb is `audit`
|
|
1469
1482
|
};
|
|
1470
1483
|
/** The first removed/renamed `vigiles <sub>` a workflow still calls, if any. */
|
|
1471
1484
|
function workflowRemovedSubcommand(content) {
|
|
@@ -1475,7 +1488,7 @@ function workflowRemovedSubcommand(content) {
|
|
|
1475
1488
|
}
|
|
1476
1489
|
return null;
|
|
1477
1490
|
}
|
|
1478
|
-
/** Rewrite removed/renamed `vigiles <sub>` invocations in place (
|
|
1491
|
+
/** Rewrite removed/renamed `vigiles <sub>` invocations in place (scan → audit).
|
|
1479
1492
|
* Surgical — preserves the rest of the user's workflow. */
|
|
1480
1493
|
function rewriteRemovedSubcommands(content) {
|
|
1481
1494
|
let out = content;
|
|
@@ -1759,7 +1772,7 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
1759
1772
|
? determineTargets(detected, targetValue, harnesses)
|
|
1760
1773
|
: redirectSyncToolTargets(cwd, collapseMirroredTargets(determineTargets(detected, targetValue, harnesses), (0, compose_js_1.detectInstructionMirror)(cwd)));
|
|
1761
1774
|
// Create specs. An existing hand-written target is faithfully ADOPTED into a
|
|
1762
|
-
// spec (
|
|
1775
|
+
// spec (scaffoldSpec() does the convert), not clobbered with a blank one — so the
|
|
1763
1776
|
// compile below reproduces it (the user reviews the diff). A greenfield target
|
|
1764
1777
|
// gets a blank starter spec.
|
|
1765
1778
|
for (const target of targets) {
|
|
@@ -1772,7 +1785,7 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
1772
1785
|
console.log(`✓ ${specPath} already exists`);
|
|
1773
1786
|
}
|
|
1774
1787
|
else {
|
|
1775
|
-
|
|
1788
|
+
scaffoldSpec(["--target=" + target]); // adopts existing content, else blank scaffold
|
|
1776
1789
|
written.push(specPath);
|
|
1777
1790
|
if (willAdopt)
|
|
1778
1791
|
adopted.push(target);
|
|
@@ -2709,14 +2722,14 @@ function flagValue(args, name) {
|
|
|
2709
2722
|
return args.find((a) => a.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
2710
2723
|
}
|
|
2711
2724
|
/**
|
|
2712
|
-
*
|
|
2713
|
-
*
|
|
2714
|
-
* sets (`--prompts`) and reports BOTH behavioral columns: trigger-rate (does
|
|
2715
|
-
* skill actually FIRE — recall + precision) and the selection-collision matrix
|
|
2716
|
-
* one skill HIJACK a sibling's prompt — the behavioral confirmation of the
|
|
2725
|
+
* The MODEL-GATED behavioral half of `vigiles audit` (the model trigger tier; the
|
|
2726
|
+
* deterministic core of `audit` stays free). Loads the author-supplied per-skill
|
|
2727
|
+
* prompt sets (`--prompts`) and reports BOTH behavioral columns: trigger-rate (does
|
|
2728
|
+
* each skill actually FIRE — recall + precision) and the selection-collision matrix
|
|
2729
|
+
* (does one skill HIJACK a sibling's prompt — the behavioral confirmation of the
|
|
2717
2730
|
* deterministic `description-overlap` rule). Needs the harness CLI + model auth;
|
|
2718
2731
|
* degrades honestly ("unavailable") when absent. The OSS-testing front door:
|
|
2719
|
-
* `vigiles
|
|
2732
|
+
* `vigiles audit ./plugin --prompts=p.json` (interactive — say yes when asked).
|
|
2720
2733
|
*/
|
|
2721
2734
|
async function handleMeasure(restArgs, args) {
|
|
2722
2735
|
const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
|
|
@@ -3051,32 +3064,6 @@ function harnessFlagFrom(argv) {
|
|
|
3051
3064
|
.find((a) => a.startsWith("--harness="))
|
|
3052
3065
|
?.slice("--harness=".length);
|
|
3053
3066
|
}
|
|
3054
|
-
/**
|
|
3055
|
-
* `vigiles scan <dir> --explain [name]` — the deterministic WHY behind a low score (C4):
|
|
3056
|
-
* scan a plugin and surface the structural CAUSE of a behavioral symptom + the
|
|
3057
|
-
* one-line fix. No model — it reads the same `ScanReport` `scan` computes. An
|
|
3058
|
-
* optional surface name narrows to one underperforming skill/agent (the
|
|
3059
|
-
* optimizer's call). `--json` for the agent-consumable shape, `--harness=` to
|
|
3060
|
-
* override detection.
|
|
3061
|
-
*/
|
|
3062
|
-
function handleExplain(restArgs, args) {
|
|
3063
|
-
const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
|
|
3064
|
-
const surface = restArgs[1];
|
|
3065
|
-
const json = args.includes("--json");
|
|
3066
|
-
const harnessFlag = harnessFlagFrom(args);
|
|
3067
|
-
const adapter = harnessFlag
|
|
3068
|
-
? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
|
|
3069
|
-
: (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
|
|
3070
|
-
const report = (0, scan_js_1.scanPlugin)(dir, adapter.layout, adapter.dialect);
|
|
3071
|
-
const exps = surface ? (0, score_explainer_js_1.explainSurface)(report, surface) : (0, score_explainer_js_1.explainScore)(report);
|
|
3072
|
-
if (json) {
|
|
3073
|
-
console.log(JSON.stringify(exps, null, 2));
|
|
3074
|
-
return;
|
|
3075
|
-
}
|
|
3076
|
-
if (surface)
|
|
3077
|
-
console.log(`Explaining "${surface}":\n`);
|
|
3078
|
-
console.log((0, score_explainer_js_1.formatExplanations)(exps));
|
|
3079
|
-
}
|
|
3080
3067
|
/**
|
|
3081
3068
|
* Whole-harness capability lattice from a scanned plugin's agents (no `tools:` line →
|
|
3082
3069
|
* inherits-all). The substrate `scan --capability-diff` diffs. Reused for both the
|
|
@@ -3227,9 +3214,9 @@ function printUsage(command) {
|
|
|
3227
3214
|
console.log(" vigiles compile [files...] Compile .spec.ts → .md");
|
|
3228
3215
|
console.log(" vigiles eject [file] Un-manage a compiled file → plain hand-owned markdown (--keep-spec)");
|
|
3229
3216
|
console.log(" vigiles lint [files...] Verify references, find gaps in instruction files");
|
|
3230
|
-
console.log(" vigiles
|
|
3231
|
-
console.log("
|
|
3232
|
-
console.log("
|
|
3217
|
+
console.log(" vigiles audit [dir...] Lighthouse for your harness — a LOCAL report: rings + what's broken + fixes (a deterministic read; 2+ dirs → leaderboard)");
|
|
3218
|
+
console.log(" writes vigiles-report.html + vigiles-report.json (--no-html/--no-json) · --json for machine output. NOT a CI step — use `vigiles lint` in CI.");
|
|
3219
|
+
console.log(" the executing checks (run your hooks · live MCP · do skills fire?) run only interactively — `audit` asks once (remembered); automation uses the vigiles/testing API");
|
|
3233
3220
|
console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
|
|
3234
3221
|
console.log(" vigiles eval [files...] Run *.eval.mjs real-model harness evals (--trials=N, --min=N, --no-skip)");
|
|
3235
3222
|
console.log(" vigiles scaffold-test [dir] Generate a starter test for each untested skill/agent/hook (--write, --json)");
|
|
@@ -4030,32 +4017,175 @@ async function runHookProgramCommand(file) {
|
|
|
4030
4017
|
}
|
|
4031
4018
|
}
|
|
4032
4019
|
/**
|
|
4033
|
-
*
|
|
4034
|
-
*
|
|
4035
|
-
*
|
|
4036
|
-
|
|
4037
|
-
|
|
4020
|
+
* Write the versioned JSON artifact (`vigiles-report.json`) — the upload/CI
|
|
4021
|
+
* boundary a hosted dashboard ingests. Stamps `meta.generatedAt` here (at write
|
|
4022
|
+
* time, not in the pure builder, so the HTML-embedded form stays deterministic).
|
|
4023
|
+
*/
|
|
4024
|
+
function writeAuditJson(report) {
|
|
4025
|
+
const jsonPath = (0, node_path_1.resolve)(process.cwd(), "vigiles-report.json");
|
|
4026
|
+
const stamped = {
|
|
4027
|
+
...report,
|
|
4028
|
+
meta: { ...report.meta, generatedAt: new Date().toISOString() },
|
|
4029
|
+
};
|
|
4030
|
+
try {
|
|
4031
|
+
(0, node_fs_1.writeFileSync)(jsonPath, JSON.stringify(stamped, null, 2) + "\n");
|
|
4032
|
+
console.log("✓ Wrote vigiles-report.json — the upload/CI artifact");
|
|
4033
|
+
}
|
|
4034
|
+
catch (e) {
|
|
4035
|
+
console.log(`\n⚠ could not write vigiles-report.json: ${e instanceof Error ? e.message : String(e)}`);
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
/** Best-effort open the report in the default browser (TTY-only caller). */
|
|
4039
|
+
function openBestEffort(file) {
|
|
4040
|
+
const cmd = process.platform === "darwin"
|
|
4041
|
+
? "open"
|
|
4042
|
+
: process.platform === "win32"
|
|
4043
|
+
? "start"
|
|
4044
|
+
: "xdg-open";
|
|
4045
|
+
void import("node:child_process")
|
|
4046
|
+
.then(({ spawn }) => {
|
|
4047
|
+
const child = spawn(cmd, [file], {
|
|
4048
|
+
stdio: "ignore",
|
|
4049
|
+
detached: true,
|
|
4050
|
+
shell: process.platform === "win32",
|
|
4051
|
+
});
|
|
4052
|
+
child.on("error", () => undefined);
|
|
4053
|
+
child.unref();
|
|
4054
|
+
})
|
|
4055
|
+
.catch(() => undefined);
|
|
4056
|
+
}
|
|
4057
|
+
/**
|
|
4058
|
+
* Write the self-contained HTML audit report to `vigiles-report.html` (cwd) and,
|
|
4059
|
+
* for a human at a TTY, open it best-effort. The shareable Lighthouse artifact;
|
|
4060
|
+
* never spawns a browser for an agent / CI run.
|
|
4061
|
+
*/
|
|
4062
|
+
function writeAuditHtml(report) {
|
|
4063
|
+
const htmlPath = (0, node_path_1.resolve)(process.cwd(), "vigiles-report.html");
|
|
4064
|
+
try {
|
|
4065
|
+
(0, node_fs_1.writeFileSync)(htmlPath, (0, audit_html_js_1.renderAuditHtml)(report));
|
|
4066
|
+
console.log("\n✓ Wrote vigiles-report.html — open it for the full report");
|
|
4067
|
+
if (process.stdout.isTTY)
|
|
4068
|
+
openBestEffort(htmlPath);
|
|
4069
|
+
}
|
|
4070
|
+
catch (e) {
|
|
4071
|
+
// No template (unbuilt checkout) or a write error — skip the HTML; the JSON
|
|
4072
|
+
// artifact + terminal report don't depend on it.
|
|
4073
|
+
console.log(`\n⚠ skipped vigiles-report.html: ${e instanceof Error ? e.message : String(e)}`);
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
/**
|
|
4077
|
+
* Run the model trigger tier with no `--prompts`: auto-generate diverse probe
|
|
4078
|
+
* prompts from each skill's description and measure trigger-rate (recall +
|
|
4079
|
+
* precision) — zero-setup. `--prompts=<file>` (handled by handleMeasure)
|
|
4080
|
+
* overrides for a curated benchmark + the collision matrix. Model-gated: the
|
|
4081
|
+
* probes are deterministic, but RUNNING them needs the harness CLI + model auth
|
|
4082
|
+
* (degrades to "unavailable" otherwise).
|
|
4083
|
+
*/
|
|
4084
|
+
async function runAutoTrigger(dir, report, adapter, args) {
|
|
4085
|
+
const json = args.includes("--json");
|
|
4086
|
+
const harness = adapter.name === "codex" ? "codex" : "claude-code";
|
|
4087
|
+
const skills = report.skills
|
|
4088
|
+
.filter((s) => s.hasDescription && !s.userInvoked && s.description)
|
|
4089
|
+
.map((s) => ({ name: s.name, description: s.description ?? "" }));
|
|
4090
|
+
if (skills.length === 0) {
|
|
4091
|
+
if (!json) {
|
|
4092
|
+
console.log("\nℹ no model-invocable skills with a description to measure.");
|
|
4093
|
+
}
|
|
4094
|
+
return;
|
|
4095
|
+
}
|
|
4096
|
+
const promptSet = (0, audit_prompts_js_1.autoTriggerPrompts)(skills);
|
|
4097
|
+
if (!json) {
|
|
4098
|
+
console.log("\nℹ auto-generated probe prompts from skill descriptions (pass --prompts=<file> for a curated set).");
|
|
4099
|
+
}
|
|
4100
|
+
const trigger = await (0, scan_behavioral_js_1.probePluginTriggers)(dir, promptSet, {
|
|
4101
|
+
minPrompts: audit_prompts_js_1.AUTO_RECALL_COUNT,
|
|
4102
|
+
minDistance: audit_prompts_js_1.AUTO_MIN_DISTANCE,
|
|
4103
|
+
model: flagValue(args, "--model"),
|
|
4104
|
+
harness,
|
|
4105
|
+
// Discover candidates with the resolved adapter's layout/dialect — a Codex
|
|
4106
|
+
// repo's skills live under the Codex layout, not the default CC one.
|
|
4107
|
+
layout: adapter.layout,
|
|
4108
|
+
dialect: adapter.dialect,
|
|
4109
|
+
});
|
|
4110
|
+
console.log(json
|
|
4111
|
+
? JSON.stringify({ trigger }, null, 2)
|
|
4112
|
+
: "\n" + (0, scan_behavioral_js_1.formatBehavioralReport)(trigger));
|
|
4113
|
+
}
|
|
4114
|
+
/**
|
|
4115
|
+
* The ONE read-vs-run decision for a single-plugin `audit`. A plain `audit` is a
|
|
4116
|
+
* deterministic READ; the executing checks are opt-in via a single consent —
|
|
4117
|
+
* `decideExecute` resolves it (run / ask / skip). At a TTY we ASK ONCE (bundled,
|
|
4118
|
+
* with a confinement + cost DISCLOSURE) and remember the answer in `.vigilesrc.json`;
|
|
4119
|
+
* headless we stay a read (the `note` is the loud nudge, printed by the caller
|
|
4120
|
+
* AFTER the report). Never hangs an agent / CI run (`great-agent-flow`).
|
|
4121
|
+
*
|
|
4122
|
+
* Returns `execute` (run the executing checks?) + a `note` to print at the end.
|
|
4038
4123
|
*/
|
|
4039
|
-
async function
|
|
4040
|
-
const
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
//
|
|
4044
|
-
//
|
|
4045
|
-
// the non-blocking hint path, not block on a read that never gets input.
|
|
4046
|
-
// `isTTY` is `undefined` at runtime when not a terminal (falsy → hint path).
|
|
4124
|
+
async function resolveExecution(s, json, args, harness) {
|
|
4125
|
+
const decision = (0, scan_trigger_suggest_js_1.decideExecute)({
|
|
4126
|
+
hasExecutable: s.hasMcp || s.triggerableSkills > 0 || s.adoptableRefs,
|
|
4127
|
+
// A human is "interactive" only when BOTH streams are a terminal — `askOnce`
|
|
4128
|
+
// reads stdin, so a TTY stdout with piped/redirected stdin (agents, shell
|
|
4129
|
+
// pipelines) must NOT block on a read that never gets input.
|
|
4047
4130
|
isTTY: process.stdout.isTTY && process.stdin.isTTY,
|
|
4048
|
-
triggerableSkills: triggerable.length,
|
|
4049
4131
|
json,
|
|
4050
4132
|
noInteractive: args.includes("--no-interactive") || args.includes("--yes"),
|
|
4133
|
+
remembered: (0, validate_js_1.loadConfig)().audit?.measure,
|
|
4051
4134
|
});
|
|
4052
|
-
if (decision === "
|
|
4053
|
-
return;
|
|
4054
|
-
if (decision === "
|
|
4055
|
-
|
|
4056
|
-
|
|
4135
|
+
if (decision.kind === "run")
|
|
4136
|
+
return { execute: true, note: null };
|
|
4137
|
+
if (decision.kind === "skip")
|
|
4138
|
+
return {
|
|
4139
|
+
execute: false,
|
|
4140
|
+
note: json ? null : (0, scan_trigger_suggest_js_1.formatExecuteSkip)(decision.reason),
|
|
4141
|
+
};
|
|
4142
|
+
// ask — prompt once, then remember the answer.
|
|
4143
|
+
const answer = await askOnce(buildExecuteDisclosure(s, harness));
|
|
4144
|
+
const yes = /^y(es)?$/i.test(answer); // default NO (executes your hooks / servers)
|
|
4145
|
+
rememberAuditMeasure(yes);
|
|
4146
|
+
return {
|
|
4147
|
+
execute: yes,
|
|
4148
|
+
note: yes
|
|
4149
|
+
? null
|
|
4150
|
+
: " Skipped (remembered — edit .vigilesrc.json `audit.measure` to change).",
|
|
4151
|
+
};
|
|
4152
|
+
}
|
|
4153
|
+
/** The bundled consent prompt — discloses exactly what will execute (and what it
|
|
4154
|
+
* costs) so the yes is informed. Default NO. Harness-aware: a Codex repo measures
|
|
4155
|
+
* via the codex CLI (not a Claude env var), so the cost wording must not falsely
|
|
4156
|
+
* read "no model access" in exactly the case the prompt is meant to disclose. */
|
|
4157
|
+
function buildExecuteDisclosure(s, harness) {
|
|
4158
|
+
const lines = ["\nRun the executing checks against your harness?"];
|
|
4159
|
+
if (s.hasMcp)
|
|
4160
|
+
lines.push(" · start your MCP servers — connects to their backends");
|
|
4161
|
+
if (s.triggerableSkills > 0) {
|
|
4162
|
+
lines.push(` · measure whether skills fire (${triggerCostWording(harness)})`);
|
|
4163
|
+
}
|
|
4164
|
+
if (s.adoptableRefs) {
|
|
4165
|
+
lines.push(` · draft + verify your instruction file's references (${triggerCostWording(harness)})`);
|
|
4166
|
+
}
|
|
4167
|
+
lines.push("Asked once — remembered in .vigilesrc.json. [y/N] ");
|
|
4168
|
+
return lines.join("\n");
|
|
4169
|
+
}
|
|
4170
|
+
/** Cost/availability wording for the trigger tier, per harness. Codex runs on the
|
|
4171
|
+
* codex CLI (its own auth/plan), so it's never gated on a Claude env var. */
|
|
4172
|
+
function triggerCostWording(harness) {
|
|
4173
|
+
if (harness === "codex")
|
|
4174
|
+
return "your Codex CLI, $0 metered — skips if `codex` isn't on PATH";
|
|
4175
|
+
return !(0, scan_trigger_suggest_js_1.hasModelAccess)(process.env)
|
|
4176
|
+
? "needs model access — none detected, will skip"
|
|
4177
|
+
: (0, scan_trigger_suggest_js_1.isMeteredAccess)(process.env)
|
|
4178
|
+
? "⚠ spends API credits"
|
|
4179
|
+
: "your subscription, $0 metered";
|
|
4180
|
+
}
|
|
4181
|
+
/** Run the trigger tier: a curated `--prompts` file, else auto-generated probes. */
|
|
4182
|
+
async function runTriggerTier(dir, report, adapter, args) {
|
|
4183
|
+
if (flagValue(args, "--prompts")) {
|
|
4184
|
+
await handleMeasure([dir], args);
|
|
4185
|
+
}
|
|
4186
|
+
else {
|
|
4187
|
+
await runAutoTrigger(dir, report, adapter, args);
|
|
4057
4188
|
}
|
|
4058
|
-
await promptTriggerSetup(report, dir, triggerable.length, args);
|
|
4059
4189
|
}
|
|
4060
4190
|
/** Ask one question on a fresh readline, closing it after (the codebase pattern). */
|
|
4061
4191
|
async function askOnce(q) {
|
|
@@ -4075,29 +4205,33 @@ async function askOnce(q) {
|
|
|
4075
4205
|
rl.close();
|
|
4076
4206
|
}
|
|
4077
4207
|
}
|
|
4078
|
-
/**
|
|
4079
|
-
*
|
|
4080
|
-
*
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4208
|
+
/**
|
|
4209
|
+
* Persist the audit consent (`audit.measure`) into `.vigilesrc.json` without
|
|
4210
|
+
* clobbering existing keys — the "ask once, remember" sticky choice. Best-effort:
|
|
4211
|
+
* a malformed user config is left untouched, a write error is non-fatal (the
|
|
4212
|
+
* measurement already ran / was skipped; only the memory is lost).
|
|
4213
|
+
*/
|
|
4214
|
+
function rememberAuditMeasure(value) {
|
|
4215
|
+
const configPath = (0, node_path_1.resolve)(process.cwd(), ".vigilesrc.json");
|
|
4216
|
+
let existing = {};
|
|
4217
|
+
if ((0, node_fs_1.existsSync)(configPath)) {
|
|
4218
|
+
try {
|
|
4219
|
+
existing = JSON.parse((0, node_fs_1.readFileSync)(configPath, "utf-8"));
|
|
4220
|
+
}
|
|
4221
|
+
catch {
|
|
4222
|
+
return; // user-owned malformed config — never clobber it
|
|
4223
|
+
}
|
|
4091
4224
|
}
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4225
|
+
const prevAudit = typeof existing.audit === "object" && existing.audit !== null
|
|
4226
|
+
? existing.audit
|
|
4227
|
+
: {};
|
|
4228
|
+
const merged = { ...existing, audit: { ...prevAudit, measure: value } };
|
|
4229
|
+
try {
|
|
4230
|
+
(0, node_fs_1.writeFileSync)(configPath, JSON.stringify(merged, null, 2) + "\n");
|
|
4231
|
+
}
|
|
4232
|
+
catch {
|
|
4233
|
+
/* non-fatal — the run already happened; only the remembered choice is lost */
|
|
4095
4234
|
}
|
|
4096
|
-
(0, node_fs_1.writeFileSync)(promptsPath, (0, scan_trigger_suggest_js_1.scaffoldTriggerPrompts)(report.skills
|
|
4097
|
-
.filter((s) => s.hasDescription && !s.userInvoked)
|
|
4098
|
-
.map((s) => s.name)));
|
|
4099
|
-
console.log(" ✓ Wrote trigger-prompts.json — fill in the placeholders, then run:");
|
|
4100
|
-
console.log(` vigiles scan ${dir} --trigger --prompts=trigger-prompts.json`);
|
|
4101
4235
|
}
|
|
4102
4236
|
async function main() {
|
|
4103
4237
|
const args = process.argv.slice(2);
|
|
@@ -4121,7 +4255,7 @@ async function main() {
|
|
|
4121
4255
|
// still runs the full wizard (project detection + auto-targets).
|
|
4122
4256
|
const hasTarget = args.some((a) => a.startsWith("--target="));
|
|
4123
4257
|
if (hasTarget) {
|
|
4124
|
-
|
|
4258
|
+
scaffoldSpec(args.slice(1));
|
|
4125
4259
|
}
|
|
4126
4260
|
else {
|
|
4127
4261
|
await setup(args);
|
|
@@ -4184,19 +4318,15 @@ async function main() {
|
|
|
4184
4318
|
case "eval":
|
|
4185
4319
|
handleRunScripts("eval", args, restArgs);
|
|
4186
4320
|
break;
|
|
4187
|
-
case "
|
|
4188
|
-
//
|
|
4189
|
-
//
|
|
4190
|
-
//
|
|
4191
|
-
//
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
if (args.includes("--explain")) {
|
|
4197
|
-
handleExplain(restArgs, args);
|
|
4198
|
-
break;
|
|
4199
|
-
}
|
|
4321
|
+
case "audit": {
|
|
4322
|
+
// The Lighthouse run: a plain `audit` is a deterministic READ — rings, each
|
|
4323
|
+
// finding's fix inline, HTML/JSON report — safe + identical on every OS,
|
|
4324
|
+
// nothing executes. Like Lighthouse it's a LOCAL report, NOT a CI step (CI
|
|
4325
|
+
// uses `vigiles lint`). The executing checks (safety battery + live MCP +
|
|
4326
|
+
// skill firing) run only on consent: at a TTY `audit` asks once (remembered
|
|
4327
|
+
// in `.vigilesrc.json`), headless it stays a read + a one-line nudge. There
|
|
4328
|
+
// is NO execution flag — automation tests the harness via the vigiles/testing
|
|
4329
|
+
// API + skills, not the report verb. See the `audit-side-effect-free` rule.
|
|
4200
4330
|
const dirs = restArgs.length > 0 ? restArgs : ["."];
|
|
4201
4331
|
const json = args.includes("--json");
|
|
4202
4332
|
// A single dir that's a marketplace (e.g. wshobson/agents' 80+ plugins
|
|
@@ -4249,19 +4379,51 @@ async function main() {
|
|
|
4249
4379
|
}
|
|
4250
4380
|
console.log("");
|
|
4251
4381
|
}
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4382
|
+
// The versioned AuditReport is the product boundary — the same JSON the
|
|
4383
|
+
// HTML renders, `--json` emits, and (later) a hosted dashboard ingests.
|
|
4384
|
+
// Built ONCE; the rings + fix list are read off it. Pure deterministic —
|
|
4385
|
+
// nothing executes to produce it.
|
|
4386
|
+
const auditReport = (0, audit_report_js_1.buildAuditReport)(report, {
|
|
4387
|
+
harness: adapter.name,
|
|
4388
|
+
vigilesVersion: getVersion(),
|
|
4389
|
+
});
|
|
4390
|
+
const sc = auditReport.score;
|
|
4391
|
+
const plan = (0, optimize_js_1.optimize)(report);
|
|
4392
|
+
if (!json) {
|
|
4393
|
+
// The Lighthouse rings: per-category 0–100 + the weighted overall,
|
|
4394
|
+
// shown before the detailed report so the headline signal leads.
|
|
4395
|
+
console.log((0, audit_score_js_1.formatAuditScore)(sc));
|
|
4396
|
+
console.log("");
|
|
4257
4397
|
}
|
|
4258
|
-
|
|
4259
|
-
|
|
4398
|
+
console.log(json
|
|
4399
|
+
? JSON.stringify(auditReport, null, 2)
|
|
4400
|
+
: (0, scan_js_1.formatScanReport)(report));
|
|
4401
|
+
if (!json) {
|
|
4402
|
+
// Fold each finding's fix inline (replaces the former --fix-plan/--explain
|
|
4403
|
+
// flags): the deterministic, free recommendation list under the report.
|
|
4404
|
+
const fixes = (0, optimize_js_1.formatRecommendations)(plan);
|
|
4405
|
+
if (fixes)
|
|
4406
|
+
console.log("\n" + fixes);
|
|
4260
4407
|
}
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4408
|
+
// ONE read-vs-run decision for the EXECUTING checks (live MCP + skill
|
|
4409
|
+
// firing). A plain `audit` is a deterministic READ; these run only on
|
|
4410
|
+
// consent — ASK once at a TTY (remembered); headless stays a read + a
|
|
4411
|
+
// nudge (no execution flag — automation uses the vigiles/testing API).
|
|
4412
|
+
// (The safety battery is NOT here — it needs cross-platform confinement
|
|
4413
|
+
// that isn't shipped, so it lives in the vigiles/testing API.)
|
|
4414
|
+
const isForeign = root !== process.cwd();
|
|
4415
|
+
const surfaces = {
|
|
4416
|
+
hasMcp: report.mcp && !isForeign,
|
|
4417
|
+
triggerableSkills: report.skills.filter((s) => s.hasDescription && !s.userInvoked).length,
|
|
4418
|
+
adoptableRefs: adapter.name === "claude-code" &&
|
|
4419
|
+
(0, node_fs_1.existsSync)((0, node_path_1.resolve)(root, adapter.layout.instructionFile)),
|
|
4420
|
+
};
|
|
4421
|
+
const { execute, note: execNote } = await resolveExecution(surfaces, json, args, adapter.name);
|
|
4422
|
+
// LIVE MCP tool resolution STARTS each declared MCP server — a server is
|
|
4423
|
+
// exactly what connects to a real Postgres / authenticates a real API on
|
|
4424
|
+
// boot. So it runs only under consent (`execute`) AND own-repo (never
|
|
4425
|
+
// spawn a stranger's server).
|
|
4426
|
+
if (execute && surfaces.hasMcp) {
|
|
4265
4427
|
const mcpErrs = await (0, scan_js_1.verifyLiveMcpTools)(report, adapter.layout, adapter.dialect);
|
|
4266
4428
|
console.log(json
|
|
4267
4429
|
? JSON.stringify({ mcpContractTools: mcpErrs }, null, 2)
|
|
@@ -4280,9 +4442,76 @@ async function main() {
|
|
|
4280
4442
|
if (args.includes("--fail-on-widen") && diff.widened)
|
|
4281
4443
|
process.exitCode = 1;
|
|
4282
4444
|
}
|
|
4283
|
-
//
|
|
4284
|
-
// the
|
|
4285
|
-
|
|
4445
|
+
// The model trigger tier — "do your skills actually FIRE?" — runs as part
|
|
4446
|
+
// of the same consent (`execute`), and only when a model is reachable;
|
|
4447
|
+
// otherwise it's a one-line note (never a hang).
|
|
4448
|
+
if (execute && surfaces.triggerableSkills > 0) {
|
|
4449
|
+
// Model-access detection is per-harness: `hasModelAccess` reads Claude
|
|
4450
|
+
// env (claude CLI / ANTHROPIC_API_KEY). A Codex repo authenticates the
|
|
4451
|
+
// codex CLI instead, so we DON'T gate it on a Claude var — the Codex
|
|
4452
|
+
// probe checks `codexDriver.available()` internally and self-reports
|
|
4453
|
+
// unavailable. (harness-parity: never block Codex behind a CC check.)
|
|
4454
|
+
const modelReachable = adapter.name === "codex" || (0, scan_trigger_suggest_js_1.hasModelAccess)(process.env);
|
|
4455
|
+
if (modelReachable) {
|
|
4456
|
+
await runTriggerTier(targets[0], report, adapter, args);
|
|
4457
|
+
}
|
|
4458
|
+
else if (!json) {
|
|
4459
|
+
console.log("\nℹ skill firing not measured — no model access (authenticate the `claude` CLI or set ANTHROPIC_API_KEY).");
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
// The adoption preview — "what would vigiles catch in YOUR repo?" The model
|
|
4463
|
+
// DRAFTS the verifiable refs in the instruction file; the cross-ref engine
|
|
4464
|
+
// VERIFIES each, so "M broken right now" is trustworthy though the extraction
|
|
4465
|
+
// is probabilistic. Same consent as the trigger tier (`surfaces.adoptableRefs`
|
|
4466
|
+
// makes a bare instruction-file repo consent-eligible — the prime adoption
|
|
4467
|
+
// target). v1: instruction-file only; drafting drives the `claude` CLI, so
|
|
4468
|
+
// `adoptableRefs` is Claude Code only (the Codex deferral note is printed
|
|
4469
|
+
// below as a LOUD harness-parity deferral, never a silent CC-only path).
|
|
4470
|
+
let adoptabilityResult;
|
|
4471
|
+
if (execute && surfaces.adoptableRefs) {
|
|
4472
|
+
if ((0, scan_trigger_suggest_js_1.hasModelAccess)(process.env)) {
|
|
4473
|
+
const instrPath = (0, node_path_1.resolve)(root, adapter.layout.instructionFile);
|
|
4474
|
+
adoptabilityResult = await (0, adoptability_js_1.runAdoptabilityTier)({
|
|
4475
|
+
instructionContent: (0, node_fs_1.readFileSync)(instrPath, "utf-8"),
|
|
4476
|
+
basePath: root,
|
|
4477
|
+
});
|
|
4478
|
+
if (!json)
|
|
4479
|
+
console.log("\n" +
|
|
4480
|
+
(0, adoptability_js_1.formatAdoptability)(adoptabilityResult, adapter.layout.instructionFile));
|
|
4481
|
+
}
|
|
4482
|
+
else if (!json && surfaces.triggerableSkills === 0) {
|
|
4483
|
+
// Only when the trigger tier didn't already print the same note.
|
|
4484
|
+
console.log("\nℹ adoptability not measured — no model access (authenticate the `claude` CLI or set ANTHROPIC_API_KEY).");
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
// LOUD harness-parity deferral: adoptability drafting drives the `claude`
|
|
4488
|
+
// CLI, so a Codex repo with an instruction file is told it's a follow-up,
|
|
4489
|
+
// never silently skipped (research/adoption-gateway-preview.md, increment 4).
|
|
4490
|
+
if (!json &&
|
|
4491
|
+
adapter.name === "codex" &&
|
|
4492
|
+
(0, node_fs_1.existsSync)((0, node_path_1.resolve)(root, adapter.layout.instructionFile))) {
|
|
4493
|
+
console.log("\nℹ adoptability preview (what vigiles would catch in your repo) is Claude Code only for now — Codex support is a follow-up.");
|
|
4494
|
+
}
|
|
4495
|
+
// The loud read-vs-run nudge for a headless / remembered-no skip — printed
|
|
4496
|
+
// AFTER the report so the deterministic read leads.
|
|
4497
|
+
if (execNote)
|
|
4498
|
+
console.log(execNote);
|
|
4499
|
+
// The final report folds in the adoptability preview (when the model-gated
|
|
4500
|
+
// tier ran) so the written HTML/JSON carry it; a deterministic read omits it.
|
|
4501
|
+
const finalReport = adoptabilityResult
|
|
4502
|
+
? { ...auditReport, adoptability: adoptabilityResult }
|
|
4503
|
+
: auditReport;
|
|
4504
|
+
// The shareable HTML report — written by default (--no-html to skip), and
|
|
4505
|
+
// opened best-effort only for a human at a TTY (never spawn a browser for
|
|
4506
|
+
// an agent / CI run).
|
|
4507
|
+
if (!json && !args.includes("--no-html")) {
|
|
4508
|
+
writeAuditHtml(finalReport);
|
|
4509
|
+
}
|
|
4510
|
+
// The versioned JSON artifact — the upload/CI boundary (a hosted dashboard
|
|
4511
|
+
// ingests this). Written by default in the human path; --no-json to skip.
|
|
4512
|
+
if (!json && !args.includes("--no-json")) {
|
|
4513
|
+
writeAuditJson(finalReport);
|
|
4514
|
+
}
|
|
4286
4515
|
}
|
|
4287
4516
|
break;
|
|
4288
4517
|
}
|
package/dist/core/inline.js
CHANGED
|
@@ -35,6 +35,16 @@ const ENFORCE_RE = /<!--\s*vigiles:enforce\s+([@A-Za-z0-9_/:.-]+)\s+"([^"\n]*)"\
|
|
|
35
35
|
* ambiguous against the closing `-->`.
|
|
36
36
|
*/
|
|
37
37
|
const FILE_RE = /<!--\s*vigiles:file\s+(\S+)\s*-->/;
|
|
38
|
+
/**
|
|
39
|
+
* Strip a single surrounding pair of matching quotes from a captured token.
|
|
40
|
+
* The documented `vigiles:file` syntax is an unquoted path, but users
|
|
41
|
+
* reasonably quote it for symmetry with `vigiles:cmd "..."` — without this,
|
|
42
|
+
* the quotes become part of the path, so it never resolves and the error
|
|
43
|
+
* reads `File not found: ""path""`. Normalize at the parse boundary.
|
|
44
|
+
*/
|
|
45
|
+
function unquote(token) {
|
|
46
|
+
return /^"[^]*"$|^'[^]*'$/.test(token) ? token.slice(1, -1) : token;
|
|
47
|
+
}
|
|
38
48
|
/**
|
|
39
49
|
* Match `<!-- vigiles:cmd "<command>" -->`. The command is quoted because
|
|
40
50
|
* commands contain spaces (e.g. `npm run build`).
|
|
@@ -122,7 +132,7 @@ function parseInlineRules(content) {
|
|
|
122
132
|
}
|
|
123
133
|
const fileMatch = FILE_RE.exec(scannable);
|
|
124
134
|
if (fileMatch) {
|
|
125
|
-
files.push({ path: fileMatch[1], line: i + 1 });
|
|
135
|
+
files.push({ path: unquote(fileMatch[1]), line: i + 1 });
|
|
126
136
|
continue;
|
|
127
137
|
}
|
|
128
138
|
const cmdMatch = CMD_RE.exec(scannable);
|
package/dist/core/types.d.ts
CHANGED
|
@@ -263,6 +263,18 @@ export interface VigilesConfig {
|
|
|
263
263
|
* `"claude-code"`. See research/multi-harness-compile.md.
|
|
264
264
|
*/
|
|
265
265
|
harness?: string | string[];
|
|
266
|
+
/**
|
|
267
|
+
* `vigiles audit` preferences. `measure` is the sticky remembered answer to the
|
|
268
|
+
* "run the executing checks against your harness?" prompt — at a TTY `audit`
|
|
269
|
+
* asks once, then records the choice here so it never asks again. `true` runs
|
|
270
|
+
* the executing checks (safety battery · live MCP · skill firing) on every
|
|
271
|
+
* interactive run, `false` keeps them off (edit this key to change). Written by
|
|
272
|
+
* the audit consent prompt, not `init`. Headless runs never execute regardless
|
|
273
|
+
* (audit is a local report, not a CI step — there is no execution flag).
|
|
274
|
+
*/
|
|
275
|
+
audit?: {
|
|
276
|
+
measure?: boolean;
|
|
277
|
+
};
|
|
266
278
|
}
|
|
267
279
|
/** Valid marker types for rule detection. */
|
|
268
280
|
export type MarkerType = "headings" | "checkboxes";
|
package/dist/dialect-drift.js
CHANGED
|
@@ -40,7 +40,7 @@ exports.formatDialectDrift = formatDialectDrift;
|
|
|
40
40
|
*
|
|
41
41
|
* Pure parsers (testable with fixtures) + a local-install locator. TWO consumers:
|
|
42
42
|
* the gated CI test in `dialect-drift.test.ts` (fails loud on tool/event drift), and
|
|
43
|
-
* `vigiles
|
|
43
|
+
* `vigiles audit` at runtime via `checkDialectDrift`/`formatDialectDrift` (a best-effort,
|
|
44
44
|
* read-local freshness WARN when the installed CC's tool surface drifts from ours).
|
|
45
45
|
*/
|
|
46
46
|
const node_fs_1 = require("node:fs");
|