vigiles 7.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 +207 -88
- 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 +3 -7
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +749 -180
- package/dist/core/adopt.d.ts +65 -0
- package/dist/core/adopt.js +199 -0
- package/dist/core/compose.d.ts +1 -1
- package/dist/core/compose.js +1 -1
- package/dist/core/evolve.d.ts +4 -0
- package/dist/core/evolve.js +4 -0
- package/dist/core/frontmatter.d.ts +8 -7
- package/dist/core/frontmatter.js +8 -7
- package/dist/core/generate-harness.d.ts +1 -1
- package/dist/core/generate-harness.js +3 -3
- package/dist/core/generate-schema.js +1 -1
- package/dist/core/inline.d.ts +6 -6
- package/dist/core/inline.js +17 -7
- package/dist/core/integrity.d.ts +31 -0
- package/dist/core/integrity.js +45 -0
- package/dist/core/orphans.js +1 -1
- package/dist/core/spec.d.ts +40 -2
- package/dist/core/spec.js +16 -1
- package/dist/core/types.d.ts +42 -6
- package/dist/core/validate.js +26 -26
- package/dist/dialect-drift.js +1 -1
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +1 -1
- package/dist/guardrail-check.d.ts +1 -1
- package/dist/guardrail-check.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 +91 -0
- package/dist/scan-trigger-suggest.js +103 -0
- package/dist/scan.d.ts +53 -12
- package/dist/scan.js +92 -16
- package/dist/score-explainer.d.ts +1 -1
- package/dist/setup-plan.d.ts +59 -1
- package/dist/setup-plan.js +103 -5
- package/hooks/post-edit.sh +1 -1
- package/package.json +4 -2
- package/skills/adopt-spec/SKILL.md +7 -7
- package/skills/linter-docs/eslint.md +1 -1
- package/skills/strengthen/SKILL.md +1 -1
- package/skills/test-harness/SKILL.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* vigiles init — scaffold a spec from scratch
|
|
8
8
|
* vigiles compile — compile .spec.ts → .md with linter verification
|
|
9
9
|
* vigiles lint — verify hashes, report coverage, detect duplicates
|
|
10
|
-
* vigiles generate
|
|
10
|
+
* vigiles generate types — emit .d.ts with types from project state
|
|
11
11
|
*/
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
const node_fs_1 = require("node:fs");
|
|
@@ -24,13 +24,18 @@ const test_coverage_js_1 = require("./test-coverage.js");
|
|
|
24
24
|
const scaffold_test_js_1 = require("./scaffold-test.js");
|
|
25
25
|
const effects_js_1 = require("./core/effects.js");
|
|
26
26
|
const scan_js_1 = require("./scan.js");
|
|
27
|
+
const scan_trigger_suggest_js_1 = require("./scan-trigger-suggest.js");
|
|
27
28
|
const dialect_drift_js_1 = require("./dialect-drift.js");
|
|
28
|
-
const score_explainer_js_1 = require("./score-explainer.js");
|
|
29
29
|
const scan_behavioral_js_1 = require("./scan-behavioral.js");
|
|
30
30
|
const adapter_registry_js_1 = require("./adapter-registry.js");
|
|
31
31
|
const skill_harness_js_1 = require("./skill-harness.js");
|
|
32
32
|
const leaderboard_js_1 = require("./leaderboard.js");
|
|
33
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");
|
|
34
39
|
const compile_js_1 = require("./core/compile.js");
|
|
35
40
|
const proofs_js_1 = require("./core/proofs.js");
|
|
36
41
|
const inline_js_1 = require("./core/inline.js");
|
|
@@ -54,6 +59,7 @@ const linters_js_1 = require("./core/linters.js");
|
|
|
54
59
|
const harness_test_js_1 = require("./harness-test.js");
|
|
55
60
|
const run_scripts_js_1 = require("./adapters/claude-code/run-scripts.js");
|
|
56
61
|
const integrity_js_1 = require("./core/integrity.js");
|
|
62
|
+
const adopt_js_1 = require("./core/adopt.js");
|
|
57
63
|
const coverage_js_1 = require("./core/coverage.js");
|
|
58
64
|
const orphans_js_1 = require("./core/orphans.js");
|
|
59
65
|
const doc_refs_js_1 = require("./core/doc-refs.js");
|
|
@@ -447,7 +453,7 @@ function validateSpecs(filePaths, rulesConfig, silent = false) {
|
|
|
447
453
|
// Verify the referenced spec still exists
|
|
448
454
|
const specRef = (0, node_path_1.resolve)(process.cwd(), hashMatch[1]);
|
|
449
455
|
if (!(0, node_fs_1.existsSync)(specRef)) {
|
|
450
|
-
log(` ✗ [require-spec] ${filePath} references "${hashMatch[1]}" but that spec no longer exists.`);
|
|
456
|
+
log(` ✗ [require-instructions-spec] ${filePath} references "${hashMatch[1]}" but that spec no longer exists.`);
|
|
451
457
|
allValid = false;
|
|
452
458
|
}
|
|
453
459
|
continue;
|
|
@@ -476,7 +482,7 @@ function check(filePaths, silent = false) {
|
|
|
476
482
|
// `validateSpecs` only returns a boolean today, so we collapse
|
|
477
483
|
// failures to 1 until it starts reporting counts. Kept in its own
|
|
478
484
|
// counter so lint's "stale hash — run vigiles compile" remediation
|
|
479
|
-
// doesn't misreport a require-spec / other validation failure.
|
|
485
|
+
// doesn't misreport a require-instructions-spec / other validation failure.
|
|
480
486
|
validationErrors: specsValid ? 0 : 1,
|
|
481
487
|
};
|
|
482
488
|
}
|
|
@@ -658,6 +664,7 @@ function lintExitCode(report) {
|
|
|
658
664
|
report.descriptionOverlapErrors > 0 ||
|
|
659
665
|
report.frontmatterValidErrors > 0 ||
|
|
660
666
|
report.mcpHookErrors > 0 ||
|
|
667
|
+
report.preferCompiledHookErrors > 0 ||
|
|
661
668
|
report.symbolRefErrors > 0 ||
|
|
662
669
|
report.mcpRefErrors > 0)
|
|
663
670
|
return 2;
|
|
@@ -983,6 +990,9 @@ async function runLint(restArgs, flags, config) {
|
|
|
983
990
|
// 7m. MCP hook-target — a `type: mcp_tool` hook action that's incomplete or
|
|
984
991
|
// targets an undeclared server (the moat applied to the hook surface).
|
|
985
992
|
const mcpHookTargets = checkMcpHookTargets(config, silent, adapter);
|
|
993
|
+
// 7n. Prefer-compiled-hooks — ONE discovery nudge (not per-hook) toward
|
|
994
|
+
// compiled `vigiles/hook` artifacts when hand-written hooks ship. Recommendation.
|
|
995
|
+
const preferCompiledHooks = checkPreferCompiledHooks(config, silent, adapter);
|
|
986
996
|
// 8. Validate vigiles builder calls inside markdown code blocks. Default
|
|
987
997
|
// is to validate every ref; illustrative blocks opt out via
|
|
988
998
|
// `<!-- vigiles:ignore -->` (single block) or
|
|
@@ -1048,6 +1058,8 @@ async function runLint(restArgs, flags, config) {
|
|
|
1048
1058
|
frontmatterValidErrors: frontmatterValid.errors,
|
|
1049
1059
|
mcpHookIssues: mcpHookTargets.issues,
|
|
1050
1060
|
mcpHookErrors: mcpHookTargets.errors,
|
|
1061
|
+
preferCompiledHookIssues: preferCompiledHooks.issues,
|
|
1062
|
+
preferCompiledHookErrors: preferCompiledHooks.errors,
|
|
1051
1063
|
docRefErrors: docRefReport.errors.length,
|
|
1052
1064
|
symbolRefErrors,
|
|
1053
1065
|
mcpRefErrors,
|
|
@@ -1198,14 +1210,40 @@ function targetHasHash(absPath) {
|
|
|
1198
1210
|
return false;
|
|
1199
1211
|
}
|
|
1200
1212
|
}
|
|
1201
|
-
|
|
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) {
|
|
1202
1223
|
const targetFlag = args.find((a) => a.startsWith("--target="));
|
|
1203
1224
|
const target = targetFlag ? targetFlag.split("=")[1] : "CLAUDE.md";
|
|
1204
1225
|
const specPath = `${target}.spec.ts`;
|
|
1205
|
-
|
|
1226
|
+
const specAbs = (0, node_path_1.resolve)(process.cwd(), specPath);
|
|
1227
|
+
if ((0, node_fs_1.existsSync)(specAbs)) {
|
|
1206
1228
|
console.log(`${specPath} already exists.`);
|
|
1207
1229
|
return;
|
|
1208
1230
|
}
|
|
1231
|
+
// Auto-adopt: when the target file already exists with hand-written content
|
|
1232
|
+
// (no integrity header), faithfully convert it into a spec instead of
|
|
1233
|
+
// scaffolding a blank one — so `init` leaves you with a spec, not homework, and
|
|
1234
|
+
// `require-instructions-spec` is satisfied by construction. The compile that
|
|
1235
|
+
// follows reproduces the file (+ the header); review the diff. `vigiles eject`
|
|
1236
|
+
// reverses it. See research/install-enforcement-dx.md.
|
|
1237
|
+
const targetAbs = (0, node_path_1.resolve)(process.cwd(), target);
|
|
1238
|
+
if ((0, node_fs_1.existsSync)(targetAbs) && !targetHasHash(targetAbs)) {
|
|
1239
|
+
const md = (0, node_fs_1.readFileSync)(targetAbs, "utf-8");
|
|
1240
|
+
const { source, tier, sectionCount } = (0, adopt_js_1.adoptMarkdown)(md, (0, node_path_1.basename)(target));
|
|
1241
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(specAbs), { recursive: true });
|
|
1242
|
+
(0, node_fs_1.writeFileSync)(specAbs, source);
|
|
1243
|
+
console.log(`Adopted ${target} → ${specPath} (${tier}, ${String(sectionCount)} section${sectionCount === 1 ? "" : "s"}). ` +
|
|
1244
|
+
`Run \`vigiles compile\` and review the diff; the \`/strengthen\` skill upgrades prose to verified rules.`);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1209
1247
|
// The compiled output is derived from the spec FILE path; the spec's `target`
|
|
1210
1248
|
// field is the h1 + the name the compiler validates against, so it must be the
|
|
1211
1249
|
// bare filename even when the spec lives in a subdir (e.g. a sync tool's
|
|
@@ -1252,11 +1290,125 @@ export default claude({${targetLine}
|
|
|
1252
1290
|
},
|
|
1253
1291
|
});
|
|
1254
1292
|
`;
|
|
1255
|
-
const specAbs = (0, node_path_1.resolve)(process.cwd(), specPath);
|
|
1256
1293
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(specAbs), { recursive: true });
|
|
1257
1294
|
(0, node_fs_1.writeFileSync)(specAbs, template);
|
|
1258
1295
|
console.log(`Created ${specPath} — edit it and run \`vigiles compile\`.`);
|
|
1259
1296
|
}
|
|
1297
|
+
/**
|
|
1298
|
+
* `vigiles eject [file]` — the inverse of `compile`: hand a compiled instruction
|
|
1299
|
+
* file back to the user as plain, hand-owned markdown. Strips the `vigiles:sha256`
|
|
1300
|
+
* integrity header, adds a `require-instructions-spec` disable marker so `lint` stays quiet,
|
|
1301
|
+
* and removes the spec that managed it (`--keep-spec` to leave it). The
|
|
1302
|
+
* "managed-but-ejectable" escape hatch: adopting a typed spec is never a one-way
|
|
1303
|
+
* door.
|
|
1304
|
+
*/
|
|
1305
|
+
function eject(args) {
|
|
1306
|
+
const keepSpec = args.includes("--keep-spec");
|
|
1307
|
+
const file = args.find((a) => !a.startsWith("-")) ?? "CLAUDE.md";
|
|
1308
|
+
const abs = (0, node_path_1.resolve)(process.cwd(), file);
|
|
1309
|
+
if (!(0, node_fs_1.existsSync)(abs)) {
|
|
1310
|
+
console.error(`✗ ${file}: no such file.`);
|
|
1311
|
+
process.exitCode = 1;
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const ejected = (0, integrity_js_1.ejectMarkdown)((0, node_fs_1.readFileSync)(abs, "utf-8"));
|
|
1315
|
+
if (!ejected) {
|
|
1316
|
+
console.log(`${file} is not vigiles-managed (no integrity header) — nothing to eject.`);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
(0, node_fs_1.writeFileSync)(abs, ejected.markdown);
|
|
1320
|
+
console.log(`✓ Ejected ${file} — it's now plain, hand-owned markdown.`);
|
|
1321
|
+
const specAbs = (0, node_path_1.resolve)(process.cwd(), ejected.specFile);
|
|
1322
|
+
// SAFETY: the `compiled from <path>` header is untrusted text — a hand-edited /
|
|
1323
|
+
// forged header could name `package.json` or `../../secret`, and a blind rmSync
|
|
1324
|
+
// would delete it. Only ever remove a `.spec.ts` that resolves INSIDE the
|
|
1325
|
+
// project (no `..` escape).
|
|
1326
|
+
const relSpec = (0, node_path_1.relative)((0, node_path_1.resolve)(process.cwd()), specAbs);
|
|
1327
|
+
const isSafeSpecTarget = ejected.specFile.endsWith(".spec.ts") &&
|
|
1328
|
+
relSpec !== "" &&
|
|
1329
|
+
!relSpec.startsWith("..");
|
|
1330
|
+
if ((0, node_fs_1.existsSync)(specAbs)) {
|
|
1331
|
+
if (!isSafeSpecTarget) {
|
|
1332
|
+
console.log(` ⚠ Kept ${ejected.specFile} — the integrity header names a path that isn't a .spec.ts inside this project (it may have been hand-edited); refusing to delete it. Remove it yourself if that's intended.`);
|
|
1333
|
+
}
|
|
1334
|
+
else if (keepSpec) {
|
|
1335
|
+
console.log(` Kept ${ejected.specFile} (--keep-spec) — but \`vigiles compile\` would re-manage ${file}.`);
|
|
1336
|
+
}
|
|
1337
|
+
else if (specReferencedElsewhere(ejected.specFile, file)) {
|
|
1338
|
+
// A multi-target spec (e.g. `target: ["CLAUDE.md", "docs/AGENTS.md"]`, or a
|
|
1339
|
+
// mirror) compiles to several files — possibly in other directories — that
|
|
1340
|
+
// all name the SAME source in their header. Deleting it while ANY of them is
|
|
1341
|
+
// still managed would orphan that file. So keep the spec until its last
|
|
1342
|
+
// consumer is ejected.
|
|
1343
|
+
console.log(` Kept ${ejected.specFile} — another compiled file in this project is still managed by it (a multi-target / mirrored spec); deleting it would orphan that file. Eject the others too, or remove the spec by hand once it's unused.`);
|
|
1344
|
+
}
|
|
1345
|
+
else {
|
|
1346
|
+
(0, node_fs_1.rmSync)(specAbs);
|
|
1347
|
+
console.log(` Removed ${ejected.specFile} (the spec that managed it).`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
// The disable marker is only added to instruction files (not skills/agents),
|
|
1351
|
+
// so only mention it when it was actually written.
|
|
1352
|
+
if (ejected.markdown.startsWith(integrity_js_1.REQUIRE_INSTRUCTIONS_SPEC_DISABLE)) {
|
|
1353
|
+
console.log(` Left a \`${integrity_js_1.REQUIRE_INSTRUCTIONS_SPEC_DISABLE}\` marker so \`vigiles lint\` won't ask for a spec; delete it if you remove vigiles entirely.`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Whether another compiled markdown file ANYWHERE under the project still carries
|
|
1358
|
+
* an integrity header naming `specFile` — i.e. the spec has OTHER compiled outputs
|
|
1359
|
+
* (a multi-target `target: [...]` spec or a CLAUDE.md⇄AGENTS.md mirror, possibly
|
|
1360
|
+
* in a different directory like `docs/AGENTS.md`), so removing it would orphan
|
|
1361
|
+
* them. Walks the project tree from cwd, skipping heavy/irrelevant dirs; the
|
|
1362
|
+
* ejected file itself is skipped (its header was already stripped). Matches on the
|
|
1363
|
+
* RESOLVED spec path (not basename), so two unrelated specs that happen to share a
|
|
1364
|
+
* filename — `src/CLAUDE.md.spec.ts` vs `CLAUDE.md.spec.ts`, common in a monorepo —
|
|
1365
|
+
* don't collide; genuine sibling outputs of one compile carry the identical
|
|
1366
|
+
* recorded spec path. Best-effort: an unreadable file is ignored.
|
|
1367
|
+
*/
|
|
1368
|
+
function specReferencedElsewhere(specFile, ejectedFile) {
|
|
1369
|
+
const root = (0, node_path_1.resolve)(process.cwd());
|
|
1370
|
+
const ejectedAbs = (0, node_path_1.resolve)(root, ejectedFile);
|
|
1371
|
+
const specAbs = (0, node_path_1.resolve)(root, specFile);
|
|
1372
|
+
const SKIP = new Set([
|
|
1373
|
+
"node_modules",
|
|
1374
|
+
".git",
|
|
1375
|
+
"dist",
|
|
1376
|
+
"build",
|
|
1377
|
+
"coverage",
|
|
1378
|
+
".next",
|
|
1379
|
+
"out",
|
|
1380
|
+
]);
|
|
1381
|
+
const stack = [root];
|
|
1382
|
+
while (stack.length > 0) {
|
|
1383
|
+
const dir = stack.pop();
|
|
1384
|
+
let entries;
|
|
1385
|
+
try {
|
|
1386
|
+
entries = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
1387
|
+
}
|
|
1388
|
+
catch {
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
for (const e of entries) {
|
|
1392
|
+
const p = (0, node_path_1.resolve)(dir, e.name);
|
|
1393
|
+
if (e.isDirectory()) {
|
|
1394
|
+
if (!SKIP.has(e.name))
|
|
1395
|
+
stack.push(p);
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
if (!e.name.endsWith(".md") || p === ejectedAbs)
|
|
1399
|
+
continue;
|
|
1400
|
+
try {
|
|
1401
|
+
const header = (0, integrity_js_1.parseIntegrityHeader)((0, node_fs_1.readFileSync)(p, "utf-8"));
|
|
1402
|
+
if (header && (0, node_path_1.resolve)(root, header.specFile) === specAbs)
|
|
1403
|
+
return true;
|
|
1404
|
+
}
|
|
1405
|
+
catch {
|
|
1406
|
+
/* unreadable file — skip */
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
return false;
|
|
1411
|
+
}
|
|
1260
1412
|
// ---------------------------------------------------------------------------
|
|
1261
1413
|
// Setup wizard
|
|
1262
1414
|
// ---------------------------------------------------------------------------
|
|
@@ -1315,7 +1467,7 @@ function workflowUsesStaleApi(content) {
|
|
|
1315
1467
|
return false; // uses the Action — fine
|
|
1316
1468
|
if (!/\bvigiles\b/.test(content))
|
|
1317
1469
|
return false; // not a vigiles workflow
|
|
1318
|
-
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);
|
|
1319
1471
|
return !hasModernCmd;
|
|
1320
1472
|
}
|
|
1321
1473
|
/**
|
|
@@ -1326,7 +1478,7 @@ function workflowUsesStaleApi(content) {
|
|
|
1326
1478
|
* the bare-API heuristic above, which an Action reference short-circuits.
|
|
1327
1479
|
*/
|
|
1328
1480
|
const REMOVED_SUBCOMMANDS = {
|
|
1329
|
-
|
|
1481
|
+
scan: "audit", // renamed: the Lighthouse report verb is `audit`
|
|
1330
1482
|
};
|
|
1331
1483
|
/** The first removed/renamed `vigiles <sub>` a workflow still calls, if any. */
|
|
1332
1484
|
function workflowRemovedSubcommand(content) {
|
|
@@ -1336,7 +1488,7 @@ function workflowRemovedSubcommand(content) {
|
|
|
1336
1488
|
}
|
|
1337
1489
|
return null;
|
|
1338
1490
|
}
|
|
1339
|
-
/** Rewrite removed/renamed `vigiles <sub>` invocations in place (
|
|
1491
|
+
/** Rewrite removed/renamed `vigiles <sub>` invocations in place (scan → audit).
|
|
1340
1492
|
* Surgical — preserves the rest of the user's workflow. */
|
|
1341
1493
|
function rewriteRemovedSubcommands(content) {
|
|
1342
1494
|
let out = content;
|
|
@@ -1436,7 +1588,8 @@ function scaffoldPillar2() {
|
|
|
1436
1588
|
console.log("✓ Scaffolded vigiles.harness.mjs — Pillar 2 starter (npx vigiles test)");
|
|
1437
1589
|
return ["vigiles.harness.mjs"];
|
|
1438
1590
|
}
|
|
1439
|
-
/** Interactive prompts (TTY only):
|
|
1591
|
+
/** Interactive prompts (TTY only): the readline IO shell over the pure
|
|
1592
|
+
* `collectSetupAnswers` (the Q&A logic is unit-tested in setup-plan.test.ts). */
|
|
1440
1593
|
async function promptSetup() {
|
|
1441
1594
|
const readline = await import("node:readline");
|
|
1442
1595
|
const rl = readline.createInterface({
|
|
@@ -1448,17 +1601,8 @@ async function promptSetup() {
|
|
|
1448
1601
|
res(a.trim() || def);
|
|
1449
1602
|
});
|
|
1450
1603
|
});
|
|
1451
|
-
const isYes = (s) => /^y(es)?$/i.test(s);
|
|
1452
1604
|
try {
|
|
1453
|
-
|
|
1454
|
-
const gha = isYes(await ask("Wire CI (GitHub Action)? [Y/n]: ", "y"));
|
|
1455
|
-
const plugin = isYes(await ask("Install the Claude Code plugin (hooks + skills)? [Y/n]: ", "y"));
|
|
1456
|
-
return {
|
|
1457
|
-
lint: pillars !== "test",
|
|
1458
|
-
test: pillars !== "lint" && pillars !== "verify",
|
|
1459
|
-
gha,
|
|
1460
|
-
plugin,
|
|
1461
|
-
};
|
|
1605
|
+
return await (0, setup_plan_js_1.collectSetupAnswers)(ask);
|
|
1462
1606
|
}
|
|
1463
1607
|
finally {
|
|
1464
1608
|
rl.close();
|
|
@@ -1620,28 +1764,31 @@ function redirectSyncToolTargets(cwd, targets) {
|
|
|
1620
1764
|
async function setupPillar1(detected, targetValue, harnesses) {
|
|
1621
1765
|
const cwd = process.cwd();
|
|
1622
1766
|
const written = [];
|
|
1623
|
-
const
|
|
1767
|
+
const adopted = [];
|
|
1624
1768
|
// An explicit --target is honoured as-is; otherwise collapse a CLAUDE.md⇄
|
|
1625
1769
|
// AGENTS.md mirror (symlink or synced) to one canonical spec, then redirect
|
|
1626
1770
|
// into a sync tool's source slot when one would own the output.
|
|
1627
1771
|
const targets = targetValue
|
|
1628
1772
|
? determineTargets(detected, targetValue, harnesses)
|
|
1629
1773
|
: redirectSyncToolTargets(cwd, collapseMirroredTargets(determineTargets(detected, targetValue, harnesses), (0, compose_js_1.detectInstructionMirror)(cwd)));
|
|
1630
|
-
// Create specs
|
|
1631
|
-
//
|
|
1774
|
+
// Create specs. An existing hand-written target is faithfully ADOPTED into a
|
|
1775
|
+
// spec (scaffoldSpec() does the convert), not clobbered with a blank one — so the
|
|
1776
|
+
// compile below reproduces it (the user reviews the diff). A greenfield target
|
|
1777
|
+
// gets a blank starter spec.
|
|
1632
1778
|
for (const target of targets) {
|
|
1633
1779
|
const specPath = `${target}.spec.ts`;
|
|
1634
|
-
const
|
|
1780
|
+
const targetAbs = (0, node_path_1.resolve)(cwd, target);
|
|
1781
|
+
const willAdopt = (0, node_fs_1.existsSync)(targetAbs) &&
|
|
1782
|
+
!targetHasHash(targetAbs) &&
|
|
1783
|
+
!(0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, specPath));
|
|
1635
1784
|
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, specPath))) {
|
|
1636
1785
|
console.log(`✓ ${specPath} already exists`);
|
|
1637
1786
|
}
|
|
1638
1787
|
else {
|
|
1639
|
-
|
|
1788
|
+
scaffoldSpec(["--target=" + target]); // adopts existing content, else blank scaffold
|
|
1640
1789
|
written.push(specPath);
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
needsMigration.push(target);
|
|
1644
|
-
console.log(` ${target} already has content — adopt it into a spec with the adopt-spec skill, then \`vigiles compile\`.`);
|
|
1790
|
+
if (willAdopt)
|
|
1791
|
+
adopted.push(target);
|
|
1645
1792
|
}
|
|
1646
1793
|
}
|
|
1647
1794
|
// Generate types + schema.
|
|
@@ -1666,17 +1813,59 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
1666
1813
|
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(cwd, ".vigiles/schema.json"), schemaResult.json);
|
|
1667
1814
|
console.log("✓ Generated .vigiles/schema.json (YAML-LSP frontmatter schema)");
|
|
1668
1815
|
written.push(".vigiles/schema.json");
|
|
1669
|
-
// Compile — but
|
|
1670
|
-
//
|
|
1671
|
-
//
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1816
|
+
// Compile — but NEVER overwrite an existing hand-written file during `init`:
|
|
1817
|
+
// we compile only GREENFIELD targets (the file doesn't exist yet) and targets
|
|
1818
|
+
// we already manage (carry our hash). An ADOPTED file is left untouched — the
|
|
1819
|
+
// user reviews the generated spec and runs `vigiles compile` themselves to
|
|
1820
|
+
// switch it to spec-managed (non-destructive by default; the compile is
|
|
1821
|
+
// byte-faithful, but it's the user's call to make, with a diff to review).
|
|
1822
|
+
// And we only compile when `vigiles` actually resolves — a fresh repo hasn't
|
|
1823
|
+
// run `npm install` yet, so compiling would just error; defer it with a clear
|
|
1824
|
+
// next step instead of a scary stack-traceless "failed to load".
|
|
1825
|
+
const canCompile = canResolveVigiles(cwd);
|
|
1826
|
+
const specs = canCompile
|
|
1827
|
+
? findSpecs().filter((s) => {
|
|
1828
|
+
const tf = (0, node_path_1.resolve)(cwd, s.replace(/\.spec\.ts$/, ""));
|
|
1829
|
+
return !(0, node_fs_1.existsSync)(tf) || targetHasHash(tf);
|
|
1830
|
+
})
|
|
1831
|
+
: [];
|
|
1832
|
+
if (specs.length > 0) {
|
|
1833
|
+
console.log("\nCompiling specs...");
|
|
1678
1834
|
await compile(specs, (0, validate_js_1.loadConfig)());
|
|
1679
|
-
|
|
1835
|
+
}
|
|
1836
|
+
else if (!canCompile) {
|
|
1837
|
+
// Honest, project-type-aware guidance. A JS repo just needs `npm install`
|
|
1838
|
+
// (init already added the devDep). A repo with NO package.json (Python, Rust,
|
|
1839
|
+
// …) can't resolve the npm package at all, so point at the no-install paths
|
|
1840
|
+
// instead of a misleading `npm install`.
|
|
1841
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, "package.json"))) {
|
|
1842
|
+
console.log("\n Skipping compile — run `npm install` (to fetch the vigiles dep just added), then `npx vigiles compile`.");
|
|
1843
|
+
}
|
|
1844
|
+
else {
|
|
1845
|
+
console.log("\n No package.json here, so the typed-spec compile isn't available yet.\n" +
|
|
1846
|
+
" • `npx vigiles lint` verifies your instruction files right now — no install needed.\n" +
|
|
1847
|
+
" • To spec-manage them, add a package.json first: `npm init -y && npm i -D vigiles`, then `npx vigiles compile`.");
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
return { specTargets: targets, written, adopted };
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* Whether `vigiles/spec` will resolve for a spec compiled from `cwd` — true when
|
|
1854
|
+
* vigiles is installed locally (`node_modules/vigiles`) or `cwd` IS the vigiles
|
|
1855
|
+
* package itself (the in-repo dogfood / a monorepo workspace). A fresh user repo
|
|
1856
|
+
* that hasn't run `npm install` yet returns false, so `init` defers the compile
|
|
1857
|
+
* instead of emitting a resolution error.
|
|
1858
|
+
*/
|
|
1859
|
+
function canResolveVigiles(cwd) {
|
|
1860
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, "node_modules", "vigiles")))
|
|
1861
|
+
return true;
|
|
1862
|
+
try {
|
|
1863
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(cwd, "package.json"), "utf-8"));
|
|
1864
|
+
return pkg.name === "vigiles";
|
|
1865
|
+
}
|
|
1866
|
+
catch {
|
|
1867
|
+
return false;
|
|
1868
|
+
}
|
|
1680
1869
|
}
|
|
1681
1870
|
/** Whether a harness binary (`claude`, `codex`) is on PATH. */
|
|
1682
1871
|
function harnessBinaryPresent(bin) {
|
|
@@ -1828,24 +2017,37 @@ function printDetection(detected, harnesses) {
|
|
|
1828
2017
|
/** Print the closing next-steps list + an honest commit hint (only files
|
|
1829
2018
|
* actually written this run). */
|
|
1830
2019
|
function printSetupSummary(opts) {
|
|
1831
|
-
const { plan, strict, targets,
|
|
2020
|
+
const { plan, strict, targets, adopted, written } = opts;
|
|
1832
2021
|
const specPathsList = targets.map((t) => `${t}.spec.ts`);
|
|
2022
|
+
// A repo with no package.json (Python/Rust/…) can't resolve the npm package,
|
|
2023
|
+
// so the typed-spec compile path needs an install first — give honest steps.
|
|
2024
|
+
const hasPkg = (0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), "package.json"));
|
|
1833
2025
|
console.log("\n---\nSetup complete.\n");
|
|
2026
|
+
// Next steps in DEPENDENCY order: install the dep first, then compile (which
|
|
2027
|
+
// needs it), then the optional hardening / test / CI steps.
|
|
1834
2028
|
const nextSteps = [];
|
|
1835
|
-
if (
|
|
1836
|
-
nextSteps.push(
|
|
2029
|
+
if (written.includes("package.json")) {
|
|
2030
|
+
nextSteps.push("Run `npm install` to fetch the vigiles dev dependency");
|
|
2031
|
+
}
|
|
2032
|
+
if (adopted.length > 0 && !hasPkg) {
|
|
2033
|
+
// Non-JS repo: compile needs a local install. Point at the no-install verify
|
|
2034
|
+
// path + how to enable specs, instead of a compile that would fail.
|
|
2035
|
+
nextSteps.push(`Verify now with \`npx vigiles lint\` (no install). To spec-manage ${adopted.join(", ")}, add a package.json first (\`npm init -y && npm i -D vigiles\`), then \`npx vigiles compile\` and review the diff`);
|
|
2036
|
+
}
|
|
2037
|
+
else if (adopted.length > 0) {
|
|
2038
|
+
// Adoption is NON-DESTRUCTIVE: the file is untouched until you compile, so
|
|
2039
|
+
// the diff to review is what compile WOULD produce (byte-faithful).
|
|
2040
|
+
nextSteps.push(`Run \`npx vigiles compile\` to put ${adopted.join(", ")} under spec management — it reproduces the file + adds an integrity header, so review the diff (\`vigiles eject\` reverses it)`);
|
|
2041
|
+
nextSteps.push("Run the `/strengthen` skill to upgrade prose rules to verified enforce()/guard()");
|
|
1837
2042
|
}
|
|
1838
2043
|
else if (specPathsList.length > 0) {
|
|
1839
|
-
nextSteps.push(`Edit ${specPathsList.join(", ")} — add your conventions, then \`/strengthen
|
|
2044
|
+
nextSteps.push(`Edit ${specPathsList.join(", ")} — add your conventions, then \`npx vigiles compile\` (and \`/strengthen\`)`);
|
|
1840
2045
|
}
|
|
1841
2046
|
if (plan.test) {
|
|
1842
2047
|
nextSteps.push("Edit vigiles.harness.mjs to test a real hook, then `npx vigiles test`");
|
|
1843
2048
|
}
|
|
1844
|
-
if (written.includes("package.json")) {
|
|
1845
|
-
nextSteps.push("Run `npm install` to fetch the vigiles dev dependency");
|
|
1846
|
-
}
|
|
1847
2049
|
if (!strict) {
|
|
1848
|
-
nextSteps.push("When ready, enforce in CI: npx vigiles init --strict");
|
|
2050
|
+
nextSteps.push("When ready, enforce specs + tests in CI: `npx vigiles init --strict`");
|
|
1849
2051
|
}
|
|
1850
2052
|
nextSteps.forEach((s, i) => {
|
|
1851
2053
|
console.log(` ${String(i + 1)}. ${s}`);
|
|
@@ -1858,12 +2060,15 @@ function printSetupSummary(opts) {
|
|
|
1858
2060
|
}
|
|
1859
2061
|
async function setup(args) {
|
|
1860
2062
|
const parsed = (0, setup_plan_js_1.parseSetupArgs)(args);
|
|
1861
|
-
const strict = parsed.strict;
|
|
1862
2063
|
// Plan: defaults → flags → interactive prompts (only a human at a TTY).
|
|
1863
2064
|
let plan = (0, setup_plan_js_1.resolvePlan)(parsed);
|
|
1864
2065
|
if ((0, setup_plan_js_1.shouldPrompt)(parsed, process.stdin.isTTY ?? false)) {
|
|
1865
2066
|
plan = (0, setup_plan_js_1.resolvePlan)(parsed, await promptSetup());
|
|
1866
2067
|
}
|
|
2068
|
+
// Read strict from the RESOLVED plan, not the raw flag — an interactive "yes"
|
|
2069
|
+
// to the workflow tier (no `--strict` flag) sets plan.strict, and the config
|
|
2070
|
+
// write + summary must honor it.
|
|
2071
|
+
const strict = plan.strict;
|
|
1867
2072
|
const pillars = [plan.lint && "lint", plan.test && "test"]
|
|
1868
2073
|
.filter(Boolean)
|
|
1869
2074
|
.join(" + ");
|
|
@@ -1876,12 +2081,12 @@ async function setup(args) {
|
|
|
1876
2081
|
const written = [];
|
|
1877
2082
|
// Lint pillar — verify instruction-file references.
|
|
1878
2083
|
let targets = [];
|
|
1879
|
-
let
|
|
2084
|
+
let adopted = [];
|
|
1880
2085
|
if (plan.lint) {
|
|
1881
2086
|
console.log("");
|
|
1882
2087
|
const p1 = await setupPillar1(detected, parsed.target, harnesses);
|
|
1883
2088
|
targets = p1.specTargets;
|
|
1884
|
-
|
|
2089
|
+
adopted = p1.adopted;
|
|
1885
2090
|
written.push(...p1.written);
|
|
1886
2091
|
}
|
|
1887
2092
|
// Test pillar — test the harness.
|
|
@@ -1912,9 +2117,16 @@ async function setup(args) {
|
|
|
1912
2117
|
console.log(" npm install -D rule-porter");
|
|
1913
2118
|
}
|
|
1914
2119
|
// Project config — record the harness(es) so compile/lint select the dialect
|
|
1915
|
-
// deterministically (no cwd sniffing), plus strict rule severities on --strict
|
|
1916
|
-
|
|
1917
|
-
|
|
2120
|
+
// deterministically (no cwd sniffing), plus strict rule severities on --strict
|
|
2121
|
+
// (or all-warn under --report-only).
|
|
2122
|
+
writeProjectConfig({
|
|
2123
|
+
harnesses,
|
|
2124
|
+
strict,
|
|
2125
|
+
reportOnly: parsed.reportOnly,
|
|
2126
|
+
lint: plan.lint,
|
|
2127
|
+
written,
|
|
2128
|
+
});
|
|
2129
|
+
printSetupSummary({ plan, strict, targets, adopted, written });
|
|
1918
2130
|
}
|
|
1919
2131
|
/** Canonical, de-duplicated harness list → a config value (string when one). */
|
|
1920
2132
|
function harnessConfigValue(harnesses) {
|
|
@@ -1941,6 +2153,8 @@ function writeProjectConfig(opts) {
|
|
|
1941
2153
|
const merged = (0, setup_plan_js_1.mergeProjectConfig)(existing, {
|
|
1942
2154
|
harness: harnessConfigValue(opts.harnesses),
|
|
1943
2155
|
strict: opts.strict,
|
|
2156
|
+
reportOnly: opts.reportOnly,
|
|
2157
|
+
lint: opts.lint,
|
|
1944
2158
|
});
|
|
1945
2159
|
if (!merged)
|
|
1946
2160
|
return;
|
|
@@ -2172,6 +2386,34 @@ function checkSkillFrontmatter(config, silent, adapter) {
|
|
|
2172
2386
|
}
|
|
2173
2387
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2174
2388
|
}
|
|
2389
|
+
/**
|
|
2390
|
+
* Apply the `prefer-compiled-hooks` rule: a SINGLE repo-level recommendation
|
|
2391
|
+
* (one finding regardless of hook count) nudging hand-written hooks toward
|
|
2392
|
+
* compiled `vigiles/hook` artifacts. A discovery nudge, not a defect — the shell
|
|
2393
|
+
* lane stays first-class — so it fires once and the message links the guide.
|
|
2394
|
+
* Reuses `scanPlugin`'s `manualHookCount` (one-detector-no-drift).
|
|
2395
|
+
*/
|
|
2396
|
+
function checkPreferCompiledHooks(config, silent, adapter) {
|
|
2397
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["prefer-compiled-hooks"]);
|
|
2398
|
+
if (!sev)
|
|
2399
|
+
return { issues: 0, errors: 0 };
|
|
2400
|
+
let count;
|
|
2401
|
+
try {
|
|
2402
|
+
count = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).manualHookCount;
|
|
2403
|
+
}
|
|
2404
|
+
catch {
|
|
2405
|
+
return { issues: 0, errors: 0 };
|
|
2406
|
+
}
|
|
2407
|
+
if (count === 0)
|
|
2408
|
+
return { issues: 0, errors: 0 };
|
|
2409
|
+
const message = (0, scan_js_1.preferCompiledHooksMessage)(count);
|
|
2410
|
+
if (!silent) {
|
|
2411
|
+
console.log("\nCompiled-hooks check:\n");
|
|
2412
|
+
console.log(` ${sev === "error" ? "✗" : "ℹ"} ${message}`);
|
|
2413
|
+
ghAnnotate(sev === "error" ? "error" : "warning", message);
|
|
2414
|
+
}
|
|
2415
|
+
return { issues: 1, errors: sev === "error" ? 1 : 0 };
|
|
2416
|
+
}
|
|
2175
2417
|
/**
|
|
2176
2418
|
* Apply the `mcp-config` rule: a declared MCP server with neither a `command`
|
|
2177
2419
|
* (stdio) nor a `url` (http/sse) can't start. Reuses `scanPlugin`'s `mcpIssues`.
|
|
@@ -2449,34 +2691,45 @@ async function countGuidanceRules(silent = false) {
|
|
|
2449
2691
|
// Command handlers for main()
|
|
2450
2692
|
// ---------------------------------------------------------------------------
|
|
2451
2693
|
function findInstructionFiles(restArgs, exclude = []) {
|
|
2452
|
-
if (restArgs.length > 0)
|
|
2453
|
-
return restArgs;
|
|
2454
2694
|
const patterns = ["**/CLAUDE.md", "**/AGENTS.md", "**/SKILL.md"];
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2695
|
+
// `exclude` (from .vigilesrc.json) drops vendored/benchmark fixtures the repo's
|
|
2696
|
+
// own lint shouldn't police — a third-party CLAUDE.md isn't held to require-instructions-spec.
|
|
2697
|
+
// node_modules/dist/.git stay always-excluded.
|
|
2698
|
+
const ignore = [...IGNORE_NODE_MODULES, "dist/**", ".git/**", ...exclude];
|
|
2699
|
+
// Discover instruction files under one directory, as paths relative to cwd.
|
|
2700
|
+
const discoverIn = (dirAbs) => patterns
|
|
2701
|
+
.flatMap((p) => (0, glob_1.globSync)(p, { ignore, cwd: dirAbs, absolute: true }))
|
|
2702
|
+
.map((abs) => (0, node_path_1.relative)(process.cwd(), abs));
|
|
2703
|
+
if (restArgs.length === 0)
|
|
2704
|
+
return discoverIn(process.cwd());
|
|
2705
|
+
// Explicit args: expand a DIRECTORY to the instruction files inside it (so
|
|
2706
|
+
// `vigiles lint .` works), keep a file arg as-is, and pass a non-existent arg
|
|
2707
|
+
// through unchanged (lint reports it as not-found rather than crashing).
|
|
2708
|
+
const out = [];
|
|
2709
|
+
for (const arg of restArgs) {
|
|
2710
|
+
const abs = (0, node_path_1.resolve)(process.cwd(), arg);
|
|
2711
|
+
if ((0, node_fs_1.existsSync)(abs) && (0, node_fs_1.lstatSync)(abs).isDirectory()) {
|
|
2712
|
+
out.push(...discoverIn(abs));
|
|
2713
|
+
}
|
|
2714
|
+
else {
|
|
2715
|
+
out.push(arg);
|
|
2716
|
+
}
|
|
2464
2717
|
}
|
|
2465
|
-
return
|
|
2718
|
+
return out;
|
|
2466
2719
|
}
|
|
2467
2720
|
/** Value of a `--flag=value` arg (the `=` form, so it never collides with a positional). */
|
|
2468
2721
|
function flagValue(args, name) {
|
|
2469
2722
|
return args.find((a) => a.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
2470
2723
|
}
|
|
2471
2724
|
/**
|
|
2472
|
-
*
|
|
2473
|
-
*
|
|
2474
|
-
* sets (`--prompts`) and reports BOTH behavioral columns: trigger-rate (does
|
|
2475
|
-
* skill actually FIRE — recall + precision) and the selection-collision matrix
|
|
2476
|
-
* 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
|
|
2477
2730
|
* deterministic `description-overlap` rule). Needs the harness CLI + model auth;
|
|
2478
2731
|
* degrades honestly ("unavailable") when absent. The OSS-testing front door:
|
|
2479
|
-
* `vigiles
|
|
2732
|
+
* `vigiles audit ./plugin --prompts=p.json` (interactive — say yes when asked).
|
|
2480
2733
|
*/
|
|
2481
2734
|
async function handleMeasure(restArgs, args) {
|
|
2482
2735
|
const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
|
|
@@ -2529,6 +2782,31 @@ async function handleMeasure(restArgs, args) {
|
|
|
2529
2782
|
console.log(`\n${(0, scan_behavioral_js_1.formatBehavioralReport)(trigger)}`);
|
|
2530
2783
|
console.log(`\n${(0, scan_behavioral_js_1.formatSelectionReport)(collisions)}`);
|
|
2531
2784
|
}
|
|
2785
|
+
/**
|
|
2786
|
+
* `vigiles generate <kind>` — one verb over the three dev-toolchain generators
|
|
2787
|
+
* (types/schema/harness). Each emits a file YOUR editor/tsc reads, not the agent
|
|
2788
|
+
* — grouped under one verb instead of N hyphenated siblings (cohesive-cli-surface,
|
|
2789
|
+
* high-bar-for-new-commands). The kind is the first positional; the rest passes
|
|
2790
|
+
* through to the per-kind handler (out path / dir).
|
|
2791
|
+
*/
|
|
2792
|
+
async function handleGenerate(restArgs, args) {
|
|
2793
|
+
const kind = restArgs[0];
|
|
2794
|
+
const rest = restArgs.slice(1);
|
|
2795
|
+
switch (kind) {
|
|
2796
|
+
case "types":
|
|
2797
|
+
handleGenerateTypes(args, rest);
|
|
2798
|
+
break;
|
|
2799
|
+
case "schema":
|
|
2800
|
+
handleGenerateSchema(args, rest);
|
|
2801
|
+
break;
|
|
2802
|
+
case "harness":
|
|
2803
|
+
await handleGenerateHarness(args, rest);
|
|
2804
|
+
break;
|
|
2805
|
+
default:
|
|
2806
|
+
console.error("Usage: vigiles generate <types|schema|harness> [out] [--check]");
|
|
2807
|
+
process.exit(2);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2532
2810
|
function handleGenerateTypes(args, restArgs) {
|
|
2533
2811
|
const checkOnly = args.includes("--check");
|
|
2534
2812
|
const outPath = restArgs[0] ?? ".vigiles/generated.d.ts";
|
|
@@ -2552,7 +2830,7 @@ function handleGenerateTypes(args, restArgs) {
|
|
|
2552
2830
|
if (checkOnly) {
|
|
2553
2831
|
// --check: compare against existing file, exit 1 if stale
|
|
2554
2832
|
if (!(0, node_fs_1.existsSync)(fullOut)) {
|
|
2555
|
-
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate
|
|
2833
|
+
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate types\` to create it.`);
|
|
2556
2834
|
process.exit(1);
|
|
2557
2835
|
}
|
|
2558
2836
|
const existing = (0, node_fs_1.readFileSync)(fullOut, "utf-8");
|
|
@@ -2567,7 +2845,7 @@ function handleGenerateTypes(args, restArgs) {
|
|
|
2567
2845
|
console.log(`\n✓ ${outPath} is up to date`);
|
|
2568
2846
|
}
|
|
2569
2847
|
else {
|
|
2570
|
-
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate
|
|
2848
|
+
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate types\` to update.`);
|
|
2571
2849
|
process.exit(1);
|
|
2572
2850
|
}
|
|
2573
2851
|
return;
|
|
@@ -2594,7 +2872,7 @@ function handleGenerateSchema(args, restArgs) {
|
|
|
2594
2872
|
const fullOut = (0, node_path_1.resolve)(process.cwd(), outPath);
|
|
2595
2873
|
if (checkOnly) {
|
|
2596
2874
|
if (!(0, node_fs_1.existsSync)(fullOut)) {
|
|
2597
|
-
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate
|
|
2875
|
+
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate schema\` to create it.`);
|
|
2598
2876
|
process.exit(1);
|
|
2599
2877
|
}
|
|
2600
2878
|
const existing = (0, node_fs_1.readFileSync)(fullOut, "utf-8");
|
|
@@ -2602,7 +2880,7 @@ function handleGenerateSchema(args, restArgs) {
|
|
|
2602
2880
|
console.log(`\n✓ ${outPath} is up to date`);
|
|
2603
2881
|
}
|
|
2604
2882
|
else {
|
|
2605
|
-
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate
|
|
2883
|
+
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate schema\` to update.`);
|
|
2606
2884
|
process.exit(1);
|
|
2607
2885
|
}
|
|
2608
2886
|
return;
|
|
@@ -2617,13 +2895,46 @@ function handleGenerateSchema(args, restArgs) {
|
|
|
2617
2895
|
` # yaml-language-server: $schema=./${outPath}`);
|
|
2618
2896
|
}
|
|
2619
2897
|
/**
|
|
2620
|
-
* `vigiles generate
|
|
2898
|
+
* `vigiles generate harness [dir] [out]` — emit one typed registry over every
|
|
2621
2899
|
* `*.spec.ts` under `dir`, so a single `tsc --noEmit` cross-checks the whole
|
|
2622
2900
|
* harness (dangling delegates → a tsc error; duplicate names → this command
|
|
2623
2901
|
* exits non-zero; the capability lattice → a computed export). The third
|
|
2624
2902
|
* generated artifact beside `generate-types` / `generate-schema`. See
|
|
2625
2903
|
* docs/cli.md and research/whole-harness-codegen.md.
|
|
2626
2904
|
*/
|
|
2905
|
+
/**
|
|
2906
|
+
* Keep an EXISTING `harness.gen.ts` fresh as a side effect of `compile`, so the
|
|
2907
|
+
* whole-harness registry tracks the specs without a separate manual
|
|
2908
|
+
* `generate-harness` run (the user almost never calls that verb by hand). Gated
|
|
2909
|
+
* on the file already existing: compile keeps a registry the user opted into
|
|
2910
|
+
* (committed like a lockfile) up to date — it never imposes one on a repo that
|
|
2911
|
+
* didn't ask for it. Cheap by construction: `generate-harness` only PARSES specs
|
|
2912
|
+
* (no linter spawning), unlike `generate-types`/`generate-schema` (which spawn
|
|
2913
|
+
* every linter and so stay on the config-change guard, off the hot compile path).
|
|
2914
|
+
* Returns false on a duplicate-name collision so it fails the compile.
|
|
2915
|
+
*/
|
|
2916
|
+
async function refreshHarnessGenIfPresent(harnessFlag) {
|
|
2917
|
+
const dir = process.cwd();
|
|
2918
|
+
const fullOut = (0, node_path_1.resolve)(dir, generate_harness_js_1.HARNESS_GEN_FILENAME);
|
|
2919
|
+
if (!(0, node_fs_1.existsSync)(fullOut))
|
|
2920
|
+
return true; // opt-in: nothing to refresh
|
|
2921
|
+
const adapter = harnessFlag
|
|
2922
|
+
? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
|
|
2923
|
+
: (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
|
|
2924
|
+
const model = await (0, generate_harness_js_1.loadHarnessModel)(dir, (abs) => loadSpec(abs));
|
|
2925
|
+
const result = (0, generate_harness_js_1.generateHarness)(model, {
|
|
2926
|
+
dialect: adapter.dialect,
|
|
2927
|
+
outDir: (0, node_path_1.dirname)(fullOut),
|
|
2928
|
+
});
|
|
2929
|
+
if (result.duplicate) {
|
|
2930
|
+
console.log(`\n✗ ${result.duplicate.message}`);
|
|
2931
|
+
console.log(`::error::${result.duplicate.message}`);
|
|
2932
|
+
return false;
|
|
2933
|
+
}
|
|
2934
|
+
(0, node_fs_1.writeFileSync)(fullOut, result.gen);
|
|
2935
|
+
console.log(` ↻ refreshed ${generate_harness_js_1.HARNESS_GEN_FILENAME} (${String(result.agentCount)} agent(s))`);
|
|
2936
|
+
return true;
|
|
2937
|
+
}
|
|
2627
2938
|
async function handleGenerateHarness(args, restArgs) {
|
|
2628
2939
|
const checkOnly = args.includes("--check");
|
|
2629
2940
|
const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
|
|
@@ -2662,7 +2973,7 @@ async function handleGenerateHarness(args, restArgs) {
|
|
|
2662
2973
|
}
|
|
2663
2974
|
if (checkOnly) {
|
|
2664
2975
|
if (!(0, node_fs_1.existsSync)(fullOut)) {
|
|
2665
|
-
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate
|
|
2976
|
+
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate harness\` to create it.`);
|
|
2666
2977
|
process.exit(1);
|
|
2667
2978
|
}
|
|
2668
2979
|
const existing = (0, node_fs_1.readFileSync)(fullOut, "utf-8");
|
|
@@ -2676,7 +2987,7 @@ async function handleGenerateHarness(args, restArgs) {
|
|
|
2676
2987
|
console.log(`\n✓ ${outPath} is up to date`);
|
|
2677
2988
|
}
|
|
2678
2989
|
else {
|
|
2679
|
-
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate
|
|
2990
|
+
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate harness\` to update.`);
|
|
2680
2991
|
process.exit(1);
|
|
2681
2992
|
}
|
|
2682
2993
|
return;
|
|
@@ -2753,32 +3064,6 @@ function harnessFlagFrom(argv) {
|
|
|
2753
3064
|
.find((a) => a.startsWith("--harness="))
|
|
2754
3065
|
?.slice("--harness=".length);
|
|
2755
3066
|
}
|
|
2756
|
-
/**
|
|
2757
|
-
* `vigiles explain <dir> [name]` — the deterministic WHY behind a low score (C4):
|
|
2758
|
-
* scan a plugin and surface the structural CAUSE of a behavioral symptom + the
|
|
2759
|
-
* one-line fix. No model — it reads the same `ScanReport` `scan` computes. An
|
|
2760
|
-
* optional surface name narrows to one underperforming skill/agent (the
|
|
2761
|
-
* optimizer's call). `--json` for the agent-consumable shape, `--harness=` to
|
|
2762
|
-
* override detection.
|
|
2763
|
-
*/
|
|
2764
|
-
function handleExplain(restArgs, args) {
|
|
2765
|
-
const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
|
|
2766
|
-
const surface = restArgs[1];
|
|
2767
|
-
const json = args.includes("--json");
|
|
2768
|
-
const harnessFlag = harnessFlagFrom(args);
|
|
2769
|
-
const adapter = harnessFlag
|
|
2770
|
-
? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
|
|
2771
|
-
: (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
|
|
2772
|
-
const report = (0, scan_js_1.scanPlugin)(dir, adapter.layout, adapter.dialect);
|
|
2773
|
-
const exps = surface ? (0, score_explainer_js_1.explainSurface)(report, surface) : (0, score_explainer_js_1.explainScore)(report);
|
|
2774
|
-
if (json) {
|
|
2775
|
-
console.log(JSON.stringify(exps, null, 2));
|
|
2776
|
-
return;
|
|
2777
|
-
}
|
|
2778
|
-
if (surface)
|
|
2779
|
-
console.log(`Explaining "${surface}":\n`);
|
|
2780
|
-
console.log((0, score_explainer_js_1.formatExplanations)(exps));
|
|
2781
|
-
}
|
|
2782
3067
|
/**
|
|
2783
3068
|
* Whole-harness capability lattice from a scanned plugin's agents (no `tools:` line →
|
|
2784
3069
|
* inherits-all). The substrate `scan --capability-diff` diffs. Reused for both the
|
|
@@ -2925,14 +3210,15 @@ function printUsage(command) {
|
|
|
2925
3210
|
console.log("vigiles — compile typed specs to instruction files");
|
|
2926
3211
|
console.log("");
|
|
2927
3212
|
console.log("Commands:");
|
|
2928
|
-
console.log(" vigiles init [flags] Setup project (--lint, --test, --harness=, --strict, --no-gha, --force)");
|
|
3213
|
+
console.log(" vigiles init [flags] Setup project (--lint, --test, --harness=, --strict, --report-only, --no-gha, --force)");
|
|
2929
3214
|
console.log(" vigiles compile [files...] Compile .spec.ts → .md");
|
|
3215
|
+
console.log(" vigiles eject [file] Un-manage a compiled file → plain hand-owned markdown (--keep-spec)");
|
|
2930
3216
|
console.log(" vigiles lint [files...] Verify references, find gaps in instruction files");
|
|
2931
|
-
console.log(" vigiles
|
|
2932
|
-
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");
|
|
2933
3220
|
console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
|
|
2934
3221
|
console.log(" vigiles eval [files...] Run *.eval.mjs real-model harness evals (--trials=N, --min=N, --no-skip)");
|
|
2935
|
-
console.log(" vigiles explain <dir> [name] Deterministic WHY a skill/agent underperforms + the fix (--json, --harness=)");
|
|
2936
3222
|
console.log(" vigiles scaffold-test [dir] Generate a starter test for each untested skill/agent/hook (--write, --json)");
|
|
2937
3223
|
console.log("");
|
|
2938
3224
|
console.log("Examples:");
|
|
@@ -2941,12 +3227,8 @@ function printUsage(command) {
|
|
|
2941
3227
|
console.log(" vigiles lint Verify references, hashes, coverage + suggestions");
|
|
2942
3228
|
console.log("");
|
|
2943
3229
|
console.log("Plumbing:");
|
|
2944
|
-
console.log(" vigiles generate-types
|
|
2945
|
-
console.log(" vigiles generate
|
|
2946
|
-
console.log(" vigiles generate-schema [out] Emit JSON Schema for vigiles: frontmatter");
|
|
2947
|
-
console.log(" vigiles generate-schema --check Verify schema.json is up to date");
|
|
2948
|
-
console.log(" vigiles generate-harness [dir] Emit harness.gen.ts — one typed registry");
|
|
2949
|
-
console.log(" vigiles generate-harness --check Verify harness.gen.ts is up to date");
|
|
3230
|
+
console.log(" vigiles generate <kind> Emit a dev-toolchain artifact: types (.d.ts) · schema (JSON Schema) · harness (harness.gen.ts)");
|
|
3231
|
+
console.log(" vigiles generate <kind> --check Verify the generated file is up to date");
|
|
2950
3232
|
console.log(" vigiles --version Print the version number");
|
|
2951
3233
|
if (command && command !== "--help") {
|
|
2952
3234
|
console.log(`\nUnknown command: "${command}"`);
|
|
@@ -3298,40 +3580,6 @@ const INSTRUCTION_FILE = /^(SKILL|CLAUDE|AGENTS)\.md$/;
|
|
|
3298
3580
|
function isInstructionFile(file) {
|
|
3299
3581
|
return INSTRUCTION_FILE.test((0, node_path_1.basename)(file));
|
|
3300
3582
|
}
|
|
3301
|
-
/**
|
|
3302
|
-
* Inspect an instruction file's symbol references: broken file-qualified refs
|
|
3303
|
-
* (`path.ext#symbol` whose file/symbol is wrong) and code-shaped references not
|
|
3304
|
-
* yet marked. Emits one line per finding via `log`; returns whether any issue
|
|
3305
|
-
* was found. `basePath` is the file's own directory (where paths resolve).
|
|
3306
|
-
*/
|
|
3307
|
-
function reportRefIssues(markdown, basePath, log) {
|
|
3308
|
-
const issues = (0, refs_js_1.collectRefIssues)(markdown, basePath);
|
|
3309
|
-
for (const m of issues)
|
|
3310
|
-
log(` ✗ ${m}`);
|
|
3311
|
-
return issues.length > 0;
|
|
3312
|
-
}
|
|
3313
|
-
/** `vigiles refs <file>` — check a file's symbol references (exit 2 on issues). */
|
|
3314
|
-
function refsCommand(target) {
|
|
3315
|
-
if (!target) {
|
|
3316
|
-
console.error("Usage: vigiles refs <instruction-file.md>");
|
|
3317
|
-
process.exit(2);
|
|
3318
|
-
}
|
|
3319
|
-
const cwd = process.cwd();
|
|
3320
|
-
let markdown;
|
|
3321
|
-
try {
|
|
3322
|
-
markdown = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(cwd, target), "utf-8");
|
|
3323
|
-
}
|
|
3324
|
-
catch {
|
|
3325
|
-
console.error(`Cannot read ${target}`);
|
|
3326
|
-
process.exit(2);
|
|
3327
|
-
}
|
|
3328
|
-
const bad = reportRefIssues(markdown, (0, node_path_1.dirname)((0, node_path_1.resolve)(cwd, target)), (m) => {
|
|
3329
|
-
console.log(m);
|
|
3330
|
-
});
|
|
3331
|
-
if (bad)
|
|
3332
|
-
process.exit(2);
|
|
3333
|
-
console.log(`✓ ${target}: all code references are marked and resolve.`);
|
|
3334
|
-
}
|
|
3335
3583
|
/**
|
|
3336
3584
|
* PostToolUse-hook entrypoint: when the agent edits an instruction file, force
|
|
3337
3585
|
* every code reference to carry a file-qualified mark (`path.ext#symbol`) and
|
|
@@ -3768,6 +4016,223 @@ async function runHookProgramCommand(file) {
|
|
|
3768
4016
|
}
|
|
3769
4017
|
}
|
|
3770
4018
|
}
|
|
4019
|
+
/**
|
|
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.
|
|
4123
|
+
*/
|
|
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.
|
|
4130
|
+
isTTY: process.stdout.isTTY && process.stdin.isTTY,
|
|
4131
|
+
json,
|
|
4132
|
+
noInteractive: args.includes("--no-interactive") || args.includes("--yes"),
|
|
4133
|
+
remembered: (0, validate_js_1.loadConfig)().audit?.measure,
|
|
4134
|
+
});
|
|
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);
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
/** Ask one question on a fresh readline, closing it after (the codebase pattern). */
|
|
4191
|
+
async function askOnce(q) {
|
|
4192
|
+
const readline = await import("node:readline");
|
|
4193
|
+
const rl = readline.createInterface({
|
|
4194
|
+
input: process.stdin,
|
|
4195
|
+
output: process.stdout,
|
|
4196
|
+
});
|
|
4197
|
+
try {
|
|
4198
|
+
return await new Promise((res) => {
|
|
4199
|
+
rl.question(q, (a) => {
|
|
4200
|
+
res(a.trim());
|
|
4201
|
+
});
|
|
4202
|
+
});
|
|
4203
|
+
}
|
|
4204
|
+
finally {
|
|
4205
|
+
rl.close();
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
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
|
+
}
|
|
4224
|
+
}
|
|
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 */
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
3771
4236
|
async function main() {
|
|
3772
4237
|
const args = process.argv.slice(2);
|
|
3773
4238
|
const command = args[0];
|
|
@@ -3786,11 +4251,11 @@ async function main() {
|
|
|
3786
4251
|
case "init": {
|
|
3787
4252
|
// Explicit --target bypasses the setup wizard and always creates a
|
|
3788
4253
|
// bare spec, so `npx vigiles init --target=<file>` is a reliable
|
|
3789
|
-
// remediation for the require-spec validator. Bare `vigiles init`
|
|
4254
|
+
// remediation for the require-instructions-spec validator. Bare `vigiles init`
|
|
3790
4255
|
// still runs the full wizard (project detection + auto-targets).
|
|
3791
4256
|
const hasTarget = args.some((a) => a.startsWith("--target="));
|
|
3792
4257
|
if (hasTarget) {
|
|
3793
|
-
|
|
4258
|
+
scaffoldSpec(args.slice(1));
|
|
3794
4259
|
}
|
|
3795
4260
|
else {
|
|
3796
4261
|
await setup(args);
|
|
@@ -3817,6 +4282,10 @@ async function main() {
|
|
|
3817
4282
|
if (specs.length > 0)
|
|
3818
4283
|
valid = (await compile(specs, config, { harnessFlag })) && valid;
|
|
3819
4284
|
valid = (await installHooks(hooks, harnessFlag)) && valid;
|
|
4285
|
+
// Keep an existing whole-harness registry in sync (cheap, opt-in) so the
|
|
4286
|
+
// user never hand-runs `generate-harness`. Skipped when no harness.gen.ts.
|
|
4287
|
+
if (specs.length > 0)
|
|
4288
|
+
valid = (await refreshHarnessGenIfPresent(harnessFlag)) && valid;
|
|
3820
4289
|
console.log("");
|
|
3821
4290
|
if (valid) {
|
|
3822
4291
|
console.log("Compilation complete.");
|
|
@@ -3827,6 +4296,11 @@ async function main() {
|
|
|
3827
4296
|
}
|
|
3828
4297
|
break;
|
|
3829
4298
|
}
|
|
4299
|
+
case "eject":
|
|
4300
|
+
// Inverse of compile: un-manage a compiled file → plain hand-owned
|
|
4301
|
+
// markdown (the "always ejectable" escape hatch).
|
|
4302
|
+
eject(args.slice(1));
|
|
4303
|
+
break;
|
|
3830
4304
|
case "lint": {
|
|
3831
4305
|
// lint = verify references + discover + guidance count
|
|
3832
4306
|
const flags = args.slice(1).filter((a) => a.startsWith("--"));
|
|
@@ -3844,7 +4318,15 @@ async function main() {
|
|
|
3844
4318
|
case "eval":
|
|
3845
4319
|
handleRunScripts("eval", args, restArgs);
|
|
3846
4320
|
break;
|
|
3847
|
-
case "
|
|
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.
|
|
3848
4330
|
const dirs = restArgs.length > 0 ? restArgs : ["."];
|
|
3849
4331
|
const json = args.includes("--json");
|
|
3850
4332
|
// A single dir that's a marketplace (e.g. wshobson/agents' 80+ plugins
|
|
@@ -3897,19 +4379,51 @@ async function main() {
|
|
|
3897
4379
|
}
|
|
3898
4380
|
console.log("");
|
|
3899
4381
|
}
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
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("");
|
|
3905
4397
|
}
|
|
3906
|
-
|
|
3907
|
-
|
|
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);
|
|
3908
4407
|
}
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
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) {
|
|
3913
4427
|
const mcpErrs = await (0, scan_js_1.verifyLiveMcpTools)(report, adapter.layout, adapter.dialect);
|
|
3914
4428
|
console.log(json
|
|
3915
4429
|
? JSON.stringify({ mcpContractTools: mcpErrs }, null, 2)
|
|
@@ -3928,30 +4442,85 @@ async function main() {
|
|
|
3928
4442
|
if (args.includes("--fail-on-widen") && diff.widened)
|
|
3929
4443
|
process.exitCode = 1;
|
|
3930
4444
|
}
|
|
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
|
+
}
|
|
3931
4515
|
}
|
|
3932
4516
|
break;
|
|
3933
4517
|
}
|
|
3934
|
-
case "explain":
|
|
3935
|
-
handleExplain(restArgs, args);
|
|
3936
|
-
break;
|
|
3937
4518
|
case "scaffold-test":
|
|
3938
4519
|
handleScaffoldTest(restArgs, args);
|
|
3939
4520
|
break;
|
|
3940
|
-
case "measure":
|
|
3941
|
-
await handleMeasure(restArgs, args);
|
|
3942
|
-
break;
|
|
3943
4521
|
// --- Plumbing ---
|
|
3944
|
-
case "generate
|
|
3945
|
-
|
|
3946
|
-
break;
|
|
3947
|
-
case "generate-schema":
|
|
3948
|
-
handleGenerateSchema(args, restArgs);
|
|
3949
|
-
break;
|
|
3950
|
-
case "generate-harness":
|
|
3951
|
-
await handleGenerateHarness(args, restArgs);
|
|
3952
|
-
break;
|
|
3953
|
-
case "refs":
|
|
3954
|
-
refsCommand(restArgs[0]);
|
|
4522
|
+
case "generate":
|
|
4523
|
+
await handleGenerate(restArgs, args);
|
|
3955
4524
|
break;
|
|
3956
4525
|
// Hidden umbrella for runtime entrypoints emitted into hooks configs — never
|
|
3957
4526
|
// typed by a human. See handleHookRuntime / the cohesive-cli-surface rule.
|