vigiles 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * Commands:
7
7
  * vigiles init — scaffold a spec from scratch
8
8
  * vigiles compile — compile .spec.ts → .md with linter verification
9
- * vigiles audit — verify hashes, report coverage, detect duplicates
9
+ * vigiles lint — verify hashes, report coverage, detect duplicates
10
10
  * vigiles generate-types — emit .d.ts with types from project state
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -28,6 +28,7 @@ const proofs_js_1 = require("./core/proofs.js");
28
28
  const inline_js_1 = require("./core/inline.js");
29
29
  const frontmatter_js_1 = require("./core/frontmatter.js");
30
30
  const generate_schema_js_1 = require("./core/generate-schema.js");
31
+ const compose_js_1 = require("./core/compose.js");
31
32
  const compile_generator_js_1 = require("./core/compile-generator.js");
32
33
  const action_gate_js_1 = require("./action-gate.js");
33
34
  const agent_runtime_js_1 = require("./adapters/claude-code/agent-runtime.js");
@@ -51,7 +52,11 @@ const IGNORE_NODE_MODULES = ["node_modules/**"];
51
52
  function findSpecs(pattern) {
52
53
  const glob = pattern ?? "**/*.md.spec.ts";
53
54
  return (0, glob_1.globSync)(glob, {
54
- ignore: [...IGNORE_NODE_MODULES, "dist/**"],
55
+ // `dot: true` so specs that live in a sync tool's source slot (e.g.
56
+ // `.ruler/AGENTS.md.spec.ts`, the redirect target) are discovered by
57
+ // compile/audit/the recompile hook — not just root-level specs.
58
+ dot: true,
59
+ ignore: [...IGNORE_NODE_MODULES, "dist/**", ".git/**"],
55
60
  cwd: process.cwd(),
56
61
  });
57
62
  }
@@ -400,7 +405,7 @@ async function findDuplicateRules(threshold = 0.3, silent = false, scopeFiles) {
400
405
  const allSpecs = findSpecs();
401
406
  // If audit was invoked with explicit file arguments, only scan the specs
402
407
  // for those files — otherwise an unrelated duplicate elsewhere in the
403
- // repo would fail a targeted CI check (e.g. `vigiles audit path/foo.md`).
408
+ // repo would fail a targeted CI check (e.g. `vigiles lint path/foo.md`).
404
409
  //
405
410
  // Resolve each requested file to its real source spec by reading the
406
411
  // compiled-from header. Multi-target projects compile one spec to
@@ -989,6 +994,34 @@ function discover(silent = false) {
989
994
  }
990
995
  return { enabled: totalEnabled, documented: totalDocumented };
991
996
  }
997
+ /** This package's own version (from the installed package.json). */
998
+ function getVersion() {
999
+ try {
1000
+ // dist/cli.js → ../package.json (the package root).
1001
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.resolve)(__dirname, "..", "package.json"), "utf-8"));
1002
+ return pkg.version ?? "unknown";
1003
+ }
1004
+ catch {
1005
+ return "unknown";
1006
+ }
1007
+ }
1008
+ /** The dependency range to pin `vigiles` to (the running CLI's major, e.g.
1009
+ * `^3`). Falls back to `latest` for the unreleased dev placeholder version. */
1010
+ function vigilesDepSpec() {
1011
+ const major = parseInt(getVersion(), 10);
1012
+ return Number.isFinite(major) && major > 0 ? `^${String(major)}` : "latest";
1013
+ }
1014
+ /** True when the file's first line carries a vigiles integrity hash (i.e. it
1015
+ * is a compiled artifact we own, safe to overwrite — not hand-written prose). */
1016
+ function targetHasHash(absPath) {
1017
+ try {
1018
+ const first = (0, node_fs_1.readFileSync)(absPath, "utf-8").split("\n", 1)[0];
1019
+ return first.includes("vigiles:sha256");
1020
+ }
1021
+ catch {
1022
+ return false;
1023
+ }
1024
+ }
992
1025
  function init(args) {
993
1026
  const targetFlag = args.find((a) => a.startsWith("--target="));
994
1027
  const target = targetFlag ? targetFlag.split("=")[1] : "CLAUDE.md";
@@ -997,7 +1030,12 @@ function init(args) {
997
1030
  console.log(`${specPath} already exists.`);
998
1031
  return;
999
1032
  }
1000
- const targetLine = target !== "CLAUDE.md" ? `\n target: "${target}",` : "";
1033
+ // The compiled output is derived from the spec FILE path; the spec's `target`
1034
+ // field is the h1 + the name the compiler validates against, so it must be the
1035
+ // bare filename even when the spec lives in a subdir (e.g. a sync tool's
1036
+ // `.ruler/AGENTS.md.spec.ts` source slot → target "AGENTS.md").
1037
+ const targetName = (0, node_path_1.basename)(target);
1038
+ const targetLine = targetName !== "CLAUDE.md" ? `\n target: "${targetName}",` : "";
1001
1039
  const template = `import { claude, enforce, guidance } from "vigiles/spec";
1002
1040
 
1003
1041
  export default claude({${targetLine}
@@ -1031,19 +1069,22 @@ export default claude({${targetLine}
1031
1069
  },
1032
1070
  });
1033
1071
  `;
1034
- (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), specPath), template);
1072
+ const specAbs = (0, node_path_1.resolve)(process.cwd(), specPath);
1073
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(specAbs), { recursive: true });
1074
+ (0, node_fs_1.writeFileSync)(specAbs, template);
1035
1075
  console.log(`Created ${specPath} — edit it and run \`vigiles compile\`.`);
