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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/dist/adapter-conformance.js +1 -1
- package/dist/adapter-registry.d.ts +45 -1
- package/dist/adapter-registry.js +78 -3
- package/dist/check.d.ts +132 -0
- package/dist/check.js +318 -0
- package/dist/cli.js +255 -73
- package/dist/core/compile.d.ts +1 -1
- package/dist/core/compile.js +1 -1
- package/dist/core/compose.d.ts +1 -1
- package/dist/core/compose.js +1 -1
- package/dist/core/generate-schema.d.ts +1 -1
- package/dist/core/generate-schema.js +4 -4
- package/dist/core/linters.js +2 -2
- package/dist/core/orphans.js +57 -14
- package/dist/core/proofs.js +1 -1
- package/dist/core/refs.d.ts +1 -1
- package/dist/core/refs.js +2 -2
- package/dist/core/sidecar.d.ts +1 -1
- package/dist/core/sidecar.js +1 -1
- package/dist/core/spec.d.ts +1 -1
- package/dist/core/spec.js +1 -1
- package/dist/core/types.d.ts +11 -1
- package/dist/core/validate.js +2 -2
- package/dist/e2e.d.ts +10 -13
- package/dist/e2e.js +10 -17
- package/dist/eval.d.ts +217 -2
- package/dist/eval.js +428 -18
- package/dist/harness-assert.d.ts +3 -0
- package/dist/harness-assert.js +16 -0
- package/dist/harness-test.d.ts +46 -0
- package/dist/harness-test.js +102 -0
- package/dist/integration.d.ts +8 -0
- package/dist/integration.js +10 -0
- package/dist/jest.d.ts +3 -1
- package/dist/jest.js +3 -2
- package/dist/run-hook.d.ts +22 -0
- package/dist/run-hook.js +28 -0
- package/dist/scan.d.ts +1 -1
- package/dist/scan.js +1 -1
- package/dist/setup-plan.d.ts +16 -1
- package/dist/setup-plan.js +38 -1
- package/dist/skill-harness.d.ts +25 -0
- package/dist/skill-harness.js +40 -0
- package/dist/test-coverage.js +8 -1
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +7 -0
- package/dist/unit.d.ts +4 -2
- package/dist/unit.js +7 -1
- package/dist/vitest.d.mts +3 -1
- package/hooks/refs-nudge.sh +1 -1
- package/package.json +3 -2
- package/skills/edit-spec/SKILL.md +21 -10
- package/skills/linter-docs/SKILL.md +23 -0
- package/skills/migrate-to-spec/SKILL.md +1 -1
- package/skills/strengthen/SKILL.md +1 -2
- 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/
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
889
|
-
function
|
|
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
|
-
|
|
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 (`
|
|
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
|
|
1140
|
-
* bare-`npx vigiles` API
|
|
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
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2307
|
-
|
|
2308
|
-
const exitCode =
|
|
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
|
}
|
package/dist/core/compile.d.ts
CHANGED
|
@@ -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 `
|
|
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;
|
package/dist/core/compile.js
CHANGED
|
@@ -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 `
|
|
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();
|
package/dist/core/compose.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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";
|
package/dist/core/compose.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
111
|
+
description: "Commands (npm scripts / script-runner invocations) referenced here, verified by `vigiles lint`.",
|
|
112
112
|
items: { type: "string" },
|
|
113
113
|
},
|
|
114
114
|
},
|