vigiles 4.0.1 → 4.1.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.
Files changed (58) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +1 -1
  3. package/dist/adapter-conformance.js +1 -1
  4. package/dist/adapter-registry.d.ts +45 -1
  5. package/dist/adapter-registry.js +78 -3
  6. package/dist/check.d.ts +132 -0
  7. package/dist/check.js +318 -0
  8. package/dist/cli.js +255 -73
  9. package/dist/core/compile.d.ts +1 -1
  10. package/dist/core/compile.js +1 -1
  11. package/dist/core/compose.d.ts +1 -1
  12. package/dist/core/compose.js +1 -1
  13. package/dist/core/generate-schema.d.ts +1 -1
  14. package/dist/core/generate-schema.js +4 -4
  15. package/dist/core/linters.js +2 -2
  16. package/dist/core/orphans.js +57 -14
  17. package/dist/core/proofs.js +1 -1
  18. package/dist/core/refs.d.ts +1 -1
  19. package/dist/core/refs.js +2 -2
  20. package/dist/core/sidecar.d.ts +1 -1
  21. package/dist/core/sidecar.js +1 -1
  22. package/dist/core/spec.d.ts +1 -1
  23. package/dist/core/spec.js +1 -1
  24. package/dist/core/types.d.ts +11 -1
  25. package/dist/core/validate.js +2 -2
  26. package/dist/e2e.d.ts +10 -13
  27. package/dist/e2e.js +10 -17
  28. package/dist/eval.d.ts +217 -2
  29. package/dist/eval.js +428 -18
  30. package/dist/harness-assert.d.ts +3 -0
  31. package/dist/harness-assert.js +16 -0
  32. package/dist/harness-test.d.ts +46 -0
  33. package/dist/harness-test.js +102 -0
  34. package/dist/integration.d.ts +8 -0
  35. package/dist/integration.js +10 -0
  36. package/dist/jest.d.ts +3 -1
  37. package/dist/jest.js +3 -2
  38. package/dist/run-hook.d.ts +22 -0
  39. package/dist/run-hook.js +28 -0
  40. package/dist/scan.d.ts +1 -1
  41. package/dist/scan.js +1 -1
  42. package/dist/setup-plan.d.ts +16 -1
  43. package/dist/setup-plan.js +38 -1
  44. package/dist/skill-harness.d.ts +25 -0
  45. package/dist/skill-harness.js +40 -0
  46. package/dist/test-coverage.js +8 -1
  47. package/dist/testing.d.ts +2 -0
  48. package/dist/testing.js +7 -0
  49. package/dist/unit.d.ts +4 -2
  50. package/dist/unit.js +7 -1
  51. package/dist/vitest.d.mts +3 -1
  52. package/hooks/refs-nudge.sh +1 -1
  53. package/package.json +3 -2
  54. package/skills/edit-spec/SKILL.md +21 -10
  55. package/skills/linter-docs/SKILL.md +23 -0
  56. package/skills/migrate-to-spec/SKILL.md +1 -1
  57. package/skills/strengthen/SKILL.md +1 -2
  58. package/skills/generate-rule/SKILL.md +0 -64
package/dist/cli.js CHANGED
@@ -21,9 +21,9 @@ const types_js_1 = require("./core/types.js");
21
21
  const test_coverage_js_1 = require("./test-coverage.js");
22
22
  const scan_js_1 = require("./scan.js");
23
23
  const adapter_registry_js_1 = require("./adapter-registry.js");
24
+ const skill_harness_js_1 = require("./skill-harness.js");
24
25
  const leaderboard_js_1 = require("./leaderboard.js");
25
26
  const compile_js_1 = require("./core/compile.js");
26
- const dialect_js_1 = require("./adapters/claude-code/dialect.js");
27
27
  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");