1036
1076
  }
1037
1077
  // ---------------------------------------------------------------------------
1038
1078
  // Setup wizard
1039
1079
  // ---------------------------------------------------------------------------
1040
1080
  /** Full GitHub Actions workflow that wires the production `zernie/vigiles@v1`
1041
- * Action (Pillar 1) and, when Pillar 2 is set up, a deterministic harness job. */
1081
+ * Action (lint pillar) and, when the test pillar is set up, a deterministic
1082
+ * harness job. */
1042
1083
  function vigilesWorkflow(plan) {
1043
1084
  const harness = plan.test
1044
1085
  ? `
1045
1086
  harness:
1046
- # Pillar 2 — run your *.harness.{mjs,ts} tests against the real agent CLI and
1087
+ # Test pillar — run your *.harness.{mjs,ts} tests against the real agent CLI and
1047
1088
  # a scripted mock model (deterministic, no API key). Drop this job if you only
1048
1089
  # author runHook unit tests, or keep it for the deterministic tier.
1049
1090
  runs-on: ubuntu-latest
@@ -1068,9 +1109,9 @@ permissions:
1068
1109
  pull-requests: write # for the sticky PR comment
1069
1110
 
1070
1111
  jobs:
1071
- verify:
1072
- # Pillar 1 — verify the references in your instruction files (composite Action
1073
- # over the published CLI). Posts a sticky PR comment + a \`valid\` output.
1112
+ lint:
1113
+ # Lint pillar — verify the references in your instruction files (composite
1114
+ # Action over the published CLI). Posts a sticky PR comment + a \`valid\` output.
1074
1115
  runs-on: ubuntu-latest
1075
1116
  steps:
1076
1117
  - uses: actions/checkout@v4
@@ -1080,18 +1121,46 @@ jobs:
1080
1121
  - uses: zernie/vigiles@v1
1081
1122
  ${harness}`;
1082
1123
  }
1083
- /** Create `.github/workflows/vigiles.yml` (or note it already exists). */
1124
+ /**
1125
+ * Detect a workflow that drives vigiles through an OLD API — a bare `npx vigiles`
1126
+ * (a no-op help screen in v2+) rather than the `zernie/vigiles@` Action or a real
1127
+ * subcommand (`audit`/`test`/…). Upgrading users whose workflow predates the
1128
+ * subcommand split silently lose CI validation, so we flag it loudly.
1129
+ */
1130
+ function workflowUsesStaleApi(content) {
1131
+ if (content.includes("zernie/vigiles@"))
1132
+ return false; // uses the Action — fine
1133
+ if (!/\bvigiles\b/.test(content))
1134
+ return false; // not a vigiles workflow
1135
+ const hasModernCmd = /vigiles\s+(lint|test|eval|compile|scan|generate-types|generate-schema|init)\b/.test(content);
1136
+ return !hasModernCmd;
1137
+ }
1138
+ /** Create `.github/workflows/vigiles.yml`. Returns the files it wrote (for the
1139
+ * commit hint). An existing workflow is never clobbered, but a STALE one (old
1140
+ * bare-`npx vigiles` API) is reported loudly instead of silently skipped. */
1084
1141
  function wireGha(plan) {
1085
1142
  const dir = (0, node_path_1.resolve)(process.cwd(), ".github", "workflows");
1086
1143
  const path = (0, node_path_1.resolve)(dir, "vigiles.yml");
1087
1144
  if ((0, node_fs_1.existsSync)(path)) {
1088
- console.log("✓ .github/workflows/vigiles.yml already exists");
1089
- return;
1145
+ const content = (0, node_fs_1.readFileSync)(path, "utf-8");
1146
+ if (workflowUsesStaleApi(content)) {
1147
+ console.log("⚠ .github/workflows/vigiles.yml is STALE — it runs a bare `npx vigiles`,\n" +
1148
+ " which is a no-op help screen now. Replace its run step with the Action +\n" +
1149
+ " the test job (CI is otherwise silently not validating anything):\n" +
1150
+ " - uses: zernie/vigiles@v1 # lint pillar — verify references\n" +
1151
+ " - run: npx vigiles test # pillar 2 — harness tests\n" +
1152
+ " Or delete the file and re-run `vigiles init` to regenerate it.");
1153
+ }
1154
+ else {
1155
+ console.log("✓ .github/workflows/vigiles.yml already exists (up to date)");
1156
+ }
1157
+ return [];
1090
1158
  }
1091
1159
  if (!(0, node_fs_1.existsSync)(dir))
1092
1160
  (0, node_fs_1.mkdirSync)(dir, { recursive: true });
1093
1161
  (0, node_fs_1.writeFileSync)(path, vigilesWorkflow(plan));
1094
1162
  console.log("✓ Created .github/workflows/vigiles.yml (uses zernie/vigiles@v1)");
1163
+ return [".github/workflows/vigiles.yml"];
1095
1164
  }
1096
1165
  const STARTER_HARNESS = `/**
1097
1166
  * Starter harness test (Pillar 2) — scaffolded by \`vigiles init\`.
@@ -1127,15 +1196,17 @@ assert.ok(!allowed.blocked, "guard should allow a safe command");
1127
1196
 
1128
1197
  console.log("\\u2713 hook blocks rm -rf / and allows safe commands");
1129
1198
  `;
