vigiles 7.0.0 → 8.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 +180 -89
- package/dist/cli-commands.d.ts +1 -1
- package/dist/cli-commands.js +2 -6
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +472 -132
- 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 +6 -6
- 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 +30 -6
- package/dist/core/validate.js +26 -26
- package/dist/guardrail-check.d.ts +1 -1
- package/dist/guardrail-check.js +1 -1
- package/dist/scan-trigger-suggest.d.ts +54 -0
- package/dist/scan-trigger-suggest.js +70 -0
- package/dist/scan.d.ts +17 -1
- package/dist/scan.js +32 -2
- 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 +1 -1
- package/skills/adopt-spec/SKILL.md +7 -7
- package/skills/linter-docs/eslint.md +1 -1
- package/skills/strengthen/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,6 +24,7 @@ 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
29
|
const score_explainer_js_1 = require("./score-explainer.js");
|
|
29
30
|
const scan_behavioral_js_1 = require("./scan-behavioral.js");
|
|
@@ -54,6 +55,7 @@ const linters_js_1 = require("./core/linters.js");
|
|
|
54
55
|
const harness_test_js_1 = require("./harness-test.js");
|
|
55
56
|
const run_scripts_js_1 = require("./adapters/claude-code/run-scripts.js");
|
|
56
57
|
const integrity_js_1 = require("./core/integrity.js");
|
|
58
|
+
const adopt_js_1 = require("./core/adopt.js");
|
|
57
59
|
const coverage_js_1 = require("./core/coverage.js");
|
|
58
60
|
const orphans_js_1 = require("./core/orphans.js");
|
|
59
61
|
const doc_refs_js_1 = require("./core/doc-refs.js");
|
|
@@ -447,7 +449,7 @@ function validateSpecs(filePaths, rulesConfig, silent = false) {
|
|
|
447
449
|
// Verify the referenced spec still exists
|
|
448
450
|
const specRef = (0, node_path_1.resolve)(process.cwd(), hashMatch[1]);
|
|
449
451
|
if (!(0, node_fs_1.existsSync)(specRef)) {
|
|
450
|
-
log(` ✗ [require-spec] ${filePath} references "${hashMatch[1]}" but that spec no longer exists.`);
|
|
452
|
+
log(` ✗ [require-instructions-spec] ${filePath} references "${hashMatch[1]}" but that spec no longer exists.`);
|
|
451
453
|
allValid = false;
|
|
452
454
|
}
|
|
453
455
|
continue;
|
|
@@ -476,7 +478,7 @@ function check(filePaths, silent = false) {
|
|
|
476
478
|
// `validateSpecs` only returns a boolean today, so we collapse
|
|
477
479
|
// failures to 1 until it starts reporting counts. Kept in its own
|
|
478
480
|
// counter so lint's "stale hash — run vigiles compile" remediation
|
|
479
|
-
// doesn't misreport a require-spec / other validation failure.
|
|
481
|
+
// doesn't misreport a require-instructions-spec / other validation failure.
|
|
480
482
|
validationErrors: specsValid ? 0 : 1,
|
|
481
483
|
};
|
|
482
484
|
}
|
|
@@ -658,6 +660,7 @@ function lintExitCode(report) {
|
|
|
658
660
|
report.descriptionOverlapErrors > 0 ||
|
|
659
661
|
report.frontmatterValidErrors > 0 ||
|
|
660
662
|
report.mcpHookErrors > 0 ||
|
|
663
|
+
report.preferCompiledHookErrors > 0 ||
|
|
661
664
|
report.symbolRefErrors > 0 ||
|
|
662
665
|
report.mcpRefErrors > 0)
|
|
663
666
|
return 2;
|
|
@@ -983,6 +986,9 @@ async function runLint(restArgs, flags, config) {
|
|
|
983
986
|
// 7m. MCP hook-target — a `type: mcp_tool` hook action that's incomplete or
|
|
984
987
|
// targets an undeclared server (the moat applied to the hook surface).
|
|
985
988
|
const mcpHookTargets = checkMcpHookTargets(config, silent, adapter);
|
|
989
|
+
// 7n. Prefer-compiled-hooks — ONE discovery nudge (not per-hook) toward
|
|
990
|
+
// compiled `vigiles/hook` artifacts when hand-written hooks ship. Recommendation.
|
|
991
|
+
const preferCompiledHooks = checkPreferCompiledHooks(config, silent, adapter);
|
|
986
992
|
// 8. Validate vigiles builder calls inside markdown code blocks. Default
|
|
987
993
|
// is to validate every ref; illustrative blocks opt out via
|
|
988
994
|
// `<!-- vigiles:ignore -->` (single block) or
|
|
@@ -1048,6 +1054,8 @@ async function runLint(restArgs, flags, config) {
|
|
|
1048
1054
|
frontmatterValidErrors: frontmatterValid.errors,
|
|
1049
1055
|
mcpHookIssues: mcpHookTargets.issues,
|
|
1050
1056
|
mcpHookErrors: mcpHookTargets.errors,
|
|
1057
|
+
preferCompiledHookIssues: preferCompiledHooks.issues,
|
|
1058
|
+
preferCompiledHookErrors: preferCompiledHooks.errors,
|
|
1051
1059
|
docRefErrors: docRefReport.errors.length,
|
|
1052
1060
|
symbolRefErrors,
|
|
1053
1061
|
mcpRefErrors,
|
|
@@ -1202,10 +1210,27 @@ function init(args) {
|
|
|
1202
1210
|
const targetFlag = args.find((a) => a.startsWith("--target="));
|
|
1203
1211
|
const target = targetFlag ? targetFlag.split("=")[1] : "CLAUDE.md";
|
|
1204
1212
|
const specPath = `${target}.spec.ts`;
|
|
1205
|
-
|
|
1213
|
+
const specAbs = (0, node_path_1.resolve)(process.cwd(), specPath);
|
|
1214
|
+
if ((0, node_fs_1.existsSync)(specAbs)) {
|
|
1206
1215
|
console.log(`${specPath} already exists.`);
|
|
1207
1216
|
return;
|
|
1208
1217
|
}
|
|
1218
|
+
// Auto-adopt: when the target file already exists with hand-written content
|
|
1219
|
+
// (no integrity header), faithfully convert it into a spec instead of
|
|
1220
|
+
// scaffolding a blank one — so `init` leaves you with a spec, not homework, and
|
|
1221
|
+
// `require-instructions-spec` is satisfied by construction. The compile that
|
|
1222
|
+
// follows reproduces the file (+ the header); review the diff. `vigiles eject`
|
|
1223
|
+
// reverses it. See research/install-enforcement-dx.md.
|
|
1224
|
+
const targetAbs = (0, node_path_1.resolve)(process.cwd(), target);
|
|
1225
|
+
if ((0, node_fs_1.existsSync)(targetAbs) && !targetHasHash(targetAbs)) {
|
|
1226
|
+
const md = (0, node_fs_1.readFileSync)(targetAbs, "utf-8");
|
|
1227
|
+
const { source, tier, sectionCount } = (0, adopt_js_1.adoptMarkdown)(md, (0, node_path_1.basename)(target));
|
|
1228
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(specAbs), { recursive: true });
|
|
1229
|
+
(0, node_fs_1.writeFileSync)(specAbs, source);
|
|
1230
|
+
console.log(`Adopted ${target} → ${specPath} (${tier}, ${String(sectionCount)} section${sectionCount === 1 ? "" : "s"}). ` +
|
|
1231
|
+
`Run \`vigiles compile\` and review the diff; the \`/strengthen\` skill upgrades prose to verified rules.`);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1209
1234
|
// The compiled output is derived from the spec FILE path; the spec's `target`
|
|
1210
1235
|
// field is the h1 + the name the compiler validates against, so it must be the
|
|
1211
1236
|
// bare filename even when the spec lives in a subdir (e.g. a sync tool's
|
|
@@ -1252,11 +1277,125 @@ export default claude({${targetLine}
|
|
|
1252
1277
|
},
|
|
1253
1278
|
});
|
|
1254
1279
|
`;
|
|
1255
|
-
const specAbs = (0, node_path_1.resolve)(process.cwd(), specPath);
|
|
1256
1280
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(specAbs), { recursive: true });
|
|
1257
1281
|
(0, node_fs_1.writeFileSync)(specAbs, template);
|
|
1258
1282
|
console.log(`Created ${specPath} — edit it and run \`vigiles compile\`.`);
|
|
1259
1283
|
}
|
|
1284
|
+
/**
|
|
1285
|
+
* `vigiles eject [file]` — the inverse of `compile`: hand a compiled instruction
|
|
1286
|
+
* file back to the user as plain, hand-owned markdown. Strips the `vigiles:sha256`
|
|
1287
|
+
* integrity header, adds a `require-instructions-spec` disable marker so `lint` stays quiet,
|
|
1288
|
+
* and removes the spec that managed it (`--keep-spec` to leave it). The
|
|
1289
|
+
* "managed-but-ejectable" escape hatch: adopting a typed spec is never a one-way
|
|
1290
|
+
* door.
|
|
1291
|
+
*/
|
|
1292
|
+
function eject(args) {
|
|
1293
|
+
const keepSpec = args.includes("--keep-spec");
|
|
1294
|
+
const file = args.find((a) => !a.startsWith("-")) ?? "CLAUDE.md";
|
|
1295
|
+
const abs = (0, node_path_1.resolve)(process.cwd(), file);
|
|
1296
|
+
if (!(0, node_fs_1.existsSync)(abs)) {
|
|
1297
|
+
console.error(`✗ ${file}: no such file.`);
|
|
1298
|
+
process.exitCode = 1;
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
const ejected = (0, integrity_js_1.ejectMarkdown)((0, node_fs_1.readFileSync)(abs, "utf-8"));
|
|
1302
|
+
if (!ejected) {
|
|
1303
|
+
console.log(`${file} is not vigiles-managed (no integrity header) — nothing to eject.`);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
(0, node_fs_1.writeFileSync)(abs, ejected.markdown);
|
|
1307
|
+
console.log(`✓ Ejected ${file} — it's now plain, hand-owned markdown.`);
|
|
1308
|
+
const specAbs = (0, node_path_1.resolve)(process.cwd(), ejected.specFile);
|
|
1309
|
+
// SAFETY: the `compiled from <path>` header is untrusted text — a hand-edited /
|
|
1310
|
+
// forged header could name `package.json` or `../../secret`, and a blind rmSync
|
|
1311
|
+
// would delete it. Only ever remove a `.spec.ts` that resolves INSIDE the
|
|
1312
|
+
// project (no `..` escape).
|
|
1313
|
+
const relSpec = (0, node_path_1.relative)((0, node_path_1.resolve)(process.cwd()), specAbs);
|
|
1314
|
+
const isSafeSpecTarget = ejected.specFile.endsWith(".spec.ts") &&
|
|
1315
|
+
relSpec !== "" &&
|
|
1316
|
+
!relSpec.startsWith("..");
|
|
1317
|
+
if ((0, node_fs_1.existsSync)(specAbs)) {
|
|
1318
|
+
if (!isSafeSpecTarget) {
|
|
1319
|
+
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.`);
|
|
1320
|
+
}
|
|
1321
|
+
else if (keepSpec) {
|
|
1322
|
+
console.log(` Kept ${ejected.specFile} (--keep-spec) — but \`vigiles compile\` would re-manage ${file}.`);
|
|
1323
|
+
}
|
|
1324
|
+
else if (specReferencedElsewhere(ejected.specFile, file)) {
|
|
1325
|
+
// A multi-target spec (e.g. `target: ["CLAUDE.md", "docs/AGENTS.md"]`, or a
|
|
1326
|
+
// mirror) compiles to several files — possibly in other directories — that
|
|
1327
|
+
// all name the SAME source in their header. Deleting it while ANY of them is
|
|
1328
|
+
// still managed would orphan that file. So keep the spec until its last
|
|
1329
|
+
// consumer is ejected.
|
|
1330
|
+
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.`);
|
|
1331
|
+
}
|
|
1332
|
+
else {
|
|
1333
|
+
(0, node_fs_1.rmSync)(specAbs);
|
|
1334
|
+
console.log(` Removed ${ejected.specFile} (the spec that managed it).`);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
// The disable marker is only added to instruction files (not skills/agents),
|
|
1338
|
+
// so only mention it when it was actually written.
|
|
1339
|
+
if (ejected.markdown.startsWith(integrity_js_1.REQUIRE_INSTRUCTIONS_SPEC_DISABLE)) {
|
|
1340
|
+
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.`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Whether another compiled markdown file ANYWHERE under the project still carries
|
|
1345
|
+
* an integrity header naming `specFile` — i.e. the spec has OTHER compiled outputs
|
|
1346
|
+
* (a multi-target `target: [...]` spec or a CLAUDE.md⇄AGENTS.md mirror, possibly
|
|
1347
|
+
* in a different directory like `docs/AGENTS.md`), so removing it would orphan
|
|
1348
|
+
* them. Walks the project tree from cwd, skipping heavy/irrelevant dirs; the
|
|
1349
|
+
* ejected file itself is skipped (its header was already stripped). Matches on the
|
|
1350
|
+
* RESOLVED spec path (not basename), so two unrelated specs that happen to share a
|
|
1351
|
+
* filename — `src/CLAUDE.md.spec.ts` vs `CLAUDE.md.spec.ts`, common in a monorepo —
|
|
1352
|
+
* don't collide; genuine sibling outputs of one compile carry the identical
|
|
1353
|
+
* recorded spec path. Best-effort: an unreadable file is ignored.
|
|
1354
|
+
*/
|
|
1355
|
+
function specReferencedElsewhere(specFile, ejectedFile) {
|
|
1356
|
+
const root = (0, node_path_1.resolve)(process.cwd());
|
|
1357
|
+
const ejectedAbs = (0, node_path_1.resolve)(root, ejectedFile);
|
|
1358
|
+
const specAbs = (0, node_path_1.resolve)(root, specFile);
|
|
1359
|
+
const SKIP = new Set([
|
|
1360
|
+
"node_modules",
|
|
1361
|
+
".git",
|
|
1362
|
+
"dist",
|
|
1363
|
+
"build",
|
|
1364
|
+
"coverage",
|
|
1365
|
+
".next",
|
|
1366
|
+
"out",
|
|
1367
|
+
]);
|
|
1368
|
+
const stack = [root];
|
|
1369
|
+
while (stack.length > 0) {
|
|
1370
|
+
const dir = stack.pop();
|
|
1371
|
+
let entries;
|
|
1372
|
+
try {
|
|
1373
|
+
entries = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true });
|
|
1374
|
+
}
|
|
1375
|
+
catch {
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
for (const e of entries) {
|
|
1379
|
+
const p = (0, node_path_1.resolve)(dir, e.name);
|
|
1380
|
+
if (e.isDirectory()) {
|
|
1381
|
+
if (!SKIP.has(e.name))
|
|
1382
|
+
stack.push(p);
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
if (!e.name.endsWith(".md") || p === ejectedAbs)
|
|
1386
|
+
continue;
|
|
1387
|
+
try {
|
|
1388
|
+
const header = (0, integrity_js_1.parseIntegrityHeader)((0, node_fs_1.readFileSync)(p, "utf-8"));
|
|
1389
|
+
if (header && (0, node_path_1.resolve)(root, header.specFile) === specAbs)
|
|
1390
|
+
return true;
|
|
1391
|
+
}
|
|
1392
|
+
catch {
|
|
1393
|
+
/* unreadable file — skip */
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return false;
|
|
1398
|
+
}
|
|
1260
1399
|
// ---------------------------------------------------------------------------
|
|
1261
1400
|
// Setup wizard
|
|
1262
1401
|
// ---------------------------------------------------------------------------
|
|
@@ -1436,7 +1575,8 @@ function scaffoldPillar2() {
|
|
|
1436
1575
|
console.log("✓ Scaffolded vigiles.harness.mjs — Pillar 2 starter (npx vigiles test)");
|
|
1437
1576
|
return ["vigiles.harness.mjs"];
|
|
1438
1577
|
}
|
|
1439
|
-
/** Interactive prompts (TTY only):
|
|
1578
|
+
/** Interactive prompts (TTY only): the readline IO shell over the pure
|
|
1579
|
+
* `collectSetupAnswers` (the Q&A logic is unit-tested in setup-plan.test.ts). */
|
|
1440
1580
|
async function promptSetup() {
|
|
1441
1581
|
const readline = await import("node:readline");
|
|
1442
1582
|
const rl = readline.createInterface({
|
|
@@ -1448,17 +1588,8 @@ async function promptSetup() {
|
|
|
1448
1588
|
res(a.trim() || def);
|
|
1449
1589
|
});
|
|
1450
1590
|
});
|
|
1451
|
-
const isYes = (s) => /^y(es)?$/i.test(s);
|
|
1452
1591
|
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
|
-
};
|
|
1592
|
+
return await (0, setup_plan_js_1.collectSetupAnswers)(ask);
|
|
1462
1593
|
}
|
|
1463
1594
|
finally {
|
|
1464
1595
|
rl.close();
|
|
@@ -1620,28 +1751,31 @@ function redirectSyncToolTargets(cwd, targets) {
|
|
|
1620
1751
|
async function setupPillar1(detected, targetValue, harnesses) {
|
|
1621
1752
|
const cwd = process.cwd();
|
|
1622
1753
|
const written = [];
|
|
1623
|
-
const
|
|
1754
|
+
const adopted = [];
|
|
1624
1755
|
// An explicit --target is honoured as-is; otherwise collapse a CLAUDE.md⇄
|
|
1625
1756
|
// AGENTS.md mirror (symlink or synced) to one canonical spec, then redirect
|
|
1626
1757
|
// into a sync tool's source slot when one would own the output.
|
|
1627
1758
|
const targets = targetValue
|
|
1628
1759
|
? determineTargets(detected, targetValue, harnesses)
|
|
1629
1760
|
: redirectSyncToolTargets(cwd, collapseMirroredTargets(determineTargets(detected, targetValue, harnesses), (0, compose_js_1.detectInstructionMirror)(cwd)));
|
|
1630
|
-
// Create specs
|
|
1631
|
-
//
|
|
1761
|
+
// 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
|
|
1763
|
+
// compile below reproduces it (the user reviews the diff). A greenfield target
|
|
1764
|
+
// gets a blank starter spec.
|
|
1632
1765
|
for (const target of targets) {
|
|
1633
1766
|
const specPath = `${target}.spec.ts`;
|
|
1634
|
-
const
|
|
1767
|
+
const targetAbs = (0, node_path_1.resolve)(cwd, target);
|
|
1768
|
+
const willAdopt = (0, node_fs_1.existsSync)(targetAbs) &&
|
|
1769
|
+
!targetHasHash(targetAbs) &&
|
|
1770
|
+
!(0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, specPath));
|
|
1635
1771
|
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, specPath))) {
|
|
1636
1772
|
console.log(`✓ ${specPath} already exists`);
|
|
1637
1773
|
}
|
|
1638
1774
|
else {
|
|
1639
|
-
init(["--target=" + target]); //
|
|
1775
|
+
init(["--target=" + target]); // adopts existing content, else blank scaffold
|
|
1640
1776
|
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\`.`);
|
|
1777
|
+
if (willAdopt)
|
|
1778
|
+
adopted.push(target);
|
|
1645
1779
|
}
|
|
1646
1780
|
}
|
|
1647
1781
|
// Generate types + schema.
|
|
@@ -1666,17 +1800,59 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
1666
1800
|
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(cwd, ".vigiles/schema.json"), schemaResult.json);
|
|
1667
1801
|
console.log("✓ Generated .vigiles/schema.json (YAML-LSP frontmatter schema)");
|
|
1668
1802
|
written.push(".vigiles/schema.json");
|
|
1669
|
-
// Compile — but
|
|
1670
|
-
//
|
|
1671
|
-
//
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1803
|
+
// Compile — but NEVER overwrite an existing hand-written file during `init`:
|
|
1804
|
+
// we compile only GREENFIELD targets (the file doesn't exist yet) and targets
|
|
1805
|
+
// we already manage (carry our hash). An ADOPTED file is left untouched — the
|
|
1806
|
+
// user reviews the generated spec and runs `vigiles compile` themselves to
|
|
1807
|
+
// switch it to spec-managed (non-destructive by default; the compile is
|
|
1808
|
+
// byte-faithful, but it's the user's call to make, with a diff to review).
|
|
1809
|
+
// And we only compile when `vigiles` actually resolves — a fresh repo hasn't
|
|
1810
|
+
// run `npm install` yet, so compiling would just error; defer it with a clear
|
|
1811
|
+
// next step instead of a scary stack-traceless "failed to load".
|
|
1812
|
+
const canCompile = canResolveVigiles(cwd);
|
|
1813
|
+
const specs = canCompile
|
|
1814
|
+
? findSpecs().filter((s) => {
|
|
1815
|
+
const tf = (0, node_path_1.resolve)(cwd, s.replace(/\.spec\.ts$/, ""));
|
|
1816
|
+
return !(0, node_fs_1.existsSync)(tf) || targetHasHash(tf);
|
|
1817
|
+
})
|
|
1818
|
+
: [];
|
|
1819
|
+
if (specs.length > 0) {
|
|
1820
|
+
console.log("\nCompiling specs...");
|
|
1678
1821
|
await compile(specs, (0, validate_js_1.loadConfig)());
|
|
1679
|
-
|
|
1822
|
+
}
|
|
1823
|
+
else if (!canCompile) {
|
|
1824
|
+
// Honest, project-type-aware guidance. A JS repo just needs `npm install`
|
|
1825
|
+
// (init already added the devDep). A repo with NO package.json (Python, Rust,
|
|
1826
|
+
// …) can't resolve the npm package at all, so point at the no-install paths
|
|
1827
|
+
// instead of a misleading `npm install`.
|
|
1828
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, "package.json"))) {
|
|
1829
|
+
console.log("\n Skipping compile — run `npm install` (to fetch the vigiles dep just added), then `npx vigiles compile`.");
|
|
1830
|
+
}
|
|
1831
|
+
else {
|
|
1832
|
+
console.log("\n No package.json here, so the typed-spec compile isn't available yet.\n" +
|
|
1833
|
+
" • `npx vigiles lint` verifies your instruction files right now — no install needed.\n" +
|
|
1834
|
+
" • To spec-manage them, add a package.json first: `npm init -y && npm i -D vigiles`, then `npx vigiles compile`.");
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
return { specTargets: targets, written, adopted };
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Whether `vigiles/spec` will resolve for a spec compiled from `cwd` — true when
|
|
1841
|
+
* vigiles is installed locally (`node_modules/vigiles`) or `cwd` IS the vigiles
|
|
1842
|
+
* package itself (the in-repo dogfood / a monorepo workspace). A fresh user repo
|
|
1843
|
+
* that hasn't run `npm install` yet returns false, so `init` defers the compile
|
|
1844
|
+
* instead of emitting a resolution error.
|
|
1845
|
+
*/
|
|
1846
|
+
function canResolveVigiles(cwd) {
|
|
1847
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, "node_modules", "vigiles")))
|
|
1848
|
+
return true;
|
|
1849
|
+
try {
|
|
1850
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(cwd, "package.json"), "utf-8"));
|
|
1851
|
+
return pkg.name === "vigiles";
|
|
1852
|
+
}
|
|
1853
|
+
catch {
|
|
1854
|
+
return false;
|
|
1855
|
+
}
|
|
1680
1856
|
}
|
|
1681
1857
|
/** Whether a harness binary (`claude`, `codex`) is on PATH. */
|
|
1682
1858
|
function harnessBinaryPresent(bin) {
|
|
@@ -1828,24 +2004,37 @@ function printDetection(detected, harnesses) {
|
|
|
1828
2004
|
/** Print the closing next-steps list + an honest commit hint (only files
|
|
1829
2005
|
* actually written this run). */
|
|
1830
2006
|
function printSetupSummary(opts) {
|
|
1831
|
-
const { plan, strict, targets,
|
|
2007
|
+
const { plan, strict, targets, adopted, written } = opts;
|
|
1832
2008
|
const specPathsList = targets.map((t) => `${t}.spec.ts`);
|
|
2009
|
+
// A repo with no package.json (Python/Rust/…) can't resolve the npm package,
|
|
2010
|
+
// so the typed-spec compile path needs an install first — give honest steps.
|
|
2011
|
+
const hasPkg = (0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), "package.json"));
|
|
1833
2012
|
console.log("\n---\nSetup complete.\n");
|
|
2013
|
+
// Next steps in DEPENDENCY order: install the dep first, then compile (which
|
|
2014
|
+
// needs it), then the optional hardening / test / CI steps.
|
|
1834
2015
|
const nextSteps = [];
|
|
1835
|
-
if (
|
|
1836
|
-
nextSteps.push(
|
|
2016
|
+
if (written.includes("package.json")) {
|
|
2017
|
+
nextSteps.push("Run `npm install` to fetch the vigiles dev dependency");
|
|
2018
|
+
}
|
|
2019
|
+
if (adopted.length > 0 && !hasPkg) {
|
|
2020
|
+
// Non-JS repo: compile needs a local install. Point at the no-install verify
|
|
2021
|
+
// path + how to enable specs, instead of a compile that would fail.
|
|
2022
|
+
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`);
|
|
2023
|
+
}
|
|
2024
|
+
else if (adopted.length > 0) {
|
|
2025
|
+
// Adoption is NON-DESTRUCTIVE: the file is untouched until you compile, so
|
|
2026
|
+
// the diff to review is what compile WOULD produce (byte-faithful).
|
|
2027
|
+
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)`);
|
|
2028
|
+
nextSteps.push("Run the `/strengthen` skill to upgrade prose rules to verified enforce()/guard()");
|
|
1837
2029
|
}
|
|
1838
2030
|
else if (specPathsList.length > 0) {
|
|
1839
|
-
nextSteps.push(`Edit ${specPathsList.join(", ")} — add your conventions, then \`/strengthen
|
|
2031
|
+
nextSteps.push(`Edit ${specPathsList.join(", ")} — add your conventions, then \`npx vigiles compile\` (and \`/strengthen\`)`);
|
|
1840
2032
|
}
|
|
1841
2033
|
if (plan.test) {
|
|
1842
2034
|
nextSteps.push("Edit vigiles.harness.mjs to test a real hook, then `npx vigiles test`");
|
|
1843
2035
|
}
|
|
1844
|
-
if (written.includes("package.json")) {
|
|
1845
|
-
nextSteps.push("Run `npm install` to fetch the vigiles dev dependency");
|
|
1846
|
-
}
|
|
1847
2036
|
if (!strict) {
|
|
1848
|
-
nextSteps.push("When ready, enforce in CI: npx vigiles init --strict");
|
|
2037
|
+
nextSteps.push("When ready, enforce specs + tests in CI: `npx vigiles init --strict`");
|
|
1849
2038
|
}
|
|
1850
2039
|
nextSteps.forEach((s, i) => {
|
|
1851
2040
|
console.log(` ${String(i + 1)}. ${s}`);
|
|
@@ -1858,12 +2047,15 @@ function printSetupSummary(opts) {
|
|
|
1858
2047
|
}
|
|
1859
2048
|
async function setup(args) {
|
|
1860
2049
|
const parsed = (0, setup_plan_js_1.parseSetupArgs)(args);
|
|
1861
|
-
const strict = parsed.strict;
|
|
1862
2050
|
// Plan: defaults → flags → interactive prompts (only a human at a TTY).
|
|
1863
2051
|
let plan = (0, setup_plan_js_1.resolvePlan)(parsed);
|
|
1864
2052
|
if ((0, setup_plan_js_1.shouldPrompt)(parsed, process.stdin.isTTY ?? false)) {
|
|
1865
2053
|
plan = (0, setup_plan_js_1.resolvePlan)(parsed, await promptSetup());
|
|
1866
2054
|
}
|
|
2055
|
+
// Read strict from the RESOLVED plan, not the raw flag — an interactive "yes"
|
|
2056
|
+
// to the workflow tier (no `--strict` flag) sets plan.strict, and the config
|
|
2057
|
+
// write + summary must honor it.
|
|
2058
|
+
const strict = plan.strict;
|
|
1867
2059
|
const pillars = [plan.lint && "lint", plan.test && "test"]
|
|
1868
2060
|
.filter(Boolean)
|
|
1869
2061
|
.join(" + ");
|
|
@@ -1876,12 +2068,12 @@ async function setup(args) {
|
|
|
1876
2068
|
const written = [];
|
|
1877
2069
|
// Lint pillar — verify instruction-file references.
|
|
1878
2070
|
let targets = [];
|
|
1879
|
-
let
|
|
2071
|
+
let adopted = [];
|
|
1880
2072
|
if (plan.lint) {
|
|
1881
2073
|
console.log("");
|
|
1882
2074
|
const p1 = await setupPillar1(detected, parsed.target, harnesses);
|
|
1883
2075
|
targets = p1.specTargets;
|
|
1884
|
-
|
|
2076
|
+
adopted = p1.adopted;
|
|
1885
2077
|
written.push(...p1.written);
|
|
1886
2078
|
}
|
|
1887
2079
|
// Test pillar — test the harness.
|
|
@@ -1912,9 +2104,16 @@ async function setup(args) {
|
|
|
1912
2104
|
console.log(" npm install -D rule-porter");
|
|
1913
2105
|
}
|
|
1914
2106
|
// 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
|
-
|
|
2107
|
+
// deterministically (no cwd sniffing), plus strict rule severities on --strict
|
|
2108
|
+
// (or all-warn under --report-only).
|
|
2109
|
+
writeProjectConfig({
|
|
2110
|
+
harnesses,
|
|
2111
|
+
strict,
|
|
2112
|
+
reportOnly: parsed.reportOnly,
|
|
2113
|
+
lint: plan.lint,
|
|
2114
|
+
written,
|
|
2115
|
+
});
|
|
2116
|
+
printSetupSummary({ plan, strict, targets, adopted, written });
|
|
1918
2117
|
}
|
|
1919
2118
|
/** Canonical, de-duplicated harness list → a config value (string when one). */
|
|
1920
2119
|
function harnessConfigValue(harnesses) {
|
|
@@ -1941,6 +2140,8 @@ function writeProjectConfig(opts) {
|
|
|
1941
2140
|
const merged = (0, setup_plan_js_1.mergeProjectConfig)(existing, {
|
|
1942
2141
|
harness: harnessConfigValue(opts.harnesses),
|
|
1943
2142
|
strict: opts.strict,
|
|
2143
|
+
reportOnly: opts.reportOnly,
|
|
2144
|
+
lint: opts.lint,
|
|
1944
2145
|
});
|
|
1945
2146
|
if (!merged)
|
|
1946
2147
|
return;
|
|
@@ -2172,6 +2373,34 @@ function checkSkillFrontmatter(config, silent, adapter) {
|
|
|
2172
2373
|
}
|
|
2173
2374
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
2174
2375
|
}
|
|
2376
|
+
/**
|
|
2377
|
+
* Apply the `prefer-compiled-hooks` rule: a SINGLE repo-level recommendation
|
|
2378
|
+
* (one finding regardless of hook count) nudging hand-written hooks toward
|
|
2379
|
+
* compiled `vigiles/hook` artifacts. A discovery nudge, not a defect — the shell
|
|
2380
|
+
* lane stays first-class — so it fires once and the message links the guide.
|
|
2381
|
+
* Reuses `scanPlugin`'s `manualHookCount` (one-detector-no-drift).
|
|
2382
|
+
*/
|
|
2383
|
+
function checkPreferCompiledHooks(config, silent, adapter) {
|
|
2384
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["prefer-compiled-hooks"]);
|
|
2385
|
+
if (!sev)
|
|
2386
|
+
return { issues: 0, errors: 0 };
|
|
2387
|
+
let count;
|
|
2388
|
+
try {
|
|
2389
|
+
count = (0, scan_js_1.scanPlugin)(process.cwd(), adapter.layout, adapter.dialect).manualHookCount;
|
|
2390
|
+
}
|
|
2391
|
+
catch {
|
|
2392
|
+
return { issues: 0, errors: 0 };
|
|
2393
|
+
}
|
|
2394
|
+
if (count === 0)
|
|
2395
|
+
return { issues: 0, errors: 0 };
|
|
2396
|
+
const message = (0, scan_js_1.preferCompiledHooksMessage)(count);
|
|
2397
|
+
if (!silent) {
|
|
2398
|
+
console.log("\nCompiled-hooks check:\n");
|
|
2399
|
+
console.log(` ${sev === "error" ? "✗" : "ℹ"} ${message}`);
|
|
2400
|
+
ghAnnotate(sev === "error" ? "error" : "warning", message);
|
|
2401
|
+
}
|
|
2402
|
+
return { issues: 1, errors: sev === "error" ? 1 : 0 };
|
|
2403
|
+
}
|
|
2175
2404
|
/**
|
|
2176
2405
|
* Apply the `mcp-config` rule: a declared MCP server with neither a `command`
|
|
2177
2406
|
* (stdio) nor a `url` (http/sse) can't start. Reuses `scanPlugin`'s `mcpIssues`.
|
|
@@ -2449,34 +2678,45 @@ async function countGuidanceRules(silent = false) {
|
|
|
2449
2678
|
// Command handlers for main()
|
|
2450
2679
|
// ---------------------------------------------------------------------------
|
|
2451
2680
|
function findInstructionFiles(restArgs, exclude = []) {
|
|
2452
|
-
if (restArgs.length > 0)
|
|
2453
|
-
return restArgs;
|
|
2454
2681
|
const patterns = ["**/CLAUDE.md", "**/AGENTS.md", "**/SKILL.md"];
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2682
|
+
// `exclude` (from .vigilesrc.json) drops vendored/benchmark fixtures the repo's
|
|
2683
|
+
// own lint shouldn't police — a third-party CLAUDE.md isn't held to require-instructions-spec.
|
|
2684
|
+
// node_modules/dist/.git stay always-excluded.
|
|
2685
|
+
const ignore = [...IGNORE_NODE_MODULES, "dist/**", ".git/**", ...exclude];
|
|
2686
|
+
// Discover instruction files under one directory, as paths relative to cwd.
|
|
2687
|
+
const discoverIn = (dirAbs) => patterns
|
|
2688
|
+
.flatMap((p) => (0, glob_1.globSync)(p, { ignore, cwd: dirAbs, absolute: true }))
|
|
2689
|
+
.map((abs) => (0, node_path_1.relative)(process.cwd(), abs));
|
|
2690
|
+
if (restArgs.length === 0)
|
|
2691
|
+
return discoverIn(process.cwd());
|
|
2692
|
+
// Explicit args: expand a DIRECTORY to the instruction files inside it (so
|
|
2693
|
+
// `vigiles lint .` works), keep a file arg as-is, and pass a non-existent arg
|
|
2694
|
+
// through unchanged (lint reports it as not-found rather than crashing).
|
|
2695
|
+
const out = [];
|
|
2696
|
+
for (const arg of restArgs) {
|
|
2697
|
+
const abs = (0, node_path_1.resolve)(process.cwd(), arg);
|
|
2698
|
+
if ((0, node_fs_1.existsSync)(abs) && (0, node_fs_1.lstatSync)(abs).isDirectory()) {
|
|
2699
|
+
out.push(...discoverIn(abs));
|
|
2700
|
+
}
|
|
2701
|
+
else {
|
|
2702
|
+
out.push(arg);
|
|
2703
|
+
}
|
|
2464
2704
|
}
|
|
2465
|
-
return
|
|
2705
|
+
return out;
|
|
2466
2706
|
}
|
|
2467
2707
|
/** Value of a `--flag=value` arg (the `=` form, so it never collides with a positional). */
|
|
2468
2708
|
function flagValue(args, name) {
|
|
2469
2709
|
return args.find((a) => a.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
2470
2710
|
}
|
|
2471
2711
|
/**
|
|
2472
|
-
* `vigiles
|
|
2712
|
+
* `vigiles scan <dir> --trigger` — the MODEL-GATED behavioral report on a plugin (the paid
|
|
2473
2713
|
* tier; `scan` stays free/deterministic). Loads the author-supplied per-skill prompt
|
|
2474
2714
|
* sets (`--prompts`) and reports BOTH behavioral columns: trigger-rate (does each
|
|
2475
2715
|
* skill actually FIRE — recall + precision) and the selection-collision matrix (does
|
|
2476
2716
|
* one skill HIJACK a sibling's prompt — the behavioral confirmation of the
|
|
2477
2717
|
* deterministic `description-overlap` rule). Needs the harness CLI + model auth;
|
|
2478
2718
|
* degrades honestly ("unavailable") when absent. The OSS-testing front door:
|
|
2479
|
-
* `vigiles
|
|
2719
|
+
* `vigiles scan ./plugin --trigger --prompts=p.json`.
|
|
2480
2720
|
*/
|
|
2481
2721
|
async function handleMeasure(restArgs, args) {
|
|
2482
2722
|
const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
|
|
@@ -2529,6 +2769,31 @@ async function handleMeasure(restArgs, args) {
|
|
|
2529
2769
|
console.log(`\n${(0, scan_behavioral_js_1.formatBehavioralReport)(trigger)}`);
|
|
2530
2770
|
console.log(`\n${(0, scan_behavioral_js_1.formatSelectionReport)(collisions)}`);
|
|
2531
2771
|
}
|
|
2772
|
+
/**
|
|
2773
|
+
* `vigiles generate <kind>` — one verb over the three dev-toolchain generators
|
|
2774
|
+
* (types/schema/harness). Each emits a file YOUR editor/tsc reads, not the agent
|
|
2775
|
+
* — grouped under one verb instead of N hyphenated siblings (cohesive-cli-surface,
|
|
2776
|
+
* high-bar-for-new-commands). The kind is the first positional; the rest passes
|
|
2777
|
+
* through to the per-kind handler (out path / dir).
|
|
2778
|
+
*/
|
|
2779
|
+
async function handleGenerate(restArgs, args) {
|
|
2780
|
+
const kind = restArgs[0];
|
|
2781
|
+
const rest = restArgs.slice(1);
|
|
2782
|
+
switch (kind) {
|
|
2783
|
+
case "types":
|
|
2784
|
+
handleGenerateTypes(args, rest);
|
|
2785
|
+
break;
|
|
2786
|
+
case "schema":
|
|
2787
|
+
handleGenerateSchema(args, rest);
|
|
2788
|
+
break;
|
|
2789
|
+
case "harness":
|
|
2790
|
+
await handleGenerateHarness(args, rest);
|
|
2791
|
+
break;
|
|
2792
|
+
default:
|
|
2793
|
+
console.error("Usage: vigiles generate <types|schema|harness> [out] [--check]");
|
|
2794
|
+
process.exit(2);
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2532
2797
|
function handleGenerateTypes(args, restArgs) {
|
|
2533
2798
|
const checkOnly = args.includes("--check");
|
|
2534
2799
|
const outPath = restArgs[0] ?? ".vigiles/generated.d.ts";
|
|
@@ -2552,7 +2817,7 @@ function handleGenerateTypes(args, restArgs) {
|
|
|
2552
2817
|
if (checkOnly) {
|
|
2553
2818
|
// --check: compare against existing file, exit 1 if stale
|
|
2554
2819
|
if (!(0, node_fs_1.existsSync)(fullOut)) {
|
|
2555
|
-
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate
|
|
2820
|
+
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate types\` to create it.`);
|
|
2556
2821
|
process.exit(1);
|
|
2557
2822
|
}
|
|
2558
2823
|
const existing = (0, node_fs_1.readFileSync)(fullOut, "utf-8");
|
|
@@ -2567,7 +2832,7 @@ function handleGenerateTypes(args, restArgs) {
|
|
|
2567
2832
|
console.log(`\n✓ ${outPath} is up to date`);
|
|
2568
2833
|
}
|
|
2569
2834
|
else {
|
|
2570
|
-
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate
|
|
2835
|
+
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate types\` to update.`);
|
|
2571
2836
|
process.exit(1);
|
|
2572
2837
|
}
|
|
2573
2838
|
return;
|
|
@@ -2594,7 +2859,7 @@ function handleGenerateSchema(args, restArgs) {
|
|
|
2594
2859
|
const fullOut = (0, node_path_1.resolve)(process.cwd(), outPath);
|
|
2595
2860
|
if (checkOnly) {
|
|
2596
2861
|
if (!(0, node_fs_1.existsSync)(fullOut)) {
|
|
2597
|
-
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate
|
|
2862
|
+
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate schema\` to create it.`);
|
|
2598
2863
|
process.exit(1);
|
|
2599
2864
|
}
|
|
2600
2865
|
const existing = (0, node_fs_1.readFileSync)(fullOut, "utf-8");
|
|
@@ -2602,7 +2867,7 @@ function handleGenerateSchema(args, restArgs) {
|
|
|
2602
2867
|
console.log(`\n✓ ${outPath} is up to date`);
|
|
2603
2868
|
}
|
|
2604
2869
|
else {
|
|
2605
|
-
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate
|
|
2870
|
+
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate schema\` to update.`);
|
|
2606
2871
|
process.exit(1);
|
|
2607
2872
|
}
|
|
2608
2873
|
return;
|
|
@@ -2617,13 +2882,46 @@ function handleGenerateSchema(args, restArgs) {
|
|
|
2617
2882
|
` # yaml-language-server: $schema=./${outPath}`);
|
|
2618
2883
|
}
|
|
2619
2884
|
/**
|
|
2620
|
-
* `vigiles generate
|
|
2885
|
+
* `vigiles generate harness [dir] [out]` — emit one typed registry over every
|
|
2621
2886
|
* `*.spec.ts` under `dir`, so a single `tsc --noEmit` cross-checks the whole
|
|
2622
2887
|
* harness (dangling delegates → a tsc error; duplicate names → this command
|
|
2623
2888
|
* exits non-zero; the capability lattice → a computed export). The third
|
|
2624
2889
|
* generated artifact beside `generate-types` / `generate-schema`. See
|
|
2625
2890
|
* docs/cli.md and research/whole-harness-codegen.md.
|
|
2626
2891
|
*/
|
|
2892
|
+
/**
|
|
2893
|
+
* Keep an EXISTING `harness.gen.ts` fresh as a side effect of `compile`, so the
|
|
2894
|
+
* whole-harness registry tracks the specs without a separate manual
|
|
2895
|
+
* `generate-harness` run (the user almost never calls that verb by hand). Gated
|
|
2896
|
+
* on the file already existing: compile keeps a registry the user opted into
|
|
2897
|
+
* (committed like a lockfile) up to date — it never imposes one on a repo that
|
|
2898
|
+
* didn't ask for it. Cheap by construction: `generate-harness` only PARSES specs
|
|
2899
|
+
* (no linter spawning), unlike `generate-types`/`generate-schema` (which spawn
|
|
2900
|
+
* every linter and so stay on the config-change guard, off the hot compile path).
|
|
2901
|
+
* Returns false on a duplicate-name collision so it fails the compile.
|
|
2902
|
+
*/
|
|
2903
|
+
async function refreshHarnessGenIfPresent(harnessFlag) {
|
|
2904
|
+
const dir = process.cwd();
|
|
2905
|
+
const fullOut = (0, node_path_1.resolve)(dir, generate_harness_js_1.HARNESS_GEN_FILENAME);
|
|
2906
|
+
if (!(0, node_fs_1.existsSync)(fullOut))
|
|
2907
|
+
return true; // opt-in: nothing to refresh
|
|
2908
|
+
const adapter = harnessFlag
|
|
2909
|
+
? (0, adapter_registry_js_1.resolveAdapter)(dir, harnessFlag)
|
|
2910
|
+
: (0, adapter_registry_js_1.detectAdapterResult)(dir).adapter;
|
|
2911
|
+
const model = await (0, generate_harness_js_1.loadHarnessModel)(dir, (abs) => loadSpec(abs));
|
|
2912
|
+
const result = (0, generate_harness_js_1.generateHarness)(model, {
|
|
2913
|
+
dialect: adapter.dialect,
|
|
2914
|
+
outDir: (0, node_path_1.dirname)(fullOut),
|
|
2915
|
+
});
|
|
2916
|
+
if (result.duplicate) {
|
|
2917
|
+
console.log(`\n✗ ${result.duplicate.message}`);
|
|
2918
|
+
console.log(`::error::${result.duplicate.message}`);
|
|
2919
|
+
return false;
|
|
2920
|
+
}
|
|
2921
|
+
(0, node_fs_1.writeFileSync)(fullOut, result.gen);
|
|
2922
|
+
console.log(` ↻ refreshed ${generate_harness_js_1.HARNESS_GEN_FILENAME} (${String(result.agentCount)} agent(s))`);
|
|
2923
|
+
return true;
|
|
2924
|
+
}
|
|
2627
2925
|
async function handleGenerateHarness(args, restArgs) {
|
|
2628
2926
|
const checkOnly = args.includes("--check");
|
|
2629
2927
|
const dir = (0, node_path_1.resolve)(restArgs[0] ?? ".");
|
|
@@ -2662,7 +2960,7 @@ async function handleGenerateHarness(args, restArgs) {
|
|
|
2662
2960
|
}
|
|
2663
2961
|
if (checkOnly) {
|
|
2664
2962
|
if (!(0, node_fs_1.existsSync)(fullOut)) {
|
|
2665
|
-
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate
|
|
2963
|
+
console.log(`\n✗ ${outPath} does not exist. Run \`vigiles generate harness\` to create it.`);
|
|
2666
2964
|
process.exit(1);
|
|
2667
2965
|
}
|
|
2668
2966
|
const existing = (0, node_fs_1.readFileSync)(fullOut, "utf-8");
|
|
@@ -2676,7 +2974,7 @@ async function handleGenerateHarness(args, restArgs) {
|
|
|
2676
2974
|
console.log(`\n✓ ${outPath} is up to date`);
|
|
2677
2975
|
}
|
|
2678
2976
|
else {
|
|
2679
|
-
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate
|
|
2977
|
+
console.log(`\n✗ ${outPath} is stale. Run \`vigiles generate harness\` to update.`);
|
|
2680
2978
|
process.exit(1);
|
|
2681
2979
|
}
|
|
2682
2980
|
return;
|
|
@@ -2754,7 +3052,7 @@ function harnessFlagFrom(argv) {
|
|
|
2754
3052
|
?.slice("--harness=".length);
|
|
2755
3053
|
}
|
|
2756
3054
|
/**
|
|
2757
|
-
* `vigiles
|
|
3055
|
+
* `vigiles scan <dir> --explain [name]` — the deterministic WHY behind a low score (C4):
|
|
2758
3056
|
* scan a plugin and surface the structural CAUSE of a behavioral symptom + the
|
|
2759
3057
|
* one-line fix. No model — it reads the same `ScanReport` `scan` computes. An
|
|
2760
3058
|
* optional surface name narrows to one underperforming skill/agent (the
|
|
@@ -2925,14 +3223,15 @@ function printUsage(command) {
|
|
|
2925
3223
|
console.log("vigiles — compile typed specs to instruction files");
|
|
2926
3224
|
console.log("");
|
|
2927
3225
|
console.log("Commands:");
|
|
2928
|
-
console.log(" vigiles init [flags] Setup project (--lint, --test, --harness=, --strict, --no-gha, --force)");
|
|
3226
|
+
console.log(" vigiles init [flags] Setup project (--lint, --test, --harness=, --strict, --report-only, --no-gha, --force)");
|
|
2929
3227
|
console.log(" vigiles compile [files...] Compile .spec.ts → .md");
|
|
3228
|
+
console.log(" vigiles eject [file] Un-manage a compiled file → plain hand-owned markdown (--keep-spec)");
|
|
2930
3229
|
console.log(" vigiles lint [files...] Verify references, find gaps in instruction files");
|
|
2931
3230
|
console.log(" vigiles scan [dir...] Report what a plugin ships + what's broken (free; 2+ dirs → leaderboard)");
|
|
2932
|
-
console.log("
|
|
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)");
|
|
2933
3233
|
console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
|
|
2934
3234
|
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
3235
|
console.log(" vigiles scaffold-test [dir] Generate a starter test for each untested skill/agent/hook (--write, --json)");
|
|
2937
3236
|
console.log("");
|
|
2938
3237
|
console.log("Examples:");
|
|
@@ -2941,12 +3240,8 @@ function printUsage(command) {
|
|
|
2941
3240
|
console.log(" vigiles lint Verify references, hashes, coverage + suggestions");
|
|
2942
3241
|
console.log("");
|
|
2943
3242
|
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");
|
|
3243
|
+
console.log(" vigiles generate <kind> Emit a dev-toolchain artifact: types (.d.ts) · schema (JSON Schema) · harness (harness.gen.ts)");
|
|
3244
|
+
console.log(" vigiles generate <kind> --check Verify the generated file is up to date");
|
|
2950
3245
|
console.log(" vigiles --version Print the version number");
|
|
2951
3246
|
if (command && command !== "--help") {
|
|
2952
3247
|
console.log(`\nUnknown command: "${command}"`);
|
|
@@ -3298,40 +3593,6 @@ const INSTRUCTION_FILE = /^(SKILL|CLAUDE|AGENTS)\.md$/;
|
|
|
3298
3593
|
function isInstructionFile(file) {
|
|
3299
3594
|
return INSTRUCTION_FILE.test((0, node_path_1.basename)(file));
|
|
3300
3595
|
}
|
|
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
3596
|
/**
|
|
3336
3597
|
* PostToolUse-hook entrypoint: when the agent edits an instruction file, force
|
|
3337
3598
|
* every code reference to carry a file-qualified mark (`path.ext#symbol`) and
|
|
@@ -3768,6 +4029,76 @@ async function runHookProgramCommand(file) {
|
|
|
3768
4029
|
}
|
|
3769
4030
|
}
|
|
3770
4031
|
}
|
|
4032
|
+
/**
|
|
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`).
|
|
4038
|
+
*/
|
|
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).
|
|
4047
|
+
isTTY: process.stdout.isTTY && process.stdin.isTTY,
|
|
4048
|
+
triggerableSkills: triggerable.length,
|
|
4049
|
+
json,
|
|
4050
|
+
noInteractive: args.includes("--no-interactive") || args.includes("--yes"),
|
|
4051
|
+
});
|
|
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;
|
|
4057
|
+
}
|
|
4058
|
+
await promptTriggerSetup(report, dir, triggerable.length, args);
|
|
4059
|
+
}
|
|
4060
|
+
/** Ask one question on a fresh readline, closing it after (the codebase pattern). */
|
|
4061
|
+
async function askOnce(q) {
|
|
4062
|
+
const readline = await import("node:readline");
|
|
4063
|
+
const rl = readline.createInterface({
|
|
4064
|
+
input: process.stdin,
|
|
4065
|
+
output: process.stdout,
|
|
4066
|
+
});
|
|
4067
|
+
try {
|
|
4068
|
+
return await new Promise((res) => {
|
|
4069
|
+
rl.question(q, (a) => {
|
|
4070
|
+
res(a.trim());
|
|
4071
|
+
});
|
|
4072
|
+
});
|
|
4073
|
+
}
|
|
4074
|
+
finally {
|
|
4075
|
+
rl.close();
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
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;
|
|
4091
|
+
}
|
|
4092
|
+
if (exists) {
|
|
4093
|
+
await handleMeasure([dir], [...args, "--prompts=trigger-prompts.json"]);
|
|
4094
|
+
return;
|
|
4095
|
+
}
|
|
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
|
+
}
|
|
3771
4102
|
async function main() {
|
|
3772
4103
|
const args = process.argv.slice(2);
|
|
3773
4104
|
const command = args[0];
|
|
@@ -3786,7 +4117,7 @@ async function main() {
|
|
|
3786
4117
|
case "init": {
|
|
3787
4118
|
// Explicit --target bypasses the setup wizard and always creates a
|
|
3788
4119
|
// bare spec, so `npx vigiles init --target=<file>` is a reliable
|
|
3789
|
-
// remediation for the require-spec validator. Bare `vigiles init`
|
|
4120
|
+
// remediation for the require-instructions-spec validator. Bare `vigiles init`
|
|
3790
4121
|
// still runs the full wizard (project detection + auto-targets).
|
|
3791
4122
|
const hasTarget = args.some((a) => a.startsWith("--target="));
|
|
3792
4123
|
if (hasTarget) {
|
|
@@ -3817,6 +4148,10 @@ async function main() {
|
|
|
3817
4148
|
if (specs.length > 0)
|
|
3818
4149
|
valid = (await compile(specs, config, { harnessFlag })) && valid;
|
|
3819
4150
|
valid = (await installHooks(hooks, harnessFlag)) && valid;
|
|
4151
|
+
// Keep an existing whole-harness registry in sync (cheap, opt-in) so the
|
|
4152
|
+
// user never hand-runs `generate-harness`. Skipped when no harness.gen.ts.
|
|
4153
|
+
if (specs.length > 0)
|
|
4154
|
+
valid = (await refreshHarnessGenIfPresent(harnessFlag)) && valid;
|
|
3820
4155
|
console.log("");
|
|
3821
4156
|
if (valid) {
|
|
3822
4157
|
console.log("Compilation complete.");
|
|
@@ -3827,6 +4162,11 @@ async function main() {
|
|
|
3827
4162
|
}
|
|
3828
4163
|
break;
|
|
3829
4164
|
}
|
|
4165
|
+
case "eject":
|
|
4166
|
+
// Inverse of compile: un-manage a compiled file → plain hand-owned
|
|
4167
|
+
// markdown (the "always ejectable" escape hatch).
|
|
4168
|
+
eject(args.slice(1));
|
|
4169
|
+
break;
|
|
3830
4170
|
case "lint": {
|
|
3831
4171
|
// lint = verify references + discover + guidance count
|
|
3832
4172
|
const flags = args.slice(1).filter((a) => a.startsWith("--"));
|
|
@@ -3845,6 +4185,18 @@ async function main() {
|
|
|
3845
4185
|
handleRunScripts("eval", args, restArgs);
|
|
3846
4186
|
break;
|
|
3847
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
|
+
}
|
|
3848
4200
|
const dirs = restArgs.length > 0 ? restArgs : ["."];
|
|
3849
4201
|
const json = args.includes("--json");
|
|
3850
4202
|
// A single dir that's a marketplace (e.g. wshobson/agents' 80+ plugins
|
|
@@ -3928,30 +4280,18 @@ async function main() {
|
|
|
3928
4280
|
if (args.includes("--fail-on-widen") && diff.widened)
|
|
3929
4281
|
process.exitCode = 1;
|
|
3930
4282
|
}
|
|
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);
|
|
3931
4286
|
}
|
|
3932
4287
|
break;
|
|
3933
4288
|
}
|
|
3934
|
-
case "explain":
|
|
3935
|
-
handleExplain(restArgs, args);
|
|
3936
|
-
break;
|
|
3937
4289
|
case "scaffold-test":
|
|
3938
4290
|
handleScaffoldTest(restArgs, args);
|
|
3939
4291
|
break;
|
|
3940
|
-
case "measure":
|
|
3941
|
-
await handleMeasure(restArgs, args);
|
|
3942
|
-
break;
|
|
3943
4292
|
// --- 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]);
|
|
4293
|
+
case "generate":
|
|
4294
|
+
await handleGenerate(restArgs, args);
|
|
3955
4295
|
break;
|
|
3956
4296
|
// Hidden umbrella for runtime entrypoints emitted into hooks configs — never
|
|
3957
4297
|
// typed by a human. See handleHookRuntime / the cohesive-cli-surface rule.
|