@@ -54,7 +54,7 @@ function findSpecs(pattern) {
54
54
  return (0, glob_1.globSync)(glob, {
55
55
  // `dot: true` so specs that live in a sync tool's source slot (e.g.
56
56
  // `.ruler/AGENTS.md.spec.ts`, the redirect target) are discovered by
57
- // compile/audit/the recompile hook — not just root-level specs.
57
+ // compile/lint/the recompile hook — not just root-level specs.
58
58
  dot: true,
59
59
  ignore: [...IGNORE_NODE_MODULES, "dist/**", ".git/**"],
60
60
  cwd: process.cwd(),
@@ -140,12 +140,12 @@ function compileGeneratorSkillToFile(specPath, source) {
140
140
  return false;
141
141
  }
142
142
  /** Compile a ClaudeSpec → its primary + any additional targets. */
143
- function compileClaudeToFile(spec, specPath, config) {
143
+ function compileClaudeToFile(spec, specPath, config, dialect) {
144
144
  const basePath = process.cwd();
145
145
  const { markdown, errors, linterResults, targets } = (0, compile_js_1.compileClaude)(spec, {
146
146
  basePath,
147
147
  specFile: specPath,
148
- dialect: dialect_js_1.claudeCodeDialect,
148
+ dialect,
149
149
  maxRules: config.maxRules,
150
150
  maxTokens: config.maxTokens,
151
151
  maxSectionLines: config.maxSectionLines,
@@ -175,15 +175,54 @@ function compileClaudeToFile(spec, specPath, config) {
175
175
  console.log(` ${String(Object.keys(spec.rules).length)} rules (${String(linterCount)} linter-verified)`);
176
176
  return true;
177
177
  }
178
+ /**
179
+ * Branch 3 of the mirror story (research/multi-harness-compile.md): when a repo
180
+ * declares ≥2 harnesses and nothing else fans out the instruction file, write a
181
+ * byte-identical copy to each other harness's instruction file (e.g. CLAUDE.md →
182
+ * AGENTS.md). A copy — not a symlink — because it works everywhere and carries
183
+ * the source's embedded integrity hash by construction, so a hand-edit of the
184
+ * mirror trips the existing `integrity` check. Never fights a sync tool or
185
+ * clobbers a target that owns its own spec.
186
+ */
187
+ function writeInstructionMirrors(primaryOutput, harnesses) {
188
+ if (harnesses.length < 2)
189
+ return;
190
+ const cwd = process.cwd();
191
+ // A sync tool (Ruler/rulesync) owns fan-out — don't fight it.
192
+ if ((0, compose_js_1.detectSyncTools)(cwd).length > 0)
193
+ return;
194
+ const primaryName = (0, node_path_1.basename)(primaryOutput);
195
+ const primaryAbs = (0, node_path_1.resolve)(cwd, primaryOutput);
196
+ if (!(0, node_fs_1.existsSync)(primaryAbs))
197
+ return;
198
+ const content = (0, node_fs_1.readFileSync)(primaryAbs, "utf-8");
199
+ for (const name of harnesses) {
200
+ const adapter = (0, adapter_registry_js_1.getAdapter)(name);
201
+ if (!adapter)
202
+ continue;
203
+ const target = adapter.layout.instructionFile;
204
+ if (target === primaryName)
205
+ continue; // the file we just compiled
206
+ // Never clobber a target that has its own spec (a genuinely separate file).
207
+ if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, `${target}.spec.ts`)))
208
+ continue;
209
+ const targetAbs = (0, node_path_1.resolve)(cwd, target);
210
+ if ((0, node_fs_1.existsSync)(targetAbs) && (0, node_fs_1.readFileSync)(targetAbs, "utf-8") === content) {
211
+ continue; // already byte-identical
212
+ }
213
+ (0, node_fs_1.writeFileSync)(targetAbs, content);
214
+ console.log(` ↳ mirrored ${primaryName} → ${target} (byte-identical)`);
215
+ }
216
+ }
178
217
  /** Compile a declarative SkillSpec → SKILL.md. */
179
- function compileSkillToFile(spec, specPath) {
218
+ function compileSkillToFile(spec, specPath, dialect) {
180
219
  const outputPath = specPath.replace(/\.spec\.ts$/, "");
181
220
  const { markdown, errors } = (0, compile_js_1.compileSkill)(spec, {
182
221
  basePath: process.cwd(),
183
222
  specFile: specPath,
184
- // Pick the SKILL.md frontmatter profile from the detected harness — a Codex
223
+ // The SKILL.md frontmatter profile comes from the resolved harness — a Codex
185
224
  // repo gets a minimal (name + description) SKILL.md; CC gets the full set.
186
- dialect: (0, adapter_registry_js_1.detectAdapter)(process.cwd()).dialect,
225
+ dialect,
187
226
  });
188
227
  (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), markdown);
189
228
  if (errors.length === 0) {
@@ -195,12 +234,12 @@ function compileSkillToFile(spec, specPath) {
195
234
  return false;
196
235
  }
197
236
  /** Compile a subagent spec → agents/<name>.md (with its result-contract section). */
198
- function compileAgentToFile(spec, specPath) {
237
+ function compileAgentToFile(spec, specPath, dialect) {
199
238
  const outputPath = specPath.replace(/\.spec\.ts$/, "");
200
239
  const { markdown, errors } = (0, compile_js_1.compileAgent)(spec, {
201
240
  basePath: process.cwd(),
202
241
  specFile: specPath,
203
- dialect: (0, adapter_registry_js_1.detectAdapter)(process.cwd()).dialect,
242
+ dialect,
204
243
  });
205
244
  (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), markdown);
206
245
  if (errors.length === 0) {
@@ -241,8 +280,21 @@ async function collectAgentNames() {
241
280
  }
242
281
  return names;
243
282
  }
244
- async function compile(specPaths, config) {
283
+ async function compile(specPaths, config, opts = {}) {
245
284
  let allValid = true;
285
+ // Parse the declared harness set ONCE (alias-normalized) and feed both the
286
+ // dialect pick and the mirror from it — no re-parsing, no cwd-sniffing in the
287
+ // helpers. A loud notice (never a silent guess) on a multi-harness or
288
+ // ambiguous-detection pick.
289
+ const declaredHarnesses = (0, adapter_registry_js_1.normalizeHarnessList)(config.harness);
290
+ const selection = (0, adapter_registry_js_1.resolveHarnessSelection)({
291
+ root: process.cwd(),
292
+ flag: opts.harnessFlag,
293
+ configHarness: declaredHarnesses,
294
+ });
295
+ if (selection.kind === "notice")
296
+ console.log(`⚠ ${selection.notice}`);
297
+ const dialect = selection.adapter.dialect;
246
298
  // Resolved lazily on the first railway spec — every delegate() target is
247
299
  // checked against the agents defined anywhere in the project.
248
300
  let knownAgents = null;
@@ -262,15 +314,36 @@ async function compile(specPaths, config) {
262
314
  continue;
263
315
  }
264
316
  if (spec._specType === "claude") {
265
- if (!compileClaudeToFile(spec, specPath, config))
317
+ // Spec-target disambiguation: a CLAUDE.md.spec.ts is a claude-code file, an
318
+ // AGENTS.md.spec.ts a codex one — the strongest dialect signal for THIS
319
+ // spec. The flag still overrides; absent one, the spec's own target wins
320
+ // over config/detect. (Skill/agent targets don't name a harness, so they
321
+ // keep the run-level dialect.)
322
+ const targetFile = (0, node_path_1.basename)(specPath).replace(/\.spec\.ts$/, "");
323
+ const specDialect = opts.harnessFlag === undefined
324
+ ? ((0, adapter_registry_js_1.adapterForInstructionFile)(targetFile)?.dialect ?? dialect)
325
+ : dialect;
326
+ if (compileClaudeToFile(spec, specPath, config, specDialect)) {
327
+ writeInstructionMirrors(specPath.replace(/\.spec\.ts$/, ""), declaredHarnesses);
328
+ }
329
+ else {
266
330
  allValid = false;
331
+ }
267
332
  }
268
333
  else if (spec._specType === "skill") {
269
- if (!compileSkillToFile(spec, specPath))
334
+ // Cross-harness verify: flag CC-only frontmatter a declared minimal-profile
335
+ // harness (Codex/OpenCode) would silently drop.
336
+ const forHarnesses = declaredHarnesses.length > 0
337
+ ? declaredHarnesses
338
+ : [selection.adapter.name];
339
+ for (const w of (0, skill_harness_js_1.skillFrontmatterDropWarnings)(spec, forHarnesses)) {
340
+ console.log(`⚠ ${w}`);
341
+ }
342
+ if (!compileSkillToFile(spec, specPath, dialect))
270
343
  allValid = false;
271
344
  }
272
345
  else if (spec._specType === "agent") {
273
- if (!compileAgentToFile(spec, specPath))
346
+ if (!compileAgentToFile(spec, specPath, dialect))
274
347
  allValid = false;
275
348
  }
276
349
  else if (spec._specType === "railway") {
@@ -310,7 +383,7 @@ function verifyHashes(filePaths, silent = false) {
310
383
  const fullPath = (0, node_path_1.resolve)(process.cwd(), filePath);
311
384
  // If the file doesn't exist at all (typo, deleted), that's an error —
312
385
  // not a "no hash" informational message. Without this check, a scoped
313
- // audit like `vigiles audit typo.md` would silently exit clean.
386
+ // lint like `vigiles lint typo.md` would silently exit clean.
314
387
  if (!(0, node_fs_1.existsSync)(fullPath)) {
315
388
  log(`\n✗ ${filePath} — file not found`);
316
389
  if (!silent) {
@@ -387,7 +460,7 @@ function check(filePaths, silent = false) {
387
460
  hashErrors: hashes.errorCount,
388
461
  // `validateSpecs` only returns a boolean today, so we collapse
389
462
  // failures to 1 until it starts reporting counts. Kept in its own
390
- // counter so audit's "stale hash — run vigiles compile" remediation
463
+ // counter so lint's "stale hash — run vigiles compile" remediation
391
464
  // doesn't misreport a require-spec / other validation failure.
392
465
  validationErrors: specsValid ? 0 : 1,
393
466
  };
@@ -403,7 +476,7 @@ async function findDuplicateRules(threshold = 0.3, silent = false, scopeFiles) {
403
476
  console.log(msg);
404
477
  };
405
478
  const allSpecs = findSpecs();
406
- // If audit was invoked with explicit file arguments, only scan the specs
479
+ // If lint was invoked with explicit file arguments, only scan the specs
407
480
  // for those files — otherwise an unrelated duplicate elsewhere in the
408
481
  // repo would fail a targeted CI check (e.g. `vigiles lint path/foo.md`).
409
482
  //
@@ -551,7 +624,7 @@ async function verifyMarkdownMcpRefs(files, silent) {
551
624
  return errors;
552
625
  }
553
626
  /** Exit codes: 0 clean, 1 warnings only, 2 hard errors. */
554
- function auditExitCode(report) {
627
+ function lintExitCode(report) {
555
628
  if (report.hashErrors > 0 ||
556
629
  report.validationErrors > 0 ||
557
630
  report.inlineErrors > 0 ||
@@ -718,7 +791,7 @@ function verifyFrontmatterRules(filePath, silent, exclude, linterOptions) {
718
791
  *
719
792
  * Spec mode is the source of truth when it exists, so a literal
720
793
  * `<!-- vigiles:enforce ... -->` snippet that survived into compiled
721
- * markdown (or an example in a spec-managed file) must not trip audit. A
794
+ * markdown (or an example in a spec-managed file) must not trip lint. A
722
795
  * file is spec-managed iff it has a sibling `<file>.spec.ts` OR its own
723
796
  * `<!-- vigiles:sha256:... compiled from <spec> -->` header. A rule
724
797
  * declared both inline and in frontmatter is verified once (inline wins as
@@ -768,14 +841,14 @@ function verifyMarkdownModeRules(files, silent, config) {
768
841
  return totals;
769
842
  }
770
843
  /**
771
- * Unified audit command: verify hashes, report coverage gaps, detect duplicates,
844
+ * Unified lint command: verify hashes, report coverage gaps, detect duplicates,
772
845
  * suggest improvements.
773
846
  *
774
847
  * Flags:
775
848
  * --summary Print a single-line summary (for SessionStart hooks)
776
849
  * --json Print structured JSON report (for CI integration)
777
850
  */
778
- async function audit(restArgs, flags, config) {
851
+ async function runLint(restArgs, flags, config) {
779
852
  const summary = flags.includes("--summary");
780
853
  const json = flags.includes("--json");
781
854
  const silent = summary || json;
@@ -801,7 +874,7 @@ async function audit(restArgs, flags, config) {
801
874
  console.log("\nLinter rule coverage:\n");
802
875
  const coverage = discover(silent);
803
876
  // 3. Duplicate rule detection (NCD). Scope to the requested files when
804
- // audit was invoked with explicit paths, so targeted CI checks don't
877
+ // lint was invoked with explicit paths, so targeted CI checks don't
805
878
  // fail on unrelated duplicates elsewhere in the repo.
806
879
  if (!silent)
807
880
  console.log("\nDuplicate rule detection:\n");
@@ -878,15 +951,15 @@ async function audit(restArgs, flags, config) {
878
951
  files,
879
952
  };
880
953
  if (summary) {
881
- printAuditSummary(report);
954
+ printLintSummary(report);
882
955
  }
883
956
  else if (json) {
884
957
  console.log(JSON.stringify(report, null, 2));
885
958
  }
886
959
  return report;
887
960
  }
888
- /** Single-line audit summary for SessionStart hooks — minimal token cost. */
889
- function printAuditSummary(report) {
961
+ /** Single-line lint summary for SessionStart hooks — minimal token cost. */
962
+ function printLintSummary(report) {
890
963
  const parts = [];
891
964
  if (report.hashErrors > 0)
892
965
  parts.push(`${String(report.hashErrors)} stale`);
@@ -1036,7 +1109,14 @@ function init(args) {
1036
1109
  // `.ruler/AGENTS.md.spec.ts` source slot → target "AGENTS.md").
1037
1110
  const targetName = (0, node_path_1.basename)(target);
1038
1111
  const targetLine = targetName !== "CLAUDE.md" ? `\n target: "${targetName}",` : "";
1039
- const template = `import { claude, enforce, guidance } from "vigiles/spec";
1112
+ // Import ONLY what the scaffold uses (`claude`) a strict ESLint with
1113
+ // \`no-unused-vars\` + \`--max-warnings=0\` (common in CI) would otherwise fail
1114
+ // the moment this is committed, because the enforce()/guidance() examples
1115
+ // below are commented out. The commented import shows what to add when you
1116
+ // write a real rule.
1117
+ const template = `import { claude } from "vigiles/spec";
1118
+ // When you add rules below, import the builders you use, e.g.:
1119
+ // import { claude, enforce, guidance } from "vigiles/spec";
1040
1120
 
1041
1121
  export default claude({${targetLine}
1042
1122
  sections: {
@@ -1124,7 +1204,7 @@ ${harness}`;
1124
1204
  /**
1125
1205
  * Detect a workflow that drives vigiles through an OLD API — a bare `npx vigiles`
1126
1206
  * (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
1207
+ * subcommand (`lint`/`test`/…). Upgrading users whose workflow predates the
1128
1208
  * subcommand split silently lose CI validation, so we flag it loudly.
1129
1209
  */
1130
1210
  function workflowUsesStaleApi(content) {
@@ -1135,24 +1215,69 @@ function workflowUsesStaleApi(content) {
1135
1215
  const hasModernCmd = /vigiles\s+(lint|test|eval|compile|scan|generate-types|generate-schema|init)\b/.test(content);
1136
1216
  return !hasModernCmd;
1137
1217
  }
1218
+ /**
1219
+ * Subcommands removed/renamed across majors → the replacement to suggest. A
1220
+ * workflow that still calls one is a silently-broken CI step (the removed
1221
+ * subcommand exits non-zero / no-ops), and this stays true even when the file
1222
+ * ALSO uses the Action or a modern command — so it is checked independently of
1223
+ * the bare-API heuristic above, which an Action reference short-circuits.
1224
+ */
1225
+ const REMOVED_SUBCOMMANDS = {
1226
+ audit: "lint", // v3 → v4 rename
1227
+ };
1228
+ /** The first removed/renamed `vigiles <sub>` a workflow still calls, if any. */
1229
+ function workflowRemovedSubcommand(content) {
1230
+ for (const [sub, replacement] of Object.entries(REMOVED_SUBCOMMANDS)) {
1231
+ if (new RegExp(`vigiles\\s+${sub}\\b`).test(content))
1232
+ return { sub, replacement };
1233
+ }
1234
+ return null;
1235
+ }
1236
+ /** Rewrite removed/renamed `vigiles <sub>` invocations in place (audit → lint).
1237
+ * Surgical — preserves the rest of the user's workflow. */
1238
+ function rewriteRemovedSubcommands(content) {
1239
+ let out = content;
1240
+ for (const [sub, replacement] of Object.entries(REMOVED_SUBCOMMANDS)) {
1241
+ out = out.replace(new RegExp(`(vigiles\\s+)${sub}\\b`, "g"), `$1${replacement}`);
1242
+ }
1243
+ return out;
1244
+ }
1138
1245
  /** 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. */
1246
+ * commit hint). An existing workflow is never clobbered unless `--force`, but a
1247
+ * STALE one (old bare-`npx vigiles` API, or a removed subcommand) is reported
1248
+ * loudly instead of silently skipped — and rewritten in place with `--force`. */
1141
1249
  function wireGha(plan) {
1142
1250
  const dir = (0, node_path_1.resolve)(process.cwd(), ".github", "workflows");
1143
1251
  const path = (0, node_path_1.resolve)(dir, "vigiles.yml");
1252
+ const rel = ".github/workflows/vigiles.yml";
1144
1253
  if ((0, node_fs_1.existsSync)(path)) {
1145
1254
  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.");
1255
+ const removed = workflowRemovedSubcommand(content);
1256
+ if (removed) {
1257
+ if (plan.force) {
1258
+ (0, node_fs_1.writeFileSync)(path, rewriteRemovedSubcommands(content));
1259
+ console.log(`✓ Rewrote ${rel} (vigiles ${removed.sub} ${removed.replacement})`);
1260
+ return [rel];
1261
+ }
1262
+ console.log(`⚠ ${rel} is STALE — it runs \`vigiles ${removed.sub}\`,\n` +
1263
+ ` which was removed/renamed (now \`vigiles ${removed.replacement}\`). That CI\n` +
1264
+ " step is silently broken. Fix it:\n" +
1265
+ ` - re-run \`vigiles init --force\` to rewrite it in place (vigiles ${removed.sub} → ${removed.replacement}), or\n` +
1266
+ " - switch the run step to `uses: zernie/vigiles@v1` (the composite Action).");
1267
+ }
1268
+ else if (workflowUsesStaleApi(content)) {
1269
+ if (plan.force) {
1270
+ (0, node_fs_1.writeFileSync)(path, vigilesWorkflow(plan));
1271
+ console.log(`✓ Regenerated ${rel} (was a stale bare \`npx vigiles\`)`);
1272
+ return [rel];
1273
+ }
1274
+ console.log(`⚠ ${rel} is STALE — it runs a bare \`npx vigiles\`,\n` +
1275
+ " which is a no-op help screen now. CI is silently not validating anything. Fix it:\n" +
1276
+ " - re-run `vigiles init --force` to regenerate the workflow, or\n" +
1277
+ " - replace its run step with `uses: zernie/vigiles@v1` + `run: npx vigiles test`.");
1153
1278
  }
1154
1279
  else {
1155
- console.log("✓ .github/workflows/vigiles.yml already exists (up to date)");
1280
+ console.log(`✓ ${rel} already exists (up to date)`);
1156
1281
  }
1157
1282
  return [];
1158
1283
  }
@@ -1461,6 +1586,51 @@ function harnessBinaryPresent(bin) {
1461
1586
  return false;
1462
1587
  }
1463
1588
  }
1589
+ /** Run a plan's auto-install commands; classify the result. */
1590
+ function runInstall(plan, exec) {
1591
+ if (plan.commands.length === 0)
1592
+ return "no-cli";
1593
+ try {
1594
+ for (const cmd of plan.commands) {
1595
+ exec(cmd, { stdio: ["ignore", "pipe", "pipe"], timeout: 120000 });
1596
+ }
1597
+ return "ok";
1598
+ }
1599
+ catch {
1600
+ return "failed";
1601
+ }
1602
+ }
1603
+ /**
1604
+ * Report a plan's outcome. On anything but success, be LOUD — when an AGENT runs
1605
+ * `init` (no human at the TTY), a quiet "Install vigiles:" hint followed by
1606
+ * `/plugin` slash commands is a trap: the agent can't run a TUI slash command,
1607
+ * so the plugin silently never installs. Surface the failure as a warning AND
1608
+ * lead with the shell-runnable CLI form (which an agent CAN run), keeping the
1609
+ * slash commands clearly labelled as the in-TUI alternative for a human.
1610
+ */
1611
+ function reportInstall(plan, outcome) {
1612
+ if (outcome === "ok") {
1613
+ console.log(plan.successMessage);
1614
+ for (const note of plan.notes)
1615
+ console.log(` ${note}`);
1616
+ return;
1617
+ }
1618
+ console.log(outcome === "failed"
1619
+ ? `⚠ vigiles plugin auto-install for ${plan.harness} FAILED — the plugin (hooks + skills) is NOT installed.`
1620
+ : `⚠ vigiles plugin for ${plan.harness} was NOT installed (the ${plan.harness} CLI isn't on PATH here).`);
1621
+ if (plan.commands.length > 0) {
1622
+ console.log(" Finish from a shell (an agent can run these):");
1623
+ for (const cmd of plan.commands)
1624
+ console.log(` ${cmd}`);
1625
+ }
1626
+ if (plan.manualSteps.length > 0) {
1627
+ console.log(" Or inside the Claude Code TUI (a human, not an agent):");
1628
+ for (const step of plan.manualSteps)
1629
+ console.log(` ${step}`);
1630
+ }
1631
+ for (const note of plan.notes)
1632
+ console.log(` ${note}`);
1633
+ }
1464
1634
  /**
1465
1635
  * Install vigiles's skills/hooks for the chosen harness(es) via the per-harness
1466
1636
  * `planPluginInstall` decision — Claude Code through the GLOBAL plugin
@@ -1474,26 +1644,7 @@ function installPlugins(harnesses) {
1474
1644
  });
1475
1645
  for (const plan of plans) {
1476
1646
  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
- }
1489
- }
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}`);
1647
+ reportInstall(plan, runInstall(plan, exec));
1497
1648
  }
1498
1649
  }
1499
1650
  /** Add/upgrade `vigiles` in the project's `devDependencies` (and move it out of
@@ -1657,16 +1808,44 @@ async function setup(args) {
1657
1808
  console.log("\n Non-markdown agent configs detected. Use a sync tool to convert:");
1658
1809
  console.log(" npm install -D rule-porter");
1659
1810
  }
1660
- // Strict config.
1661
- if (strict) {
1662
- const configPath = (0, node_path_1.resolve)(process.cwd(), ".vigilesrc.json");
1663
- if (!(0, node_fs_1.existsSync)(configPath)) {
1664
- (0, node_fs_1.writeFileSync)(configPath, JSON.stringify({ rules: { "require-spec": "error", "require-skill-spec": "error" } }, null, 2) + "\n");
1665
- console.log("✓ Created .vigilesrc.json with strict rules");
1666
- written.push(".vigilesrc.json");
1811
+ // Project config — record the harness(es) so compile/lint select the dialect
1812
+ // deterministically (no cwd sniffing), plus strict rule severities on --strict.
1813
+ writeProjectConfig({ harnesses, strict, written });
1814
+ printSetupSummary({ plan, strict, targets, needsMigration, written });
1815
+ }
1816
+ /** Canonical, de-duplicated harness list → a config value (string when one). */
1817
+ function harnessConfigValue(harnesses) {
1818
+ const canon = [...new Set(harnesses.map(adapter_registry_js_1.normalizeHarnessName))];
1819
+ return canon.length === 1 ? canon[0] : canon;
1820
+ }
1821
+ /**
1822
+ * Merge the resolved harness(es) (and strict rule severities) into
1823
+ * `.vigilesrc.json` without clobbering existing keys — an existing `harness`
1824
+ * stays, a missing one is added, a malformed file is left untouched.
1825
+ */
1826
+ function writeProjectConfig(opts) {
1827
+ const configPath = (0, node_path_1.resolve)(process.cwd(), ".vigilesrc.json");
1828
+ const existed = (0, node_fs_1.existsSync)(configPath);
1829
+ let existing = {};
1830
+ if (existed) {
1831
+ try {
1832
+ existing = JSON.parse((0, node_fs_1.readFileSync)(configPath, "utf-8"));
1833
+ }
1834
+ catch {
1835
+ return; // user-owned malformed config — never clobber it
1667
1836
  }
1668
1837
  }
1669
- printSetupSummary({ plan, strict, targets, needsMigration, written });
1838
+ const merged = (0, setup_plan_js_1.mergeProjectConfig)(existing, {
1839
+ harness: harnessConfigValue(opts.harnesses),
1840
+ strict: opts.strict,
1841
+ });
1842
+ if (!merged)
1843
+ return;
1844
+ (0, node_fs_1.writeFileSync)(configPath, JSON.stringify(merged, null, 2) + "\n");
1845
+ console.log(`✓ ${existed ? "Updated" : "Created"} .vigilesrc.json`);
1846
+ if (!opts.written.includes(".vigilesrc.json")) {
1847
+ opts.written.push(".vigilesrc.json");
1848
+ }
1670
1849
  }
1671
1850
  // ---------------------------------------------------------------------------
1672
1851
  // Strengthen: guidance() → enforce() suggestions
@@ -1725,7 +1904,7 @@ function checkUntestedSurfaces(config, silent) {
1725
1904
  }
1726
1905
  /**
1727
1906
  * Apply the configured coverage thresholds. Returns the number of failing
1728
- * thresholds (so the audit can fail CI when severity is "error").
1907
+ * thresholds (so the lint can fail CI when severity is "error").
1729
1908
  *
1730
1909
  * Loads specs directly via loadSpec() when the scripts threshold is set —
1731
1910
  * avoids depending on a pre-built `dist/` tree, which the setup-generated
@@ -1934,7 +2113,7 @@ function printUsage(command) {
1934
2113
  console.log("vigiles — compile typed specs to instruction files");
1935
2114
  console.log("");
1936
2115
  console.log("Commands:");
1937
- console.log(" vigiles init [flags] Setup project (--lint, --test, --harness=, --strict, --no-gha)");
2116
+ console.log(" vigiles init [flags] Setup project (--lint, --test, --harness=, --strict, --no-gha, --force)");
1938
2117
  console.log(" vigiles compile [files...] Compile .spec.ts → .md");
1939
2118
  console.log(" vigiles lint [files...] Verify references, find gaps in instruction files");
1940
2119
  console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
@@ -1960,11 +2139,11 @@ function printUsage(command) {
1960
2139
  // Main
1961
2140
  // ---------------------------------------------------------------------------
1962
2141
  /**
1963
- * Emit GitHub Actions annotations for an audit report. Skipped when --json or
2142
+ * Emit GitHub Actions annotations for an lint report. Skipped when --json or
1964
2143
  * --summary is active — those modes promise clean machine-readable stdout, and
1965
2144
  * ::error/::warning lines would contaminate output parsed as JSON.
1966
2145
  */
1967
- function annotateAuditForGitHub(report, flags) {
2146
+ function annotateLintForGitHub(report, flags) {
1968
2147
  const structuredOutput = flags.includes("--json") || flags.includes("--summary");
1969
2148
  if (!isGitHubActions() || structuredOutput)
1970
2149
  return;
@@ -1972,7 +2151,7 @@ function annotateAuditForGitHub(report, flags) {
1972
2151
  ghAnnotate("error", `${String(report.hashErrors)} compiled file(s) with stale hash — run vigiles compile`);
1973
2152
  }
1974
2153
  if (report.validationErrors > 0) {
1975
- ghAnnotate("error", `${String(report.validationErrors)} spec validation failure(s) — see audit output`);
2154
+ ghAnnotate("error", `${String(report.validationErrors)} spec validation failure(s) — see lint output`);
1976
2155
  }
1977
2156
  if (report.duplicatePairs > 0) {
1978
2157
  ghAnnotate("warning", `${String(report.duplicatePairs)} near-duplicate rule pair(s) detected — consider merging`);
@@ -2289,7 +2468,10 @@ async function main() {
2289
2468
  console.log("Run `vigiles init` to create one.");
2290
2469
  process.exit(0);
2291
2470
  }
2292
- const valid = await compile(specs, config);
2471
+ const harnessFlag = args
2472
+ .find((a) => a.startsWith("--harness="))
2473
+ ?.slice("--harness=".length);
2474
+ const valid = await compile(specs, config, { harnessFlag });
2293
2475
  console.log("");
2294
2476
  if (valid) {
2295
2477
  console.log("Compilation complete.");
@@ -2303,9 +2485,9 @@ async function main() {
2303
2485
  case "lint": {
2304
2486
  // lint = verify references + discover + guidance count
2305
2487
  const flags = args.slice(1).filter((a) => a.startsWith("--"));
2306
- const report = await audit(restArgs, flags, config);
2307
- annotateAuditForGitHub(report, flags);
2308
- const exitCode = auditExitCode(report);
2488
+ const report = await runLint(restArgs, flags, config);
2489
+ annotateLintForGitHub(report, flags);
2490
+ const exitCode = lintExitCode(report);
2309
2491
  if (exitCode !== 0) {
2310
2492
  process.exit(exitCode);
2311
2493
  }
@@ -92,7 +92,7 @@ export interface CompileAgentResult {
92
92
  /**
93
93
  * Compile an AgentSpec into a subagent markdown file with YAML frontmatter.
94
94
  * Verifies the tool contract and the body's references; the marks the body
95
- * carries (`vigiles:symbol`, file/cmd refs) are the same ones `audit` re-checks.
95
+ * carries (`vigiles:symbol`, file/cmd refs) are the same ones `lint` re-checks.
96
96
  */
97
97
  export declare function compileAgent(spec: AgentSpec, options: {
98
98
  basePath?: string;
@@ -802,7 +802,7 @@ function renderAgentRules(rules) {
802
802
  /**
803
803
  * Compile an AgentSpec into a subagent markdown file with YAML frontmatter.
804
804
  * Verifies the tool contract and the body's references; the marks the body
805
- * carries (`vigiles:symbol`, file/cmd refs) are the same ones `audit` re-checks.
805
+ * carries (`vigiles:symbol`, file/cmd refs) are the same ones `lint` re-checks.
806
806
  */
807
807
  function compileAgent(spec, options) {
808
808
  const basePath = options.basePath ?? process.cwd();
@@ -13,7 +13,7 @@
13
13
  * This detector is pure filesystem inspection — the same deterministic-detector
14
14
  * shape as `orphans.ts` / `test-coverage.ts`. It reports which tools are present
15
15
  * and any target that collides with a file the tool regenerates, so `vigiles
16
- * audit` can warn before the integrity guarantee is lost.
16
+ * lint` can warn before the integrity guarantee is lost.
17
17
  */
18
18
  /** A rule-sync tool vigiles should compose with rather than reimplement. */
19
19
  export type SyncToolName = "ruler" | "rulesync";
@@ -14,7 +14,7 @@
14
14
  * This detector is pure filesystem inspection — the same deterministic-detector
15
15
  * shape as `orphans.ts` / `test-coverage.ts`. It reports which tools are present
16
16
  * and any target that collides with a file the tool regenerates, so `vigiles
17
- * audit` can warn before the integrity guarantee is lost.
17
+ * lint` can warn before the integrity guarantee is lost.
18
18
  */
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
20
  exports.detectSyncTools = detectSyncTools;
@@ -20,7 +20,7 @@ export interface GenerateSchemaOptions {
20
20
  basePath?: string;
21
21
  /**
22
22
  * Custom linters from `.vigilesrc.json` (`rulesDir`-backed). These aren't
23
- * auto-discovered by `generate-types`, but `vigiles audit` resolves their
23
+ * auto-discovered by `generate-types`, but `vigiles lint` resolves their
24
24
  * rules via `checkLinterRule`, so the schema enum must include them too —
25
25
  * otherwise the YAML LSP false-flags a rule that CI accepts.
26
26
  */
@@ -26,7 +26,7 @@ const generate_types_js_1 = require("./generate-types.js");
26
26
  /**
27
27
  * Rule references for config-declared custom linters. Mirrors
28
28
  * `checkLinterRule`'s rulesDir lookup (any file `<rule>.*` is a rule) so the
29
- * enum matches what `vigiles audit` accepts for these linters.
29
+ * enum matches what `vigiles lint` accepts for these linters.
30
30
  */
31
31
  function customRuleRefs(basePath, linters) {
32
32
  const refs = [];
@@ -84,7 +84,7 @@ function generateSchema(options = {}) {
84
84
  properties: {
85
85
  enforce: {
86
86
  type: "array",
87
- description: "Linter rules to enforce, verified by `vigiles audit`.",
87
+ description: "Linter rules to enforce, verified by `vigiles lint`.",
88
88
  items: {
89
89
  type: "object",
90
90
  additionalProperties: false,
@@ -103,12 +103,12 @@ function generateSchema(options = {}) {
103
103
  },
104
104
  files: {
105
105
  type: "array",
106
- description: "File paths referenced by this instruction file, verified to exist by `vigiles audit`.",
106
+ description: "File paths referenced by this instruction file, verified to exist by `vigiles lint`.",
107
107
  items: { type: "string" },
108
108
  },
109
109
  commands: {
110
110
  type: "array",
111
- description: "Commands (npm scripts / script-runner invocations) referenced here, verified by `vigiles audit`.",
111
+ description: "Commands (npm scripts / script-runner invocations) referenced here, verified by `vigiles lint`.",
112
112
  items: { type: "string" },
113
113
  },
114
114
  },