1130
- /** Pillar 2 — scaffold a starter harness test the user adapts to their hooks. */
1199
+ /** Pillar 2 — scaffold a starter harness test the user adapts to their hooks.
1200
+ * Returns the files it wrote (for the commit hint). */
1131
1201
  function scaffoldPillar2() {
1132
1202
  const path = (0, node_path_1.resolve)(process.cwd(), "vigiles.harness.mjs");
1133
1203
  if ((0, node_fs_1.existsSync)(path)) {
1134
1204
  console.log("✓ vigiles.harness.mjs already exists");
1135
- return;
1205
+ return [];
1136
1206
  }
1137
1207
  (0, node_fs_1.writeFileSync)(path, STARTER_HARNESS);
1138
- console.log("✓ Scaffolded vigiles.harness.mjs — Pillar 2 starter (npm i -D vigiles && npx vigiles test)");
1208
+ console.log("✓ Scaffolded vigiles.harness.mjs — Pillar 2 starter (npx vigiles test)");
1209
+ return ["vigiles.harness.mjs"];
1139
1210
  }
1140
1211
  /** Interactive prompts (TTY only): which pillars, CI, plugin. */
1141
1212
  async function promptSetup() {
@@ -1151,12 +1222,12 @@ async function promptSetup() {
1151
1222
  });
1152
1223
  const isYes = (s) => /^y(es)?$/i.test(s);
1153
1224
  try {
1154
- const pillars = (await ask("Set up which pillars? [both/verify/test] (both): ", "both")).toLowerCase();
1225
+ const pillars = (await ask("Set up which pillars? [both/lint/test] (both): ", "both")).toLowerCase();
1155
1226
  const gha = isYes(await ask("Wire CI (GitHub Action)? [Y/n]: ", "y"));
1156
1227
  const plugin = isYes(await ask("Install the Claude Code plugin (hooks + skills)? [Y/n]: ", "y"));
1157
1228
  return {
1158
- verify: pillars !== "test",
1159
- test: pillars !== "verify",
1229
+ lint: pillars !== "test",
1230
+ test: pillars !== "lint" && pillars !== "verify",
1160
1231
  gha,
1161
1232
  plugin,
1162
1233
  };
@@ -1240,55 +1311,118 @@ function detectProject() {
1240
1311
  hasClaude: (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, ".claude")),
1241
1312
  };
1242
1313
  }
