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/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
- function init(args) {
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|scan|generate-types|generate-schema|init)\b/.test(content);
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
- audit: "lint", // v3 → v4 rename
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 (audit → lint).
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 (init() does the convert), not clobbered with a blank one — so the
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
- init(["--target=" + target]); // adopts existing content, else blank scaffold
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
- * `vigiles scan <dir> --trigger` — the MODEL-GATED behavioral report on a plugin (the paid
2713
- * tier; `scan` stays free/deterministic). Loads the author-supplied per-skill prompt
2714
- * sets (`--prompts`) and reports BOTH behavioral columns: trigger-rate (does each
2715
- * skill actually FIRE — recall + precision) and the selection-collision matrix (does
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 scan ./plugin --trigger --prompts=p.json`.
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 scan [dir...] Report what a plugin ships + what's broken (free; 2+ dirs → leaderboard)");
3231
- console.log(" --trigger: do skills fire/collide? (real model) · --explain: why a surface underperforms · --fix-plan");
3232
- console.log(" with model access + a TTY, scan offers to measure firing; --no-interactive/--yes/--json hint instead (agents)");
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
- * After a single-plugin `scan` report: if the plugin ships model-invocable
4034
- * skills AND a real model is reachable, surface the real-model `--trigger` tier
4035
- * that measures whether those skills actually FIRE. A human at a TTY is offered
4036
- * setup; an agent / CI (non-TTY / `--json` / `--no-interactive` / `--yes`) gets a
4037
- * one-line, non-blocking hint — a `scan` must never hang (`great-agent-flow`).
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 maybeSuggestTrigger(report, dir, json, args) {
4040
- const triggerable = report.skills.filter((s) => s.hasDescription && !s.userInvoked);
4041
- const decision = (0, scan_trigger_suggest_js_1.decideTriggerSuggestion)({
4042
- modelAccess: (0, scan_trigger_suggest_js_1.hasModelAccess)(process.env),
4043
- // Prompt only when BOTH streams are a terminal — `askOnce` reads stdin, so a
4044
- // TTY stdout with piped/redirected stdin (agents, shell pipelines) must take
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 === "none")
4053
- return;
4054
- if (decision === "hint") {
4055
- console.log("\n" + (0, scan_trigger_suggest_js_1.formatTriggerHint)(dir, triggerable.length));
4056
- return;
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
- /** Interactive (TTY) trigger-tier setup: run an existing prompts file now, or
4079
- * scaffold one to fill in. The real-model run stays an explicit confirmation so
4080
- * a plain `scan` never spends a token without a yes. */
4081
- async function promptTriggerSetup(report, dir, n, args) {
4082
- const promptsPath = (0, node_path_1.resolve)(process.cwd(), "trigger-prompts.json");
4083
- const exists = (0, node_fs_1.existsSync)(promptsPath);
4084
- const q = exists
4085
- ? `\nℹ Model access detected. Measure whether your ${String(n)} skill(s) FIRE now using trigger-prompts.json (real model)? [y/N] `
4086
- : `\nℹ ${String(n)} model-invocable skill(s) + model access detected. Scaffold trigger-prompts.json to measure firing? [y/N] `;
4087
- const answer = await askOnce(q);
4088
- if (!/^y(es)?$/i.test(answer)) {
4089
- console.log(" Skipped. " + (0, scan_trigger_suggest_js_1.formatTriggerHint)(dir, n));
4090
- return;
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
- if (exists) {
4093
- await handleMeasure([dir], [...args, "--prompts=trigger-prompts.json"]);
4094
- return;
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
- init(args.slice(1));
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 "scan": {
4188
- // Model-gated behavioral column + the deterministic diagnostic, folded into
4189
- // scan (formerly the `measure` / `explain` verbs): `--trigger` measures
4190
- // whether each skill FIRES / COLLIDES (real model), `--explain` is the
4191
- // free WHY-a-surface-underperforms + the fix.
4192
- if (args.includes("--trigger")) {
4193
- await handleMeasure(restArgs, args);
4194
- break;
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
- if (args.includes("--fix-plan")) {
4253
- // The deterministic optimization lens on the SAME report: health score
4254
- // + the ranked free fixes to clear before measuring (the A2 spine).
4255
- const plan = (0, optimize_js_1.optimize)(report);
4256
- console.log(json ? JSON.stringify(plan, null, 2) : (0, optimize_js_1.formatOptimize)(plan));
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
- else {
4259
- console.log(json ? JSON.stringify(report, null, 2) : (0, scan_js_1.formatScanReport)(report));
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
- if (args.includes("--verify-mcp")) {
4262
- // Opt-in LIVE MCP tool resolution: starts each declared server and checks
4263
- // the agent's mcp__server__tool refs actually exist (the dynamic check no
4264
- // static linter can do). Side-effecting (spawns servers) → opt-in only.
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
- // Nudge toward the real-model trigger tier when a model is reachable and
4284
- // the plugin ships model-invocable skills (hint for agents, offer for humans).
4285
- await maybeSuggestTrigger(report, targets[0], json, args);
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
  }
@@ -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);
@@ -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";
@@ -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 scan` at runtime via `checkDialectDrift`/`formatDialectDrift` (a best-effort,
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");