vigiles 22.0.0 → 24.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/adapters/claude-code/hook-protocol.js +3 -0
- package/dist/adapters/claude-code/layout.js +2 -0
- package/dist/adapters/claude-code/run-scripts.js +26 -2
- package/dist/cli-flag-check.js +2 -1
- package/dist/cli.d.ts +121 -0
- package/dist/cli.js +375 -32
- package/dist/core/hook-block-ineffective.js +24 -1
- package/dist/core/hook-protocol.d.ts +14 -0
- package/dist/core/layout.d.ts +15 -0
- package/dist/core/rule-meta.d.ts +1 -1
- package/dist/core/rule-meta.js +16 -0
- package/dist/core/types.d.ts +44 -0
- package/dist/core/validate.js +18 -0
- package/dist/coverage-evidence.d.ts +48 -2
- package/dist/coverage-evidence.js +96 -3
- package/dist/eval.d.ts +10 -0
- package/dist/eval.js +14 -3
- package/dist/harness-resolve-hooks.d.mts +13 -0
- package/dist/harness-resolve-hooks.mjs +50 -0
- package/dist/plugin-loader.js +35 -0
- package/dist/run-hook.d.ts +23 -2
- package/dist/run-hook.js +10 -3
- package/dist/scan-core.d.ts +5 -0
- package/dist/scan-core.js +10 -1
- package/dist/test-coverage-files.js +17 -8
- package/dist/test-coverage.js +29 -16
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.specLoadFailureReason = specLoadFailureReason;
|
|
15
|
+
exports.lintTotals = lintTotals;
|
|
16
|
+
exports.discoverNestedBundles = discoverNestedBundles;
|
|
15
17
|
const node_fs_1 = require("node:fs");
|
|
16
18
|
const node_path_1 = require("node:path");
|
|
19
|
+
const minimatch_1 = require("minimatch");
|
|
17
20
|
const node_child_process_1 = require("node:child_process");
|
|
18
21
|
const glob_1 = require("glob");
|
|
19
22
|
const generate_types_js_1 = require("./core/generate-types.js");
|
|
@@ -316,7 +319,20 @@ function compileClaudeToFile(spec, specPath, config, dialect) {
|
|
|
316
319
|
if (errors.length > 0) {
|
|
317
320
|
console.log(`\n✗ ${specPath} — ${String(errors.length)} error(s)`);
|
|
318
321
|
printErrors(specPath, errors);
|
|
319
|
-
|
|
322
|
+
// 🔴 NOTHING IS WRITTEN ON A FAILED COMPILE (#173).
|
|
323
|
+
//
|
|
324
|
+
// It used to write the artifact anyway, and the result was the exact
|
|
325
|
+
// false-confidence object this tool exists to prevent: a `CLAUDE.md`
|
|
326
|
+
// carrying refs already KNOWN to be dead, stamped with a VALID integrity
|
|
327
|
+
// hash. `lint` then verified the hash, found it intact, and exited 0 — so
|
|
328
|
+
// the command the README calls "the CI gate … broken refs" went green over
|
|
329
|
+
// breakage `compile` had printed minutes earlier. Compile locally, get
|
|
330
|
+
// distracted, commit: CI never mentions it again.
|
|
331
|
+
//
|
|
332
|
+
// Not writing leaves the LAST GOOD artifact in place, which is strictly
|
|
333
|
+
// better than replacing it with a broken one: the error is on screen, the
|
|
334
|
+
// exit code is 1, and no green hash is minted over a known-bad file.
|
|
335
|
+
console.log(` → ${primaryOutput} was NOT written; the previous version is left in place.`);
|
|
320
336
|
return false;
|
|
321
337
|
}
|
|
322
338
|
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(basePath, primaryOutput), markdown);
|
|
@@ -788,6 +804,55 @@ async function verifyMarkdownMcpRefs(files, silent) {
|
|
|
788
804
|
return errors;
|
|
789
805
|
}
|
|
790
806
|
/** Exit codes: 0 clean, 1 warnings only, 2 hard errors. */
|
|
807
|
+
/**
|
|
808
|
+
* The run's totals, derived from the report itself.
|
|
809
|
+
*
|
|
810
|
+
* 🔴 ONE SOURCE, because the two numbers disagreeing IS the bug (#183). The
|
|
811
|
+
* human-readable log had no total, and counting its `⚠` lines gave a different
|
|
812
|
+
* number from the JSON — 21 against 88 on a real repo — because some checks print
|
|
813
|
+
* one line per finding and others one line carrying a count. Both numbers were
|
|
814
|
+
* right and nothing said why they differed, so "vigiles reports 21 warnings" and
|
|
815
|
+
* "88 warnings" were equally defensible readings of one run.
|
|
816
|
+
*
|
|
817
|
+
* Counted GENERICALLY off the `*Issues` / `*Errors` / count keys rather than a
|
|
818
|
+
* hand-maintained list, so a rule added later is included by existing, not by
|
|
819
|
+
* somebody remembering. `orphanCount` and `duplicatePairs` are named explicitly
|
|
820
|
+
* only because they predate the `*Issues` convention (#181).
|
|
821
|
+
*/
|
|
822
|
+
function lintTotals(report) {
|
|
823
|
+
let errors = 0;
|
|
824
|
+
let findings = 0;
|
|
825
|
+
for (const [key, value] of Object.entries(report)) {
|
|
826
|
+
if (typeof value !== "number" || value === 0)
|
|
827
|
+
continue;
|
|
828
|
+
if (key === "files")
|
|
829
|
+
continue;
|
|
830
|
+
// Informational counters: not findings, they describe the corpus.
|
|
831
|
+
if (key === "inlineRules" ||
|
|
832
|
+
key === "frontmatterRules" ||
|
|
833
|
+
key === "coverageEnabled" ||
|
|
834
|
+
key === "coverageDocumented" ||
|
|
835
|
+
key === "strengthenSuggestions")
|
|
836
|
+
continue;
|
|
837
|
+
if (key.endsWith("Errors")) {
|
|
838
|
+
errors += value;
|
|
839
|
+
findings += value;
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
// `*Issues` counts EVERY finding of that rule; when the rule is at "error"
|
|
843
|
+
// the same findings are also in `*Errors`, so they must not be counted twice.
|
|
844
|
+
if (key.endsWith("Issues")) {
|
|
845
|
+
const paired = report[`${key.slice(0, -"Issues".length)}Errors`];
|
|
846
|
+
findings += paired && paired > 0 ? 0 : value;
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (key === "orphanCount" ||
|
|
850
|
+
key === "duplicatePairs" ||
|
|
851
|
+
key === "untestedSurfaces")
|
|
852
|
+
findings += value;
|
|
853
|
+
}
|
|
854
|
+
return { findings, errors, warnings: findings - errors };
|
|
855
|
+
}
|
|
791
856
|
function lintExitCode(report) {
|
|
792
857
|
if (report.hashErrors > 0 ||
|
|
793
858
|
report.validationErrors > 0 ||
|
|
@@ -822,9 +887,15 @@ function lintExitCode(report) {
|
|
|
822
887
|
// it to "error", so it belongs in the hard tier with every other explicit
|
|
823
888
|
// error — it used to sit at exit 1 because it fired unasked and could not be
|
|
824
889
|
// turned off.
|
|
825
|
-
report.docRefErrors > 0
|
|
890
|
+
report.docRefErrors > 0 ||
|
|
891
|
+
report.specRefErrors > 0)
|
|
826
892
|
return 2;
|
|
827
|
-
|
|
893
|
+
// Tierable now: a `warn` orphan/duplicate finding is reported and does NOT
|
|
894
|
+
// change the exit code. Both used to feed the exit directly, which is what
|
|
895
|
+
// made them the only untierable findings in the tool.
|
|
896
|
+
if (report.orphanCount > 0 && report.orphanSeverity === "error")
|
|
897
|
+
return 1;
|
|
898
|
+
if (report.duplicatePairs > 0 && report.duplicateSeverity === "error")
|
|
828
899
|
return 1;
|
|
829
900
|
// Guidance counts are informational, not failures
|
|
830
901
|
return 0;
|
|
@@ -1072,6 +1143,157 @@ function sharedDirsRootFor(scanTarget) {
|
|
|
1072
1143
|
const underCwd = rel === "" || (!rel.startsWith("..") && !(0, node_path_1.isAbsolute)(rel));
|
|
1073
1144
|
return underCwd ? cwd : target;
|
|
1074
1145
|
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Nested plugin bundles under a lint root — a directory that is itself a harness
|
|
1148
|
+
* (its own `.claude-plugin/plugin.json`, or its own skills dir) and is NOT the
|
|
1149
|
+
* root being linted.
|
|
1150
|
+
*
|
|
1151
|
+
* 🔴 WHY THIS EXISTS. Every per-surface check reads ONE root, so in a monorepo
|
|
1152
|
+
* holding `skills/` plus `plugins/ * /skills/` the nested skills were never scored
|
|
1153
|
+
* and nothing said so. Measured on a fixture: 4 skills over the description
|
|
1154
|
+
* budget, `lint .` reported 2, exit 0 — a repo reads that as green-with-2 while
|
|
1155
|
+
* the other 2 carry the same defect (#185). The failure is silent, which is the
|
|
1156
|
+
* shape this repo treats as worse than a loud one.
|
|
1157
|
+
*
|
|
1158
|
+
* Deliberately shallow (one level under a container dir): deep recursion would
|
|
1159
|
+
* sweep vendored corpora — this repo's own `test/dogfood/` holds real pinned
|
|
1160
|
+
* third-party plugins — and scoring someone else's vendored plugin as if it were
|
|
1161
|
+
* yours is the false-positive that gets a gate switched off.
|
|
1162
|
+
*/
|
|
1163
|
+
function discoverNestedBundles(root, exclude = []) {
|
|
1164
|
+
const out = [];
|
|
1165
|
+
const skip = new Set([
|
|
1166
|
+
"node_modules",
|
|
1167
|
+
".git",
|
|
1168
|
+
"dist",
|
|
1169
|
+
"coverage",
|
|
1170
|
+
".vigiles",
|
|
1171
|
+
]);
|
|
1172
|
+
const isBundle = (dir) => (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, ".claude-plugin", "plugin.json")) ||
|
|
1173
|
+
(0, node_fs_1.existsSync)((0, node_path_1.join)(dir, "skills"));
|
|
1174
|
+
const excluded = (rel) => exclude.some((pattern) => rel === pattern ||
|
|
1175
|
+
rel.startsWith(`${pattern}/`) ||
|
|
1176
|
+
(0, minimatch_1.minimatch)(rel, pattern) ||
|
|
1177
|
+
(0, minimatch_1.minimatch)(rel, `${pattern}/**`));
|
|
1178
|
+
let entries;
|
|
1179
|
+
try {
|
|
1180
|
+
entries = (0, node_fs_1.readdirSync)(root, { withFileTypes: true })
|
|
1181
|
+
.filter((e) => e.isDirectory() && !skip.has(e.name) && !e.name.startsWith("."))
|
|
1182
|
+
.map((e) => e.name);
|
|
1183
|
+
}
|
|
1184
|
+
catch {
|
|
1185
|
+
return out;
|
|
1186
|
+
}
|
|
1187
|
+
for (const name of entries) {
|
|
1188
|
+
const dir = (0, node_path_1.join)(root, name);
|
|
1189
|
+
if (excluded(name))
|
|
1190
|
+
continue;
|
|
1191
|
+
// A container (`plugins/`) holds bundles; a bundle may also sit directly.
|
|
1192
|
+
if (isBundle(dir)) {
|
|
1193
|
+
out.push(dir);
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
let inner;
|
|
1197
|
+
try {
|
|
1198
|
+
inner = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })
|
|
1199
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
1200
|
+
.map((e) => e.name);
|
|
1201
|
+
}
|
|
1202
|
+
catch {
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
for (const child of inner) {
|
|
1206
|
+
const sub = (0, node_path_1.join)(dir, child);
|
|
1207
|
+
if (excluded(`${name}/${child}`))
|
|
1208
|
+
continue;
|
|
1209
|
+
if (isBundle(sub))
|
|
1210
|
+
out.push(sub);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return out.sort();
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Run one per-surface check over EVERY root and sum its counters.
|
|
1217
|
+
*
|
|
1218
|
+
* The checks all share `(config, silent, adapter, root)` and return a small
|
|
1219
|
+
* record of numbers, so one wrapper covers all twenty rather than twenty edits —
|
|
1220
|
+
* and a check added later is swept in by using it, not by remembering to.
|
|
1221
|
+
*/
|
|
1222
|
+
function overBundles(fn, config, silent, adapter, roots) {
|
|
1223
|
+
const [first, ...rest] = roots;
|
|
1224
|
+
const total = { ...fn(config, silent, adapter, first) };
|
|
1225
|
+
for (const root of rest) {
|
|
1226
|
+
const next = fn(config, silent, adapter, root);
|
|
1227
|
+
for (const key of Object.keys(next))
|
|
1228
|
+
total[key] =
|
|
1229
|
+
(total[key] ?? 0) + (next[key] ?? 0);
|
|
1230
|
+
}
|
|
1231
|
+
return total;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Re-derive a compiled artifact's references from its SPEC, and report the dead
|
|
1235
|
+
* ones — the half of #173 that deleting the write-on-error did not close.
|
|
1236
|
+
*
|
|
1237
|
+
* 🔴 THE HOLE. `lint` verifies the integrity HASH, which answers "is this file
|
|
1238
|
+
* still what the spec compiled to" and says nothing about whether the things it
|
|
1239
|
+
* NAMES still exist. So a `CLAUDE.md` committed while its refs were live stays
|
|
1240
|
+
* green forever after the referenced file is deleted: `compile` errors, `lint`
|
|
1241
|
+
* prints "hash valid — All compiled files intact" and exits 0. Reproduced:
|
|
1242
|
+
*
|
|
1243
|
+
* $ vigiles compile CLAUDE.md.spec.ts
|
|
1244
|
+
* ✗ [stale-file] File not found: "docs/guide.md"
|
|
1245
|
+
* $ vigiles lint .
|
|
1246
|
+
* ✓ CLAUDE.md — hash valid # exit 0
|
|
1247
|
+
*
|
|
1248
|
+
* The reporter named this residue himself when filing #173 and I deferred it;
|
|
1249
|
+
* it is the gap between what `README.md` promises of `lint` ("the CI gate …
|
|
1250
|
+
* broken refs") and what it checked. A hash is an integrity claim, not a
|
|
1251
|
+
* reference claim, and the two were being read as one.
|
|
1252
|
+
*
|
|
1253
|
+
* Cost is bounded: only specs whose compiled target actually EXISTS are loaded,
|
|
1254
|
+
* so a repo with no specs does no extra work at all.
|
|
1255
|
+
*/
|
|
1256
|
+
async function checkSpecRefs(config, silent, dialect) {
|
|
1257
|
+
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["spec-refs"]) ?? "error";
|
|
1258
|
+
if (!sev)
|
|
1259
|
+
return { issues: 0, errors: 0 };
|
|
1260
|
+
const found = [];
|
|
1261
|
+
for (const specPath of findSpecs()) {
|
|
1262
|
+
const target = specPath.replace(/\.spec\.ts$/, "");
|
|
1263
|
+
if (!(0, node_fs_1.existsSync)(target))
|
|
1264
|
+
continue; // never compiled — `compile` reports it
|
|
1265
|
+
const spec = await loadSpec(specPath);
|
|
1266
|
+
if (!spec || spec._specType !== "claude")
|
|
1267
|
+
continue;
|
|
1268
|
+
try {
|
|
1269
|
+
const { errors } = (0, compile_js_1.compileClaude)(spec, {
|
|
1270
|
+
basePath: process.cwd(),
|
|
1271
|
+
specFile: specPath,
|
|
1272
|
+
dialect,
|
|
1273
|
+
maxRules: config?.maxRules,
|
|
1274
|
+
maxTokens: config?.maxTokens,
|
|
1275
|
+
maxSectionLines: config?.maxSectionLines,
|
|
1276
|
+
catalogOnly: config?.catalogOnly,
|
|
1277
|
+
linters: config?.linters,
|
|
1278
|
+
});
|
|
1279
|
+
for (const e of errors)
|
|
1280
|
+
found.push(`${target}: ${e.message} (from ${specPath})`);
|
|
1281
|
+
}
|
|
1282
|
+
catch {
|
|
1283
|
+
// A spec that will not load is `compile`'s finding, not this one's —
|
|
1284
|
+
// reporting it here would double-report and blame the wrong command.
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
if (found.length > 0 && !silent) {
|
|
1289
|
+
console.log("\nSpec reference check:\n");
|
|
1290
|
+
for (const msg of found) {
|
|
1291
|
+
console.log(` ${sev === "error" ? "✗" : "⚠"} ${msg}`);
|
|
1292
|
+
ghAnnotate(sev === "error" ? "error" : "warning", msg);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
1296
|
+
}
|
|
1075
1297
|
async function runLint(restArgs, flags, config) {
|
|
1076
1298
|
const summary = flags.includes("--summary");
|
|
1077
1299
|
const json = flags.includes("--json");
|
|
@@ -1104,6 +1326,30 @@ async function runLint(restArgs, flags, config) {
|
|
|
1104
1326
|
configHarness: (0, adapter_registry_js_1.normalizeHarnessList)(config?.harness),
|
|
1105
1327
|
});
|
|
1106
1328
|
const adapter = lintSelection.adapter;
|
|
1329
|
+
// 🔴 WHICH ROOTS GET SCORED, and saying so either way (#185).
|
|
1330
|
+
//
|
|
1331
|
+
// Every per-surface check reads ONE root, so a monorepo with `skills/` plus
|
|
1332
|
+
// `plugins/*/skills/` scored only the first and said nothing — measured at 2
|
|
1333
|
+
// findings reported against 4 real ones, exit 0. A skipped surface that is not
|
|
1334
|
+
// announced reads as a clean surface.
|
|
1335
|
+
//
|
|
1336
|
+
// Default stays ROOT-ONLY on purpose: descending by default would start
|
|
1337
|
+
// scoring vendored third-party corpora (this repo's own `test/dogfood/` holds
|
|
1338
|
+
// pinned real plugins), and scoring someone else's plugin as if it were yours
|
|
1339
|
+
// is the false positive that gets a gate turned off. So the DEFAULT fixes the
|
|
1340
|
+
// SILENCE, and `bundles: "all"` fixes the COVERAGE — one exit code over the
|
|
1341
|
+
// whole repo, which is what a CI gate needs.
|
|
1342
|
+
const nestedBundles = discoverNestedBundles(scanRoot, config?.exclude ?? []);
|
|
1343
|
+
const scoreAll = flags.includes("--bundles=all") || config?.bundles === "all";
|
|
1344
|
+
const lintRoots = scoreAll ? [scanRoot, ...nestedBundles] : [scanRoot];
|
|
1345
|
+
if (!silent && nestedBundles.length > 0) {
|
|
1346
|
+
const rel = nestedBundles.map((b) => (0, node_path_1.relative)(scanRoot, b) || b);
|
|
1347
|
+
console.log(scoreAll
|
|
1348
|
+
? `\nScoring ${String(lintRoots.length)} bundles: the root + ${rel.join(", ")}`
|
|
1349
|
+
: `\n⚠ ${String(nestedBundles.length)} nested bundle(s) discovered but NOT scored: ${rel.join(", ")}\n` +
|
|
1350
|
+
` Their skills/agents/hooks are not in the counters below. Add \`"bundles": "all"\` to ` +
|
|
1351
|
+
`.vigilesrc.json (or pass --bundles=all) to score them in this run.`);
|
|
1352
|
+
}
|
|
1107
1353
|
// Discover the compiled files whose integrity is verified. Include the active
|
|
1108
1354
|
// harness's subagent dir (dogfood E2): a compiled `agents/<name>.md` carries a
|
|
1109
1355
|
// vigiles hash, but the default glob only matched CLAUDE/AGENTS/SKILL, so a
|
|
@@ -1157,7 +1403,14 @@ async function runLint(restArgs, flags, config) {
|
|
|
1157
1403
|
// declares the block; its `include` defaults to docs/ (research/ etc. are
|
|
1158
1404
|
// opted into explicitly). `enforce("vigiles/orphan-docs")` in a spec only
|
|
1159
1405
|
// validates the rule NAME — the block is what drives the scan.
|
|
1160
|
-
|
|
1406
|
+
// 🔴 SEVERITY IS READ, not just the block's presence (#181). `orphan-docs` had
|
|
1407
|
+
// a RULE_META entry and a documented severity, and nothing ever read it: both
|
|
1408
|
+
// `"warn"` and `"off"` still exited 1, so a repo could only choose between an
|
|
1409
|
+
// always-blocking check and deleting the `orphans` block. `warn` now reports
|
|
1410
|
+
// without touching the exit code and `false`/`"off"` skips the scan, exactly
|
|
1411
|
+
// like every other rule.
|
|
1412
|
+
const orphanSeverity = (0, types_js_1.ruleSeverity)(config?.rules?.["orphan-docs"]) ?? "warn";
|
|
1413
|
+
const orphansCfg = orphanSeverity ? config?.orphans : undefined;
|
|
1161
1414
|
if (!silent)
|
|
1162
1415
|
console.log("\nOrphan docs check:\n");
|
|
1163
1416
|
let orphanReport = {
|
|
@@ -1188,72 +1441,75 @@ async function runLint(restArgs, flags, config) {
|
|
|
1188
1441
|
// 7b. Untested-surface check — skills/agents/hooks shipping without a test or
|
|
1189
1442
|
// eval. Warning by default (a nudge, exit 0); set rules.untested-{skill,agent,
|
|
1190
1443
|
// hook} to "error" to gate CI. See src/test-coverage.ts and docs/rules/.
|
|
1191
|
-
|
|
1444
|
+
// A compiled artifact's refs, re-derived from its spec — the hash says the file
|
|
1445
|
+
// is unchanged, not that what it names still exists (#173).
|
|
1446
|
+
const specRefs = await checkSpecRefs(config, silent, adapter.dialect);
|
|
1447
|
+
const untested = overBundles(checkUntestedSurfaces, config, silent, adapter, lintRoots);
|
|
1192
1448
|
// 7c. Subagent tool-contract check — cross-reference each subagent's `tools:`
|
|
1193
1449
|
// rail against the harness catalog (the moat). n/a on a harness with no
|
|
1194
1450
|
// subagents. Off by default unless a severity is configured; warning surfaces
|
|
1195
1451
|
// a typo/never-available tool, error gates CI.
|
|
1196
|
-
const toolContract = checkSubagentToolContracts
|
|
1452
|
+
const toolContract = overBundles(checkSubagentToolContracts, config, silent, adapter, lintRoots);
|
|
1197
1453
|
// 7d. Hook-event check — a hook registered under an event the harness doesn't
|
|
1198
1454
|
// define never fires. High-precision (close typos only). Off unless configured.
|
|
1199
|
-
const hookEvents = checkHookEvents
|
|
1455
|
+
const hookEvents = overBundles(checkHookEvents, config, silent, adapter, lintRoots);
|
|
1200
1456
|
// 7e. Subagent-frontmatter check — a subagent missing required frontmatter
|
|
1201
1457
|
// (name + description) won't register. n/a on a harness with no subagents.
|
|
1202
|
-
const frontmatter = checkFrontmatterSchema
|
|
1458
|
+
const frontmatter = overBundles(checkFrontmatterSchema, config, silent, adapter, lintRoots);
|
|
1203
1459
|
// 7f. MCP-config check — a declared MCP server with no command/url can't start.
|
|
1204
|
-
const mcpConfig = checkMcpConfig
|
|
1460
|
+
const mcpConfig = overBundles(checkMcpConfig, config, silent, adapter, lintRoots);
|
|
1205
1461
|
// 7g. Skill-frontmatter — RECOMMEND explicit name/description on skills (a
|
|
1206
1462
|
// reliable trigger surface). Best-practice nudge; skills load without it.
|
|
1207
|
-
const skillFm = checkSkillFrontmatter
|
|
1463
|
+
const skillFm = overBundles(checkSkillFrontmatter, config, silent, adapter, lintRoots);
|
|
1208
1464
|
// 7h. MCP tool-resolution — an `mcp__server__tool` in a contract whose server
|
|
1209
1465
|
// the plugin doesn't declare can't resolve (the MCP half of the tool moat).
|
|
1210
|
-
const mcpToolResolves = checkMcpToolResolves
|
|
1466
|
+
const mcpToolResolves = overBundles(checkMcpToolResolves, config, silent, adapter, lintRoots);
|
|
1211
1467
|
// 7i. Hook-script existence — a hook command referencing a missing script file
|
|
1212
1468
|
// never runs (matches Anthropic's own `claude plugin validate`).
|
|
1213
|
-
const hookScripts = checkHookScriptExists
|
|
1469
|
+
const hookScripts = overBundles(checkHookScriptExists, config, silent, adapter, lintRoots);
|
|
1214
1470
|
// 7j. Disallowed-tools — a `disallowedTools:` block-list typo blocks nothing
|
|
1215
1471
|
// (the deny-side mirror of subagent-tool-contract; close-typo only).
|
|
1216
|
-
const disallowedTools = checkDisallowedTools
|
|
1472
|
+
const disallowedTools = overBundles(checkDisallowedTools, config, silent, adapter, lintRoots);
|
|
1217
1473
|
// 7k. Description-overlap — two model-invocable skills with near-identical
|
|
1218
1474
|
// descriptions collide in the selector (deterministic NCD precision proxy).
|
|
1219
|
-
const descriptionOverlap = checkDescriptionOverlap
|
|
1475
|
+
const descriptionOverlap = overBundles(checkDescriptionOverlap, config, silent, adapter, lintRoots);
|
|
1220
1476
|
// 7k². Skill-description-budget — a model-invocable skill whose description is
|
|
1221
1477
|
// so long the trigger signal is buried (heuristic proxy; degrades recall +
|
|
1222
1478
|
// precision). Generous 500-char budget; warn-tier, never gates.
|
|
1223
|
-
const descriptionBudget = checkDescriptionBudget
|
|
1479
|
+
const descriptionBudget = overBundles(checkDescriptionBudget, config, silent, adapter, lintRoots);
|
|
1224
1480
|
// 7l. Frontmatter-valid — a `---` block that isn't valid YAML (warn; js-yaml is
|
|
1225
1481
|
// stricter than some loaders, so verify before enforcing).
|
|
1226
|
-
const frontmatterValid = checkFrontmatterValid
|
|
1482
|
+
const frontmatterValid = overBundles(checkFrontmatterValid, config, silent, adapter, lintRoots);
|
|
1227
1483
|
// 7m. MCP hook-target — a `type: mcp_tool` hook action that's incomplete or
|
|
1228
1484
|
// targets an undeclared server (the moat applied to the hook surface).
|
|
1229
|
-
const mcpHookTargets = checkMcpHookTargets
|
|
1485
|
+
const mcpHookTargets = overBundles(checkMcpHookTargets, config, silent, adapter, lintRoots);
|
|
1230
1486
|
// 7n. Prefer-compiled-hooks — ONE discovery nudge (not per-hook) toward
|
|
1231
1487
|
// compiled `vigiles/hook` artifacts when hand-written hooks ship. Recommendation.
|
|
1232
|
-
const preferCompiledHooks = checkPreferCompiledHooks
|
|
1488
|
+
const preferCompiledHooks = overBundles(checkPreferCompiledHooks, config, silent, adapter, lintRoots);
|
|
1233
1489
|
// 7o. Lethal-trifecta — a unit (subagent / model-invocable skill) whose tools
|
|
1234
1490
|
// hold all three legs (read-private + ingest-untrusted + exfiltrate) is a
|
|
1235
1491
|
// prompt-injection exfil path (Rule of Two). Capability SET-intersection.
|
|
1236
|
-
const lethalTrifecta = checkLethalTrifecta
|
|
1492
|
+
const lethalTrifecta = overBundles(checkLethalTrifecta, config, silent, adapter, lintRoots);
|
|
1237
1493
|
// 7p. Skill-resource — a SKILL.md body referencing a bundled file that doesn't
|
|
1238
1494
|
// exist on disk under the skill dir (the agent gets nothing). FP-safe.
|
|
1239
|
-
const skillResources = checkSkillResourceResolves
|
|
1495
|
+
const skillResources = overBundles(checkSkillResourceResolves, config, silent, adapter, lintRoots);
|
|
1240
1496
|
// 7q. Skill-missing-fence — a SKILL.md opening with `name:`/`description:` but no
|
|
1241
1497
|
// `---` fence loads as plain body (invisible — no name/description/trigger).
|
|
1242
|
-
const skillFence = checkSkillMissingFence
|
|
1498
|
+
const skillFence = overBundles(checkSkillMissingFence, config, silent, adapter, lintRoots);
|
|
1243
1499
|
// 7r. Plugin-dir-layout — functional surface dirs (skills/agents/commands) nested
|
|
1244
1500
|
// inside the `.claude-plugin/` manifest dir where the harness can't see them.
|
|
1245
|
-
const pluginLayout = checkPluginDirLayout
|
|
1501
|
+
const pluginLayout = overBundles(checkPluginDirLayout, config, silent, adapter, lintRoots);
|
|
1246
1502
|
// 7s. Delegation-trifecta — a lethal trifecta that emerges across a delegation
|
|
1247
1503
|
// edge (a subagent's own ∪ delegated-to capability) though no single unit trips it.
|
|
1248
|
-
const delegationTrifecta = checkDelegationTrifecta
|
|
1504
|
+
const delegationTrifecta = overBundles(checkDelegationTrifecta, config, silent, adapter, lintRoots);
|
|
1249
1505
|
// 7t. Hook-block-ineffective — a hook that looks like it blocks but silently
|
|
1250
1506
|
// doesn't (block decision on a non-blocking event, or the legacy `decision`
|
|
1251
1507
|
// field on a permission-gated event). The #1 verified hook pain (#19009).
|
|
1252
|
-
const hookBlock = checkHookBlockIneffective
|
|
1508
|
+
const hookBlock = overBundles(checkHookBlockIneffective, config, silent, adapter, lintRoots);
|
|
1253
1509
|
// 7u. Hook-matcher — a hook `matcher` that doesn't fire as written (tool-name
|
|
1254
1510
|
// typo, an uncompilable or unreachable MCP pattern, one too narrow for real
|
|
1255
1511
|
// server naming, or an undeclared MCP server).
|
|
1256
|
-
const hookMatcher = checkHookMatcher
|
|
1512
|
+
const hookMatcher = overBundles(checkHookMatcher, config, silent, adapter, lintRoots);
|
|
1257
1513
|
// 8. Validate vigiles builder calls inside markdown code blocks — the
|
|
1258
1514
|
// `doc-refs` rule, DEFAULT OFF. Illustrative blocks opt out via
|
|
1259
1515
|
// `<!-- vigiles:ignore -->` (single block) or `<!-- vigiles:ignore-file -->`
|
|
@@ -1329,13 +1585,17 @@ async function runLint(restArgs, flags, config) {
|
|
|
1329
1585
|
inlineRules,
|
|
1330
1586
|
frontmatterErrors,
|
|
1331
1587
|
frontmatterRules,
|
|
1588
|
+
specRefIssues: specRefs.issues,
|
|
1589
|
+
specRefErrors: specRefs.errors,
|
|
1332
1590
|
duplicatePairs: dups.pairCount,
|
|
1591
|
+
duplicateSeverity: (0, types_js_1.ruleSeverity)(config?.rules?.["duplicate-rules"]) ?? "warn",
|
|
1333
1592
|
coverageEnabled: coverage.enabled,
|
|
1334
1593
|
coverageDocumented: coverage.documented,
|
|
1335
1594
|
strengthenSuggestions: guidanceCount,
|
|
1336
1595
|
integrityErrors,
|
|
1337
1596
|
coverageErrors,
|
|
1338
1597
|
orphanCount: orphanReport.orphans.length,
|
|
1598
|
+
orphanSeverity: orphanSeverity,
|
|
1339
1599
|
untestedSurfaces: untested.untested,
|
|
1340
1600
|
untestedErrors: untested.errors,
|
|
1341
1601
|
toolContractIssues: toolContract.issues,
|
|
@@ -1386,13 +1646,41 @@ async function runLint(restArgs, flags, config) {
|
|
|
1386
1646
|
mcpRefErrors,
|
|
1387
1647
|
files,
|
|
1388
1648
|
};
|
|
1649
|
+
// The totals both surfaces quote, computed ONCE (#183). Attaching them to the
|
|
1650
|
+
// report is what makes the log line and `--json` incapable of disagreeing —
|
|
1651
|
+
// the previous gap was not a wrong number, it was two right numbers with
|
|
1652
|
+
// nothing explaining the difference.
|
|
1653
|
+
const totals = lintTotals(report);
|
|
1654
|
+
const reported = { ...report, totals };
|
|
1655
|
+
// `--json-out=<file>` writes the JSON to disk while stdout keeps the
|
|
1656
|
+
// human-readable run — one scan, both artefacts (#182). A CI job needed both
|
|
1657
|
+
// (the log is what a human opens; the JSON is what the PR comment is built
|
|
1658
|
+
// from) and had to scan the repo TWICE to get them, which is the same work
|
|
1659
|
+
// done twice and grows with the corpus.
|
|
1660
|
+
const jsonOutFlag = flags.find((f) => f.startsWith("--json-out="));
|
|
1661
|
+
if (jsonOutFlag) {
|
|
1662
|
+
const dest = (0, node_path_1.resolve)(jsonOutFlag.slice("--json-out=".length));
|
|
1663
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(dest), { recursive: true });
|
|
1664
|
+
(0, node_fs_1.writeFileSync)(dest, `${JSON.stringify(reported, null, 2)}\n`);
|
|
1665
|
+
if (!silent)
|
|
1666
|
+
console.log(`\n✓ JSON report written to ${dest}`);
|
|
1667
|
+
}
|
|
1389
1668
|
if (summary) {
|
|
1390
|
-
printLintSummary(
|
|
1669
|
+
printLintSummary(reported);
|
|
1391
1670
|
}
|
|
1392
1671
|
else if (json) {
|
|
1393
|
-
console.log(JSON.stringify(
|
|
1672
|
+
console.log(JSON.stringify(reported, null, 2));
|
|
1673
|
+
}
|
|
1674
|
+
else {
|
|
1675
|
+
// The one number a reader can quote. Counting `⚠` lines gives a DIFFERENT
|
|
1676
|
+
// number, because some checks print one line per finding and others one line
|
|
1677
|
+
// carrying a count — so the log now states the finding total outright rather
|
|
1678
|
+
// than leaving the reader to infer it from line shapes.
|
|
1679
|
+
const code = lintExitCode(reported);
|
|
1680
|
+
console.log(`\n${String(totals.findings)} finding(s): ${String(totals.errors)} error, ` +
|
|
1681
|
+
`${String(totals.warnings)} warning — exit ${String(code)}`);
|
|
1394
1682
|
}
|
|
1395
|
-
return
|
|
1683
|
+
return reported;
|
|
1396
1684
|
}
|
|
1397
1685
|
/** Single-line lint summary for SessionStart hooks — minimal token cost. */
|
|
1398
1686
|
function printLintSummary(report) {
|
|
@@ -1937,6 +2225,15 @@ function scaffoldSpec(args) {
|
|
|
1937
2225
|
(0, node_fs_1.writeFileSync)(specAbs, source);
|
|
1938
2226
|
console.log(`Adopted ${target} → ${specPath} (${tier}, ${String(sectionCount)} section${sectionCount === 1 ? "" : "s"}). ` +
|
|
1939
2227
|
`Run \`vigiles compile\` and review the diff; the \`/strengthen\` skill upgrades prose to verified rules.`);
|
|
2228
|
+
// Adoption is faithful by design: it infers NO rules and extracts NO refs,
|
|
2229
|
+
// so a raw adoption verifies nothing on its own. Saying so is the whole
|
|
2230
|
+
// fix — the cost (the file becomes a build artifact, edits move into TS)
|
|
2231
|
+
// lands immediately, and without this line the benefit reads as zero
|
|
2232
|
+
// rather than as not-yet-claimed. Deliberately not a heuristic extractor:
|
|
2233
|
+
// guessing refs out of prose is what got `doc-refs` disabled.
|
|
2234
|
+
if (tier === "raw")
|
|
2235
|
+
console.log(` ℹ 0 refs extracted — this spec verifies nothing yet. Wrap paths in \`file()\` ` +
|
|
2236
|
+
`and commands in \`cmd()\` to make \`compile\` check them.`);
|
|
1940
2237
|
}
|
|
1941
2238
|
return;
|
|
1942
2239
|
}
|
|
@@ -3566,7 +3863,12 @@ function checkSkillResourceResolves(config, silent, adapter, scanRoot) {
|
|
|
3566
3863
|
if (found.length > 0 && !silent) {
|
|
3567
3864
|
console.log("\nSkill-resource check:\n");
|
|
3568
3865
|
for (const s of found) {
|
|
3569
|
-
const msg = `${s.name}: bundled resource "${s.finding.ref}" (line ${String(s.finding.line)}) is referenced but missing — the agent reads the instruction and gets nothing
|
|
3866
|
+
const msg = `${s.name}: bundled resource "${s.finding.ref}" (line ${String(s.finding.line)}) is referenced but missing — the agent reads the instruction and gets nothing.` +
|
|
3867
|
+
// The main false-positive source in a skills monorepo, where a SKILL.md
|
|
3868
|
+
// legitimately names a repo-root path. The fix already exists and works;
|
|
3869
|
+
// it was documented only in docs/skills-monorepo.md, so a CI log gave no
|
|
3870
|
+
// hint and the rule read as broken rather than misconfigured.
|
|
3871
|
+
` If it resolves from the repo root instead, add its directory to \`sharedDirs\` in .vigilesrc.json.`;
|
|
3570
3872
|
console.log(` ${sev === "error" ? "✗" : "⚠"} ${s.path}: ${msg}`);
|
|
3571
3873
|
ghAnnotate(sev === "error" ? "error" : "warning", msg, s.path);
|
|
3572
3874
|
}
|
|
@@ -4587,6 +4889,7 @@ const COMMAND_HELP = {
|
|
|
4587
4889
|
usage: " vigiles audit [dir...] Grade it on your machine. Reports everything, fails nothing.",
|
|
4588
4890
|
detail: [
|
|
4589
4891
|
" 2+ dirs → a leaderboard. Writes vigiles-report.html + .json (auto-gitignored).",
|
|
4892
|
+
" --single audit this dir as ONE harness, even if it holds many bundles",
|
|
4590
4893
|
" The executing checks (run your hooks · live MCP · do skills fire?) run only",
|
|
4591
4894
|
" interactively — audit asks once and remembers; automation uses the testing API.",
|
|
4592
4895
|
],
|
|
@@ -4721,7 +5024,12 @@ function printUsage(command) {
|
|
|
4721
5024
|
console.log("New here? Start with `vigiles audit .`");
|
|
4722
5025
|
if (command && command !== "--help") {
|
|
4723
5026
|
console.log(`\nUnknown command: "${command}"`);
|
|
4724
|
-
|
|
5027
|
+
// 2, not 1 — `docs/cli.md` fixes the contract as 1 = "I ran, and what you
|
|
5028
|
+
// asked about is bad", 2 = "I could not do what you asked". A typo'd verb is
|
|
5029
|
+
// the second, and the unknown-FLAG path (cli-flag-check.ts) already exits 2.
|
|
5030
|
+
// A script telling "found problems" from "could not start" by exit code read
|
|
5031
|
+
// a mistyped command as a finding.
|
|
5032
|
+
process.exit(2);
|
|
4725
5033
|
}
|
|
4726
5034
|
}
|
|
4727
5035
|
// ---------------------------------------------------------------------------
|
|
@@ -5914,6 +6222,14 @@ async function runHookProgramCommand(file) {
|
|
|
5914
6222
|
function ensureReportGitignored(cwd, entries) {
|
|
5915
6223
|
if (entries.length === 0)
|
|
5916
6224
|
return;
|
|
6225
|
+
// An `--out` outside the repo produced entries like
|
|
6226
|
+
// `../../../../private/tmp/x/vigiles-report.json`, which ignore NOTHING —
|
|
6227
|
+
// .gitignore does not reach outside its own tree — and accumulate one dead
|
|
6228
|
+
// block per output path. Worse in principle than in practice: `audit` is
|
|
6229
|
+
// documented as a read-only report, and this made it edit a tracked file for
|
|
6230
|
+
// no benefit at all. Inside the repo the write is expected and documented.
|
|
6231
|
+
if (entries.some((e) => e.startsWith("..") || (0, node_path_1.isAbsolute)(e)))
|
|
6232
|
+
return;
|
|
5917
6233
|
const gi = (0, node_path_1.resolve)(cwd, ".gitignore");
|
|
5918
6234
|
try {
|
|
5919
6235
|
if (!(0, node_fs_1.existsSync)(gi)) {
|
|
@@ -6429,10 +6745,37 @@ async function main() {
|
|
|
6429
6745
|
// carries `market` into its explanation and exits 2 — this repo's own rule
|
|
6430
6746
|
// that 1 is "I measured, and it's bad" and 2 is "I could not do what you
|
|
6431
6747
|
// asked". Nothing was measured here, so it is a 2.
|
|
6432
|
-
|
|
6433
|
-
|
|
6748
|
+
// `--single` pins the SINGLE-harness reading of the given directory, whatever
|
|
6749
|
+
// is nested inside it. Without it, a repo holding many bundles auto-switches
|
|
6750
|
+
// to the leaderboard and there is no way back — so the full ring report for
|
|
6751
|
+
// the ROOT was simply unreachable, and the reported workaround was a CI job
|
|
6752
|
+
// looping `audit` over 29 directories. The mode branch already existed; this
|
|
6753
|
+
// only stops it being decided for you.
|
|
6754
|
+
const single = args.includes("--single");
|
|
6755
|
+
// `--single` names ONE harness, so more than one explicit directory is a
|
|
6756
|
+
// contradiction. Refuse it (exit 2 = "could not do what you asked") rather
|
|
6757
|
+
// than auditing the first and dropping the rest — silently honouring half
|
|
6758
|
+
// an argument list is the same defect class as the ignored `--out` above.
|
|
6759
|
+
if (single && dirs.length > 1) {
|
|
6760
|
+
console.error(`--single audits ONE directory as one harness, but ${String(dirs.length)} were given. ` +
|
|
6761
|
+
`Drop --single for a leaderboard, or pass a single directory.`);
|
|
6762
|
+
process.exit(2);
|
|
6763
|
+
}
|
|
6764
|
+
const targets = !single && market && market.onDisk.length > 0
|
|
6765
|
+
? [...market.onDisk]
|
|
6766
|
+
: dirs;
|
|
6767
|
+
if (!single && targets.length > 1) {
|
|
6434
6768
|
// Multiple targets → rank them (the leaderboard engine). `--md` emits the
|
|
6435
6769
|
// publishable Markdown table (a README / gist / the leaderboard site).
|
|
6770
|
+
//
|
|
6771
|
+
// `--out` writes nothing here: the per-bundle HTML/JSON report is built
|
|
6772
|
+
// in the single-target branch below, and this one only prints a table.
|
|
6773
|
+
// SAY SO. A silent no-op is how a CI job ships an empty artifact and
|
|
6774
|
+
// stays green — which is exactly how this was found, and the same
|
|
6775
|
+
// never-fail-silently shape as the rest of this file.
|
|
6776
|
+
if (args.some((a) => a.startsWith("--out=")) && !json)
|
|
6777
|
+
console.log(`⚠ --out is ignored here: ${String(targets.length)} bundles → leaderboard mode, ` +
|
|
6778
|
+
`which produces no per-bundle report. Run audit per directory to write one.`);
|
|
6436
6779
|
const scores = (0, leaderboard_js_1.rankPlugins)(targets);
|
|
6437
6780
|
const text = args.includes("--md")
|
|
6438
6781
|
? (0, leaderboard_js_1.formatLeaderboardMarkdown)(scores)
|
|
@@ -79,6 +79,24 @@ const DECISION_BLOCK = /"decision"\s*:\s*"(block|deny)"/;
|
|
|
79
79
|
* (Both require a structured response, as opposed to the legacy field.)
|
|
80
80
|
*/
|
|
81
81
|
const PERMISSION_DENY = /"permissionDecision"\s*:\s*"(deny|ask)"/;
|
|
82
|
+
/**
|
|
83
|
+
* A `"continue": false` halt — the OTHER documented way a Claude Code hook stops
|
|
84
|
+
* an action (it ends the turn and returns `stopReason` to the agent).
|
|
85
|
+
*
|
|
86
|
+
* Read ONLY as a SUPPRESSOR of `wrong-field`, never as a block ATTEMPT, and the
|
|
87
|
+
* asymmetry is the whole point. #174 proposed adding it alongside the three
|
|
88
|
+
* mechanisms above; doing that would have made `wrong-event` fire on a hook that
|
|
89
|
+
* works, because a halt is NOT event-scoped — it stops the turn from
|
|
90
|
+
* `SessionStart` just as it does from `PreToolUse`, which is precisely the set
|
|
91
|
+
* `wrong-event` flags. For a rule that can be wired at `error`, a false positive
|
|
92
|
+
* costs more than a miss: it fails a correct build, and a rule that fails
|
|
93
|
+
* correct builds gets switched off rather than fixed.
|
|
94
|
+
*
|
|
95
|
+
* What it legitimately fixes is the reverse: a hook on a permission-gated event
|
|
96
|
+
* that pairs a legacy `"decision":"block"` with a real halt was told "nothing is
|
|
97
|
+
* blocked" while it blocked.
|
|
98
|
+
*/
|
|
99
|
+
const CONTINUE_FALSE = /"continue"\s*:\s*false/;
|
|
82
100
|
// ---------------------------------------------------------------------------
|
|
83
101
|
// Detector
|
|
84
102
|
// ---------------------------------------------------------------------------
|
|
@@ -125,6 +143,8 @@ function hookBlockIssues(entries, opts) {
|
|
|
125
143
|
const hasExit2 = EXIT_2.test(text) || EXIT_2_CODE.test(text);
|
|
126
144
|
const hasDecisionBlock = DECISION_BLOCK.test(text);
|
|
127
145
|
const hasPermissionDeny = PERMISSION_DENY.test(text);
|
|
146
|
+
const hasContinueFalse = CONTINUE_FALSE.test(text);
|
|
147
|
+
// Deliberately NOT `|| hasContinueFalse` — see CONTINUE_FALSE.
|
|
128
148
|
const triesBlock = hasExit2 || hasDecisionBlock || hasPermissionDeny;
|
|
129
149
|
if (!triesBlock)
|
|
130
150
|
continue;
|
|
@@ -143,7 +163,10 @@ function hookBlockIssues(entries, opts) {
|
|
|
143
163
|
}
|
|
144
164
|
else if (permissionDecisionEvents.has(entry.event) &&
|
|
145
165
|
hasDecisionBlock &&
|
|
146
|
-
!hasPermissionDeny
|
|
166
|
+
!hasPermissionDeny &&
|
|
167
|
+
// A halt alongside the legacy field DOES stop the action, so the legacy
|
|
168
|
+
// field being ignored costs nothing. Flagging it would be a false alarm.
|
|
169
|
+
!hasContinueFalse) {
|
|
147
170
|
// wrong-field: on a permission-gated event, uses the legacy field.
|
|
148
171
|
kind = "wrong-field";
|
|
149
172
|
message =
|
|
@@ -46,5 +46,19 @@ export interface HookProtocol {
|
|
|
46
46
|
* shell-hook harness declares a non-empty set.
|
|
47
47
|
*/
|
|
48
48
|
readonly injectableEvents: readonly string[];
|
|
49
|
+
/**
|
|
50
|
+
* The boolean stdout field whose `false` value HALTS THE WHOLE TURN, if the
|
|
51
|
+
* harness has one (Claude Code: `"continue"`). Distinct from a deny: a deny
|
|
52
|
+
* refuses one tool call, this stops the iteration and hands `stopReason` back
|
|
53
|
+
* to the agent as text — so authors reach for it exactly when they want to
|
|
54
|
+
* explain themselves, and a guard written that way still prevents the action.
|
|
55
|
+
*
|
|
56
|
+
* Optional (additive, non-breaking) and per-harness on purpose. It is
|
|
57
|
+
* DOCUMENTED for Claude Code and UNVERIFIED for Codex, whose protocol notes
|
|
58
|
+
* only record the shared exit-2 / `decision` / `permissionDecision` model — so
|
|
59
|
+
* Codex leaves it unset rather than inheriting a claim nobody measured. Read
|
|
60
|
+
* by `decideHook`; absent ⇒ no field halts the turn on this harness.
|
|
61
|
+
*/
|
|
62
|
+
readonly haltsTurnField?: string;
|
|
49
63
|
}
|
|
50
64
|
//# sourceMappingURL=hook-protocol.d.ts.map
|