1243
- /** Pillar 1 specs + types + schema + compile. Returns the spec targets
1244
- * created/found (empty when every existing file needs migration instead). */
1245
- async function setupPillar1(detected, targetValue) {
1246
- // Determine targets
1247
- let targets;
1248
- if (targetValue) {
1249
- targets = [targetValue];
1250
- }
1251
- else {
1252
- const needsSpec = detected.instructionFiles.filter((f) => !f.hasSpec);
1253
- if (needsSpec.length > 0) {
1254
- for (const f of needsSpec) {
1255
- console.log(`Found ${f.path} without a spec. Migrate with the migrate-to-spec skill`);
1256
- console.log(` or create a blank spec: npx vigiles init --target=${f.path}\n`);
1257
- }
1258
- const hasAnySpec = detected.instructionFiles.some((f) => f.hasSpec);
1259
- if (!hasAnySpec &&
1260
- needsSpec.length === detected.instructionFiles.length) {
1261
- // ALL existing files need migration — don't create new specs.
1262
- console.log("Use the migration skill: npx skills add zernie/vigiles\n");
1263
- return [];
1264
- }
1265
- }
1266
- targets = ["CLAUDE.md"];
1267
- const hasAgentsMd = detected.instructionFiles.some((f) => f.path === "AGENTS.md");
1268
- const hasCodex = detected.agents.includes("Codex / GitHub Copilot") || hasAgentsMd;
1269
- if (hasCodex && !hasAgentsMd)
1270
- targets.push("AGENTS.md");
1314
+ /** The instruction-file targets Pillar 1 will create specs for the harness's
1315
+ * native instruction file (CLAUDE.md for Claude Code, AGENTS.md for Codex),
1316
+ * plus any existing instruction file that lacks a spec. */
1317
+ function determineTargets(detected, targetValue, harnesses) {
1318
+ if (targetValue)
1319
+ return [targetValue];
1320
+ const targets = [];
1321
+ if (harnesses.includes("claude"))
1322
+ targets.push("CLAUDE.md");
1323
+ if (harnesses.includes("codex"))
1324
+ targets.push("AGENTS.md");
1325
+ if (targets.length === 0)
1326
+ targets.push("CLAUDE.md");
1327
+ // Any existing instruction file without a spec also gets one.
1328
+ for (const f of detected.instructionFiles) {
1329
+ if (!f.hasSpec && !targets.includes(f.path))
1330
+ targets.push(f.path);
1271
1331
  }
1272
- // Create specs
1332
+ return targets;
1333
+ }
1334
+ /**
1335
+ * When CLAUDE.md and AGENTS.md are ONE artifact — a symlink, or kept
1336
+ * byte-identical by rulesync/Ruler — collapse them to a single canonical spec
1337
+ * target. Two specs would fight over one file and collide on the integrity hash
1338
+ * (see the "Compose With Sync Tools" rule). The mirror is distributed from the
1339
+ * canonical, not compiled separately.
1340
+ */
1341
+ function collapseMirroredTargets(targets, mirror) {
1342
+ if (!mirror)
1343
+ return targets;
1344
+ // The compile source slot: the real file for a symlink, else CLAUDE.md (the
1345
+ // one Claude Code reads natively; the sync tool fans out to AGENTS.md).
1346
+ const canonical = mirror.kind === "symlink"
1347
+ ? (mirror.realTarget ?? "CLAUDE.md")
1348
+ : "CLAUDE.md";
1349
+ const mirrored = mirror.files.find((f) => f !== canonical);
1350
+ if (!mirrored)
1351
+ return targets;
1352
+ if (!targets.includes(canonical) && !targets.includes(mirrored)) {
1353
+ return targets; // neither file is a target — nothing to collapse
1354
+ }
1355
+ const collapsed = targets.filter((t) => t !== mirrored);
1356
+ if (!collapsed.includes(canonical))
1357
+ collapsed.push(canonical);
1358
+ console.log(`Note: CLAUDE.md and AGENTS.md are one artifact (${mirror.kind}). ` +
1359
+ `Scaffolding a single spec for ${canonical}; ${mirrored} is its mirror ` +
1360
+ `(don't add a second spec — it would collide on the integrity hash).`);
1361
+ return collapsed;
1362
+ }
1363
+ /**
1364
+ * When a detected rule-sync tool (rulesync / Ruler) regenerates a file vigiles
1365
+ * would compile to, REDIRECT the compile target to the tool's source slot
1366
+ * (`.ruler/AGENTS.md`, `.rulesync/rules/vigiles.md`). vigiles compiles upstream;
1367
+ * the tool distributes to CLAUDE.md/AGENTS.md/Cursor/… — so the integrity hash
1368
+ * never collides with the tool's output (the "Compose With Sync Tools" rule).
1369
+ */
1370
+ function redirectSyncToolTargets(cwd, targets) {
1371
+ const collisions = (0, compose_js_1.composeCollisions)(cwd, targets);
1372
+ if (collisions.length === 0)
1373
+ return targets;
1374
+ const slotFor = new Map(collisions.map((c) => [c.target, c.redirectTo]));
1375
+ const out = [];
1376
+ for (const t of targets) {
1377
+ const redirected = slotFor.get(t) ?? t;
1378
+ if (!out.includes(redirected))
1379
+ out.push(redirected);
1380
+ }
1381
+ const tool = collisions[0].tool;
1382
+ const slots = [...new Set(collisions.map((c) => c.redirectTo))].join(", ");
1383
+ const from = [...slotFor.keys()].join(", ");
1384
+ console.log(`Note: ${tool} detected — scaffolding the spec to compile into its source ` +
1385
+ `slot (${slots}) instead of ${from}, so ${tool} distributes it without ` +
1386
+ `staling the integrity hash.`);
1387
+ return out;
1388
+ }
1389
+ /** Pillar 1 — specs + types + schema + compile. Scaffolds a spec for every
1390
+ * instruction file (so `--lint` always delivers a spec), but never compiles
1391
+ * OVER a hand-written file — that is left to the migrate-to-spec skill. */
1392
+ async function setupPillar1(detected, targetValue, harnesses) {
1393
+ const cwd = process.cwd();
1394
+ const written = [];
1395
+ const needsMigration = [];
1396
+ // An explicit --target is honoured as-is; otherwise collapse a CLAUDE.md⇄
1397
+ // AGENTS.md mirror (symlink or synced) to one canonical spec, then redirect
1398
+ // into a sync tool's source slot when one would own the output.
1399
+ const targets = targetValue
1400
+ ? determineTargets(detected, targetValue, harnesses)
1401
+ : redirectSyncToolTargets(cwd, collapseMirroredTargets(determineTargets(detected, targetValue, harnesses), (0, compose_js_1.detectInstructionMirror)(cwd)));
1402
+ // Create specs (blank). An existing hand-written target keeps its content —
1403
+ // we scaffold the spec but flag it for migration rather than clobbering it.
1273
1404
  for (const target of targets) {
1274
1405
  const specPath = `${target}.spec.ts`;
1275
- if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), specPath))) {
1406
+ const targetExists = (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, target));
1407
+ if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, specPath))) {
1276
1408
  console.log(`✓ ${specPath} already exists`);
1277
1409
  }
1278
- else if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), target))) {
1279
- console.log(`⚠ ${target} exists without spec — migrate with migrate-to-spec skill`);
1280
- }
1281
1410
  else {
1282
- init(["--target=" + target]);
1411
+ init(["--target=" + target]); // prints "Created …"
1412
+ written.push(specPath);
1413
+ }
1414
+ if (targetExists && !targetHasHash((0, node_path_1.resolve)(cwd, target))) {
1415
+ needsMigration.push(target);
1416
+ console.log(` ${target} already has content — port it into the spec with the migrate-to-spec skill, then \`vigiles compile\`.`);
1283
1417
  }
1284
1418
  }
1285
- // Generate types + schema
1419
+ // Generate types + schema.
1286
1420
  console.log("\nScanning linters and project files...");
1287
- const typesResult = (0, generate_types_js_1.generateTypes)({ basePath: process.cwd() });
1288
- const outDir = (0, node_path_1.resolve)(process.cwd(), ".vigiles");
1421
+ const typesResult = (0, generate_types_js_1.generateTypes)({ basePath: cwd });
1422
+ const outDir = (0, node_path_1.resolve)(cwd, ".vigiles");
1289
1423
  if (!(0, node_fs_1.existsSync)(outDir))
1290
1424
  (0, node_fs_1.mkdirSync)(outDir, { recursive: true });
1291
- (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), ".vigiles/generated.d.ts"), typesResult.dts);
1425
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(cwd, ".vigiles/generated.d.ts"), typesResult.dts);
1292
1426
  for (const l of typesResult.linters) {
1293
1427
  console.log(` ${l.linter}: ${String(l.rules.length)} rules`);
1294
1428
  }
@@ -1296,83 +1430,132 @@ async function setupPillar1(detected, targetValue) {
1296
1430
  console.log(` npm scripts: ${String(typesResult.scripts.length)}`);
1297
1431
  }
1298
1432
  console.log("✓ Generated .vigiles/generated.d.ts");
1433
+ written.push(".vigiles/generated.d.ts");
1299
1434
  const schemaResult = (0, generate_schema_js_1.generateSchema)({
1300
- basePath: process.cwd(),
1435
+ basePath: cwd,
1301
1436
  linters: (0, validate_js_1.loadConfig)().linters,
1302
1437
  });
1303
- (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), ".vigiles/schema.json"), schemaResult.json);
1438
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(cwd, ".vigiles/schema.json"), schemaResult.json);
1304
1439
  console.log("✓ Generated .vigiles/schema.json (YAML-LSP frontmatter schema)");
1305
- // Compile specs
1440
+ written.push(".vigiles/schema.json");
1441
+ // Compile — but only specs whose target is greenfield or already ours. A
1442
+ // freshly-scaffolded blank spec over an existing hand-written file is skipped
1443
+ // so we never overwrite the user's instructions with an empty compile.
1306
1444
  console.log("\nCompiling specs...");
1307
- const specs = findSpecs();
1445
+ const specs = findSpecs().filter((s) => {
1446
+ const tf = (0, node_path_1.resolve)(cwd, s.replace(/\.spec\.ts$/, ""));
1447
+ return !(0, node_fs_1.existsSync)(tf) || targetHasHash(tf);
1448
+ });
1308
1449
  if (specs.length > 0)
1309
1450
  await compile(specs, (0, validate_js_1.loadConfig)());
1310
- return targets;
1451
+ return { specTargets: targets, written, needsMigration };
1311
1452
  }
1312
- /** Install the Claude Code plugin (hooks + skills). Returns whether the
1313
- * fallback wrote `.claude/settings.json` (for the commit hint). */
1314
- function installPlugin() {
1453
+ /** Whether a harness binary (`claude`, `codex`) is on PATH. */
1454
+ function harnessBinaryPresent(bin) {
1315
1455
  try {
1316
1456
  const { execSync: exec } = require("node:child_process");
1317
- exec("npx skills add zernie/vigiles", {
1318
- cwd: process.cwd(),
1319
- stdio: ["pipe", "pipe", "pipe"],
1320
- timeout: 30000,
1321
- });
1322
- console.log("✓ Installed vigiles plugin (hooks + skills) via skills CLI");
1323
- return false;
1457
+ exec(`${bin} --version`, { stdio: "ignore", timeout: 10000 });
1458
+ return true;
1324
1459
  }
1325
1460
  catch {
1326
- // skills CLI not available — fall back to direct hook installation.
1327
- }
1328
- const settingsDir = (0, node_path_1.resolve)(process.cwd(), ".claude");
1329
- const settingsPath = (0, node_path_1.resolve)(settingsDir, "settings.json");
1330
- if (!(0, node_fs_1.existsSync)(settingsDir))
1331
- (0, node_fs_1.mkdirSync)(settingsDir, { recursive: true });
1332
- let settings = {};
1333
- if ((0, node_fs_1.existsSync)(settingsPath)) {
1334
- try {
1335
- settings = JSON.parse((0, node_fs_1.readFileSync)(settingsPath, "utf-8"));
1461
+ return false;
1462
+ }
1463
+ }
1464
+ /**
1465
+ * Install vigiles's skills/hooks for the chosen harness(es) via the per-harness
1466
+ * `planPluginInstall` decision Claude Code through the GLOBAL plugin
1467
+ * marketplace (nothing vendored into the repo), Codex via AGENTS.md-direct (no
1468
+ * global store). The decision is pure and unit-tested; this is the thin IO.
1469
+ */
1470
+ function installPlugins(harnesses) {
1471
+ const { execSync: exec } = require("node:child_process");
1472
+ const plans = (0, setup_plan_js_1.planPluginInstall)(harnesses, {
1473
+ hasClaude: harnesses.includes("claude") && harnessBinaryPresent("claude"),
1474
+ });
1475
+ for (const plan of plans) {
1476
+ console.log("");
1477
+ let installed = false;
1478
+ if (plan.commands.length > 0) {
1479
+ try {
1480
+ for (const cmd of plan.commands) {
1481
+ exec(cmd, { stdio: ["ignore", "pipe", "pipe"], timeout: 120000 });
1482
+ }
1483
+ console.log(plan.successMessage);
1484
+ installed = true;
1485
+ }
1486
+ catch {
1487
+ // Fall through to the manual instructions below.
1488
+ }
1336
1489
  }
1337
- catch {
1338
- // Ignore malformed settings.
1339
- }
1340
- }
1341
- if (!settings["hooks"])
1342
- settings["hooks"] = {};
1343
- const hooks = settings["hooks"];
1344
- const preCmd = `FILE=$(cat | jq -r '.tool_input.file_path // empty') && case "$FILE" in *.md) [ -f "$FILE" ] && head -1 "$FILE" | grep -q 'vigiles:sha256:' && { SPEC=$(head -1 "$FILE" | sed -n 's/.*compiled from \\(.*\\) -->/\\1/p'); echo "BLOCKED: Edit $SPEC instead." >&2; exit 2; } ;; esac; exit 0`;
1345
- const postCmd = `FILE=$(cat | jq -r '.tool_input.file_path // empty') && case "$(basename "$FILE")" in eslint.config.*|.eslintrc*|package.json|pyproject.toml|Cargo.toml) npx vigiles generate-types 2>&1 || true ;; esac && case "$FILE" in *.spec.ts) npx vigiles compile 2>&1 || true ;; esac`;
1346
- const existingStr = JSON.stringify(settings);
1347
- if (!existingStr.includes("vigiles:sha256")) {
1348
- const pre = (hooks["PreToolUse"] ?? []);
1349
- pre.push({ matcher: "Edit|Write", command: preCmd });
1350
- hooks["PreToolUse"] = pre;
1351
- }
1352
- if (!existingStr.includes("vigiles compile")) {
1353
- const post = (hooks["PostToolUse"] ?? []);
1354
- post.push({ matcher: "Edit|Write", command: postCmd });
1355
- hooks["PostToolUse"] = post;
1356
- }
1357
- (0, node_fs_1.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1358
- console.log("✓ Installed hooks in .claude/settings.json");
1359
- console.log(" (For skills like edit-spec and migrate-to-spec, also run: npx skills add zernie/vigiles)");
1360
- return true;
1490
+ if (!installed) {
1491
+ console.log(`Install vigiles for ${plan.harness}:`);
1492
+ for (const step of plan.manualSteps)
1493
+ console.log(` ${step}`);
1494
+ }
1495
+ for (const note of plan.notes)
1496
+ console.log(` ${note}`);
1497
+ }
1361
1498
  }
1362
- async function setup(args) {
1363
- const parsed = (0, setup_plan_js_1.parseSetupArgs)(args);
1364
- const strict = parsed.strict;
1365
- // Plan: defaults → flags → interactive prompts (only a human at a TTY).
1366
- let plan = (0, setup_plan_js_1.resolvePlan)(parsed);
1367
- if ((0, setup_plan_js_1.shouldPrompt)(parsed, process.stdin.isTTY ?? false)) {
1368
- plan = (0, setup_plan_js_1.resolvePlan)(parsed, await promptSetup());
1499
+ /** Add/upgrade `vigiles` in the project's `devDependencies` (and move it out of
1500
+ * `dependencies` if it's there). Returns the files it wrote (for the commit
1501
+ * hint). No-op in the vigiles repo itself and when there is no package.json. */
1502
+ function ensureVigilesDevDep() {
1503
+ const pkgPath = (0, node_path_1.resolve)(process.cwd(), "package.json");
1504
+ if (!(0, node_fs_1.existsSync)(pkgPath))
1505
+ return [];
1506
+ let pkg;
1507
+ try {
1508
+ pkg = JSON.parse((0, node_fs_1.readFileSync)(pkgPath, "utf-8"));
1369
1509
  }
1370
- const pillars = [plan.verify && "verify", plan.test && "test"]
1371
- .filter(Boolean)
1372
- .join(" + ");
1373
- console.log(`vigiles setup${strict ? " (strict)" : ""} — pillars: ${pillars}\n`);
1374
- // Detect project.
1375
- const detected = detectProject();
1510
+ catch {
1511
+ return [];
1512
+ }
1513
+ if (pkg.name === "vigiles")
1514
+ return []; // don't self-depend in this repo
1515
+ const spec = vigilesDepSpec();
1516
+ let changed = false;
1517
+ // Move a stale/misplaced runtime dependency (e.g. a `github:zernie/vigiles`
1518
+ // git pin, or vigiles sitting in `dependencies`) into devDependencies.
1519
+ if (pkg.dependencies && "vigiles" in pkg.dependencies) {
1520
+ delete pkg.dependencies.vigiles;
1521
+ changed = true;
1522
+ }
1523
+ const dev = (pkg.devDependencies ??= {});
1524
+ if (dev.vigiles !== spec) {
1525
+ dev.vigiles = spec;
1526
+ changed = true;
1527
+ }
1528
+ if (!changed)
1529
+ return [];
1530
+ (0, node_fs_1.writeFileSync)(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
1531
+ console.log(`✓ Set vigiles@${spec} in devDependencies — run \`npm install\` to fetch it`);
1532
+ return ["package.json"];
1533
+ }
1534
+ /** Which harnesses to set up: an explicit `--harness=` list, else auto-detected
1535
+ * from the repo (Claude Code / Codex), defaulting to Claude Code. */
1536
+ function resolveHarnesses(parsed, detected) {
1537
+ if (parsed.harness) {
1538
+ return parsed.harness
1539
+ .split(",")
1540
+ .map((s) => s.trim().toLowerCase())
1541
+ .filter(Boolean);
1542
+ }
1543
+ const set = new Set();
1544
+ if (detected.hasClaude ||
1545
+ detected.agents.includes("Claude Code") ||
1546
+ detected.instructionFiles.some((f) => f.path === "CLAUDE.md")) {
1547
+ set.add("claude");
1548
+ }
1549
+ if (detected.agents.includes("Codex / GitHub Copilot") ||
1550
+ detected.instructionFiles.some((f) => f.path === "AGENTS.md")) {
1551
+ set.add("codex");
1552
+ }
1553
+ if (set.size === 0)
1554
+ set.add("claude");
1555
+ return [...set];
1556
+ }
1557
+ /** Print the project-detection summary line(s). */
1558
+ function printDetection(detected, harnesses) {
1376
1559
  if (detected.agents.length > 0) {
1377
1560
  console.log(`Detected: ${detected.agents.join(", ")}`);
1378
1561
  }
@@ -1386,24 +1569,85 @@ async function setup(args) {
1386
1569
  if (f.isSymlink)
1387
1570
  console.log(`Note: ${f.path} is a symlink`);
1388
1571
  }
1389
- // Pillar 1 — verify instruction files.
1572
+ console.log(`Harness: ${harnesses.join(", ")}`);
1573
+ }
1574
+ /** Print the closing next-steps list + an honest commit hint (only files
1575
+ * actually written this run). */
1576
+ function printSetupSummary(opts) {
1577
+ const { plan, strict, targets, needsMigration, written } = opts;
1578
+ const specPathsList = targets.map((t) => `${t}.spec.ts`);
1579
+ console.log("\n---\nSetup complete.\n");
1580
+ const nextSteps = [];
1581
+ if (needsMigration.length > 0) {
1582
+ nextSteps.push(`Port ${needsMigration.join(", ")} into its spec with the migrate-to-spec skill, then \`npx vigiles compile\``);
1583
+ }
1584
+ else if (specPathsList.length > 0) {
1585
+ nextSteps.push(`Edit ${specPathsList.join(", ")} — add your conventions, then \`/strengthen\``);
1586
+ }
1587
+ if (plan.test) {
1588
+ nextSteps.push("Edit vigiles.harness.mjs to test a real hook, then `npx vigiles test`");
1589
+ }
1590
+ if (written.includes("package.json")) {
1591
+ nextSteps.push("Run `npm install` to fetch the vigiles dev dependency");
1592
+ }
1593
+ if (!strict) {
1594
+ nextSteps.push("When ready, enforce in CI: npx vigiles init --strict");
1595
+ }
1596
+ nextSteps.forEach((s, i) => {
1597
+ console.log(` ${String(i + 1)}. ${s}`);
1598
+ });
1599
+ // Only list files actually written this run (deduped, in a stable order).
1600
+ const files = [...new Set(written)];
1601
+ if (files.length > 0) {
1602
+ console.log(`\n Commit:\n git add ${files.join(" ")} && git commit -m "Add vigiles"`);
1603
+ }
1604
+ }
1605
+ async function setup(args) {
1606
+ const parsed = (0, setup_plan_js_1.parseSetupArgs)(args);
1607
+ const strict = parsed.strict;
1608
+ // Plan: defaults → flags → interactive prompts (only a human at a TTY).
1609
+ let plan = (0, setup_plan_js_1.resolvePlan)(parsed);
1610
+ if ((0, setup_plan_js_1.shouldPrompt)(parsed, process.stdin.isTTY ?? false)) {
1611
+ plan = (0, setup_plan_js_1.resolvePlan)(parsed, await promptSetup());
1612
+ }
1613
+ const pillars = [plan.lint && "lint", plan.test && "test"]
1614
+ .filter(Boolean)
1615
+ .join(" + ");
1616
+ console.log(`vigiles setup${strict ? " (strict)" : ""} — pillars: ${pillars}\n`);
1617
+ // Detect project.
1618
+ const detected = detectProject();
1619
+ const harnesses = resolveHarnesses(parsed, detected);
1620
+ printDetection(detected, harnesses);
1621
+ // Files actually written, accumulated for an honest commit hint.
1622
+ const written = [];
1623
+ // Lint pillar — verify instruction-file references.
1390
1624
  let targets = [];
1391
- if (plan.verify) {
1625
+ let needsMigration = [];
1626
+ if (plan.lint) {
1392
1627
  console.log("");
1393
- targets = await setupPillar1(detected, parsed.target);
1628
+ const p1 = await setupPillar1(detected, parsed.target, harnesses);
1629
+ targets = p1.specTargets;
1630
+ needsMigration = p1.needsMigration;
1631
+ written.push(...p1.written);
1394
1632
  }
1395
- // Pillar 2 — test the harness.
1633
+ // Test pillar — test the harness.
1396
1634
  if (plan.test) {
1397
1635
  console.log("");
1398
- scaffoldPillar2();
1636
+ written.push(...scaffoldPillar2());
1637
+ }
1638
+ // Add/upgrade the vigiles dev dependency (both pillars import from it).
1639
+ if (plan.lint || plan.test) {
1640
+ written.push(...ensureVigilesDevDep());
1399
1641
  }
1400
1642
  // CI — the production Action (+ a harness job when Pillar 2 is on).
1401
1643
  if (plan.gha) {
1402
1644
  console.log("");
1403
- wireGha(plan);
1645
+ written.push(...wireGha(plan));
1646
+ }
1647
+ // Plugin/skill install — per-harness (Claude marketplace / Codex direct).
1648
+ if (plan.plugin) {
1649
+ installPlugins(harnesses);
1404
1650
  }
1405
- // Claude Code plugin (hooks + skills).
1406
- const wroteSettings = plan.plugin ? installPlugin() : false;
1407
1651
  // Agent-specific guidance.
1408
1652
  if (targets.includes("AGENTS.md")) {
1409
1653
  console.log("\n Codex / Copilot reads AGENTS.md directly — no hooks needed.");
@@ -1419,36 +1663,10 @@ async function setup(args) {
1419
1663
  if (!(0, node_fs_1.existsSync)(configPath)) {
1420
1664
  (0, node_fs_1.writeFileSync)(configPath, JSON.stringify({ rules: { "require-spec": "error", "require-skill-spec": "error" } }, null, 2) + "\n");
1421
1665
  console.log("✓ Created .vigilesrc.json with strict rules");
1666
+ written.push(".vigilesrc.json");
1422
1667
  }
1423
1668
  }
1424
- // Summary.
1425
- const specPathsList = targets.map((t) => `${t}.spec.ts`);
1426
- console.log("\n---\nSetup complete.\n");
1427
- const nextSteps = [];
1428
- if (specPathsList.length > 0) {
1429
- nextSteps.push(`Edit ${specPathsList.join(", ")} — add your conventions, then \`/strengthen\``);
1430
- }
1431
- if (plan.test) {
1432
- nextSteps.push("Edit vigiles.harness.mjs to test a real hook, then `npx vigiles test`");
1433
- }
1434
- if (!strict) {
1435
- nextSteps.push("When ready, enforce in CI: npx vigiles init --strict");
1436
- }
1437
- nextSteps.forEach((s, i) => {
1438
- console.log(` ${String(i + 1)}. ${s}`);
1439
- });
1440
- const files = [
1441
- ...targets,
1442
- ...specPathsList,
1443
- ...(plan.verify ? [".vigiles/generated.d.ts", ".vigiles/schema.json"] : []),
1444
- ...(plan.test ? ["vigiles.harness.mjs"] : []),
1445
- ...(plan.gha ? [".github/workflows/vigiles.yml"] : []),
1446
- ...(wroteSettings ? [".claude/settings.json"] : []),
1447
- ...(strict ? [".vigilesrc.json"] : []),
1448
- ];
1449
- if (files.length > 0) {
1450
- console.log(`\n Commit:\n git add ${files.join(" ")} && git commit -m "Add vigiles"`);
1451
- }
1669
+ printSetupSummary({ plan, strict, targets, needsMigration, written });
1452
1670
  }
1453
1671
  // ---------------------------------------------------------------------------
1454
1672
  // Strengthen: guidance() → enforce() suggestions
@@ -1716,9 +1934,9 @@ function printUsage(command) {
1716
1934
  console.log("vigiles — compile typed specs to instruction files");
1717
1935
  console.log("");
1718
1936
  console.log("Commands:");
1719
- console.log(" vigiles init [flags] Setup project (--target=X.md, --strict, --no-gha)");
1937
+ console.log(" vigiles init [flags] Setup project (--lint, --test, --harness=, --strict, --no-gha)");
1720
1938
  console.log(" vigiles compile [files...] Compile .spec.ts → .md");
1721
- console.log(" vigiles audit [files...] Verify, find gaps, suggest improvements");
1939
+ console.log(" vigiles lint [files...] Verify references, find gaps in instruction files");
1722
1940
  console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
1723
1941
  console.log(" vigiles eval [files...] Run *.eval.mjs real-model harness evals (--trials=N)");
1724
1942
  console.log("");
@@ -1732,6 +1950,7 @@ function printUsage(command) {
1732
1950
  console.log(" vigiles generate-types --check Verify .d.ts is up to date");
1733
1951
  console.log(" vigiles generate-schema [out] Emit JSON Schema for vigiles: frontmatter");
1734
1952
  console.log(" vigiles generate-schema --check Verify schema.json is up to date");
1953
+ console.log(" vigiles --version Print the version number");
1735
1954
  if (command && command !== "--help") {
1736
1955
  console.log(`\nUnknown command: "${command}"`);
1737
1956
  process.exit(1);
@@ -2037,6 +2256,12 @@ function refsHookCommand() {
2037
2256
  async function main() {
2038
2257
  const args = process.argv.slice(2);
2039
2258
  const command = args[0];
2259
+ // `--version` / `-v` / `version` prints the version number, not the help
2260
+ // banner — so `npx vigiles --version` reports e.g. `3.0.0`.
2261
+ if (command === "--version" || command === "-v" || command === "version") {
2262
+ console.log(getVersion());
2263
+ return;
2264
+ }
2040
2265
  const restArgs = args.slice(1).filter((a) => !a.startsWith("--"));
2041
2266
  // Shared flags (--max-rules, --catalog-only) override the loaded config so
2042
2267
  // every GitHub Action input maps to a real CLI flag. See src/cli-flags.ts.
@@ -2075,8 +2300,8 @@ async function main() {
2075
2300
  }
2076
2301
  break;
2077
2302
  }
2078
- case "audit": {
2079
- // audit = verify + discover + guidance count
2303
+ case "lint": {
2304
+ // lint = verify references + discover + guidance count
2080
2305
  const flags = args.slice(1).filter((a) => a.startsWith("--"));
2081
2306
  const report = await audit(restArgs, flags, config);
2082
2307
  annotateAuditForGitHub(report, flags);