vigiles 24.0.0 → 25.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +50 -20
- package/dist/core/compile-generator.d.ts +6 -0
- package/dist/core/compile-generator.js +4 -1
- package/dist/core/compile.d.ts +53 -1
- package/dist/core/compile.js +20 -4
- package/dist/core/repo-path.d.ts +41 -0
- package/dist/core/repo-path.js +19 -0
- package/dist/run-hook.d.ts +27 -0
- package/dist/run-hook.js +31 -12
- package/dist/test.d.ts +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ exports.discoverNestedBundles = discoverNestedBundles;
|
|
|
17
17
|
const node_fs_1 = require("node:fs");
|
|
18
18
|
const node_path_1 = require("node:path");
|
|
19
19
|
const minimatch_1 = require("minimatch");
|
|
20
|
+
const repo_path_js_1 = require("./core/repo-path.js");
|
|
20
21
|
const node_child_process_1 = require("node:child_process");
|
|
21
22
|
const glob_1 = require("glob");
|
|
22
23
|
const generate_types_js_1 = require("./core/generate-types.js");
|
|
@@ -288,11 +289,14 @@ function printWarnings(specFile, warnings) {
|
|
|
288
289
|
/** Compile a generator-skill spec from source → SKILL.md. Returns validity. */
|
|
289
290
|
function compileGeneratorSkillToFile(specPath, source) {
|
|
290
291
|
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
291
|
-
const {
|
|
292
|
+
const { artifact, errors } = (0, compile_generator_js_1.compileGeneratorSkill)(source, {
|
|
292
293
|
basePath: process.cwd(),
|
|
293
294
|
specFile: specPath,
|
|
294
295
|
});
|
|
295
|
-
|
|
296
|
+
// Written only when the compile is clean: `artifact` is null otherwise, and
|
|
297
|
+
// `writeArtifact` takes nothing else.
|
|
298
|
+
if (artifact)
|
|
299
|
+
writeArtifact(outputPath, artifact);
|
|
296
300
|
if (errors.length === 0) {
|
|
297
301
|
console.log(`\n✓ ${specPath} → ${outputPath} (generator skill)`);
|
|
298
302
|
return true;
|
|
@@ -393,14 +397,17 @@ function writeInstructionMirrors(primaryOutput, harnesses) {
|
|
|
393
397
|
/** Compile a declarative SkillSpec → SKILL.md. */
|
|
394
398
|
function compileSkillToFile(spec, specPath, dialect) {
|
|
395
399
|
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
396
|
-
const {
|
|
400
|
+
const { artifact, errors, warnings } = (0, compile_js_1.compileSkill)(spec, {
|
|
397
401
|
basePath: process.cwd(),
|
|
398
402
|
specFile: specPath,
|
|
399
403
|
// The SKILL.md frontmatter profile comes from the resolved harness — a Codex
|
|
400
404
|
// repo gets a minimal (name + description) SKILL.md; CC gets the full set.
|
|
401
405
|
dialect,
|
|
402
406
|
});
|
|
403
|
-
|
|
407
|
+
// Written only when the compile is clean: `artifact` is null otherwise, and
|
|
408
|
+
// `writeArtifact` takes nothing else.
|
|
409
|
+
if (artifact)
|
|
410
|
+
writeArtifact(outputPath, artifact);
|
|
404
411
|
if (errors.length === 0) {
|
|
405
412
|
console.log(`\n✓ ${specPath} → ${outputPath}`);
|
|
406
413
|
printWarnings(specPath, warnings);
|
|
@@ -414,12 +421,15 @@ function compileSkillToFile(spec, specPath, dialect) {
|
|
|
414
421
|
/** Compile a subagent spec → agents/<name>.md (with its result-contract section). */
|
|
415
422
|
function compileAgentToFile(spec, specPath, dialect) {
|
|
416
423
|
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
417
|
-
const {
|
|
424
|
+
const { artifact, errors, warnings } = (0, compile_js_1.compileAgent)(spec, {
|
|
418
425
|
basePath: process.cwd(),
|
|
419
426
|
specFile: specPath,
|
|
420
427
|
dialect,
|
|
421
428
|
});
|
|
422
|
-
|
|
429
|
+
// Written only when the compile is clean: `artifact` is null otherwise, and
|
|
430
|
+
// `writeArtifact` takes nothing else.
|
|
431
|
+
if (artifact)
|
|
432
|
+
writeArtifact(outputPath, artifact);
|
|
423
433
|
if (errors.length === 0) {
|
|
424
434
|
console.log(`\n✓ ${specPath} → ${outputPath}`);
|
|
425
435
|
printWarnings(specPath, warnings);
|
|
@@ -437,11 +447,14 @@ function compileAgentToFile(spec, specPath, dialect) {
|
|
|
437
447
|
*/
|
|
438
448
|
function compileRailwayToFile(spec, specPath, knownAgents) {
|
|
439
449
|
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
440
|
-
const {
|
|
450
|
+
const { artifact, errors } = (0, compile_js_1.compileRailway)(spec, {
|
|
441
451
|
specFile: specPath,
|
|
442
452
|
knownAgents,
|
|
443
453
|
});
|
|
444
|
-
|
|
454
|
+
// Written only when the compile is clean: `artifact` is null otherwise, and
|
|
455
|
+
// `writeArtifact` takes nothing else.
|
|
456
|
+
if (artifact)
|
|
457
|
+
writeArtifact(outputPath, artifact);
|
|
445
458
|
if (errors.length === 0) {
|
|
446
459
|
console.log(`\n✓ ${specPath} → ${outputPath}`);
|
|
447
460
|
return true;
|
|
@@ -1294,6 +1307,18 @@ async function checkSpecRefs(config, silent, dialect) {
|
|
|
1294
1307
|
}
|
|
1295
1308
|
return { issues: found.length, errors: sev === "error" ? found.length : 0 };
|
|
1296
1309
|
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Write a compiled artifact. Accepts ONLY a {@link StampedMarkdown}, so a body
|
|
1312
|
+
* that failed to compile cannot reach the disk — there is no stamp to pass.
|
|
1313
|
+
*
|
|
1314
|
+
* This replaces four copies of `writeFileSync(path, markdown)` that ran BEFORE
|
|
1315
|
+
* their error check (#173, reproduced in the skill/subagent/railway/generator
|
|
1316
|
+
* compilers after the CLAUDE.md one was fixed). Guarding four call sites would
|
|
1317
|
+
* have left the fifth writable; the type leaves nothing to remember.
|
|
1318
|
+
*/
|
|
1319
|
+
function writeArtifact(outputPath, artifact) {
|
|
1320
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), artifact);
|
|
1321
|
+
}
|
|
1297
1322
|
async function runLint(restArgs, flags, config) {
|
|
1298
1323
|
const summary = flags.includes("--summary");
|
|
1299
1324
|
const json = flags.includes("--json");
|
|
@@ -6222,14 +6247,12 @@ async function runHookProgramCommand(file) {
|
|
|
6222
6247
|
function ensureReportGitignored(cwd, entries) {
|
|
6223
6248
|
if (entries.length === 0)
|
|
6224
6249
|
return;
|
|
6225
|
-
//
|
|
6226
|
-
//
|
|
6227
|
-
//
|
|
6228
|
-
//
|
|
6229
|
-
//
|
|
6230
|
-
//
|
|
6231
|
-
if (entries.some((e) => e.startsWith("..") || (0, node_path_1.isAbsolute)(e)))
|
|
6232
|
-
return;
|
|
6250
|
+
// 🔴 THE GUARD THAT USED TO BE HERE IS GONE, and its absence is the point.
|
|
6251
|
+
// It checked at the write site that no entry escaped the repo (#176.8) — which
|
|
6252
|
+
// worked, and left the bug writable: the next caller to build an entry list
|
|
6253
|
+
// still got a bare `string[]`. `RepoRelativePath` moves the check into the
|
|
6254
|
+
// TYPE, so an escaping path cannot be handed to this function at all. One
|
|
6255
|
+
// place mints them (`repoRelative`), and it returns null instead.
|
|
6233
6256
|
const gi = (0, node_path_1.resolve)(cwd, ".gitignore");
|
|
6234
6257
|
try {
|
|
6235
6258
|
if (!(0, node_fs_1.existsSync)(gi)) {
|
|
@@ -7135,10 +7158,17 @@ async function main() {
|
|
|
7135
7158
|
// backslashes (`relative()` yields `reports\x` on Windows, which would
|
|
7136
7159
|
// never match `reports/x`).
|
|
7137
7160
|
if (wroteReports.length > 0) {
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7161
|
+
// Only paths that PROVABLY sit inside the repo can be minted, so an
|
|
7162
|
+
// `--out` pointing elsewhere yields nothing to write rather than a
|
|
7163
|
+
// dead `../../..` entry.
|
|
7164
|
+
const rel = wroteReports
|
|
7165
|
+
.map((f) => (0, repo_path_js_1.repoRelative)(process.cwd(), (0, node_path_1.resolve)(outDir, f), {
|
|
7166
|
+
relative: node_path_1.relative,
|
|
7167
|
+
resolve: node_path_1.resolve,
|
|
7168
|
+
isAbsolute: node_path_1.isAbsolute,
|
|
7169
|
+
sep: node_path_1.sep,
|
|
7170
|
+
}))
|
|
7171
|
+
.filter((p) => p !== null);
|
|
7142
7172
|
ensureReportGitignored(process.cwd(), rel);
|
|
7143
7173
|
}
|
|
7144
7174
|
// A shareable deep-link for a public GitHub repo: the in-browser demo
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* Gate references (`cmd`/`file`/`project`) are collected and verified, so the
|
|
26
26
|
* cross-referencing moat works on generators too (literal args only).
|
|
27
27
|
*/
|
|
28
|
+
import { type StampedMarkdown } from "./compile.js";
|
|
28
29
|
export interface GeneratorError {
|
|
29
30
|
type: "stale-file" | "stale-command";
|
|
30
31
|
message: string;
|
|
@@ -34,6 +35,11 @@ export interface CompileGeneratorResult {
|
|
|
34
35
|
errors: GeneratorError[];
|
|
35
36
|
}
|
|
36
37
|
export interface CompileGeneratorSkillResult {
|
|
38
|
+
/**
|
|
39
|
+
* The stamped artifact — present ONLY when `errors` is empty. `null` is what
|
|
40
|
+
* makes a failed compile unwritable: `writeArtifact` accepts nothing else.
|
|
41
|
+
*/
|
|
42
|
+
artifact: StampedMarkdown | null;
|
|
37
43
|
markdown: string;
|
|
38
44
|
errors: GeneratorError[];
|
|
39
45
|
}
|
|
@@ -278,6 +278,9 @@ function compileGeneratorSkill(source, options = {}) {
|
|
|
278
278
|
!genArg.body) {
|
|
279
279
|
return {
|
|
280
280
|
markdown: "",
|
|
281
|
+
// No stamp for a spec that did not compile — the error branch has nothing
|
|
282
|
+
// to write, which is the whole point of the field.
|
|
283
|
+
artifact: null,
|
|
281
284
|
errors: [
|
|
282
285
|
{
|
|
283
286
|
type: "stale-command",
|
|
@@ -300,7 +303,7 @@ function compileGeneratorSkill(source, options = {}) {
|
|
|
300
303
|
}
|
|
301
304
|
fm.push("", "---");
|
|
302
305
|
const content = `${fm.join("\n")}\n\n${body.trim()}\n`;
|
|
303
|
-
return {
|
|
306
|
+
return { ...(0, compile_js_1.seal)(content, errors, specFile), errors };
|
|
304
307
|
}
|
|
305
308
|
/**
|
|
306
309
|
* Compile a generator's SOURCE text to SKILL.md markdown + verified-ref
|
package/dist/core/compile.d.ts
CHANGED
|
@@ -10,7 +10,44 @@ import type { HarnessDialect } from "./dialect.js";
|
|
|
10
10
|
/** @internal Compute SHA-256 hash of content (excluding any existing hash line). */
|
|
11
11
|
export declare function computeHash(content: string): string;
|
|
12
12
|
/** @internal Prepend a hash comment to compiled content. */
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* A compiled body that carries a valid integrity stamp — mintable ONLY by
|
|
15
|
+
* {@link addHash}, and by construction only handed out for a CLEAN compile.
|
|
16
|
+
*
|
|
17
|
+
* 🔴 WHY A BRAND AND NOT A CHECK AT THE WRITE SITE. #173 was a `CLAUDE.md`
|
|
18
|
+
* written while its refs were known-dead: `compile` printed the errors, exited
|
|
19
|
+
* 1, and wrote the file anyway, stamped. `lint` then verified the stamp and
|
|
20
|
+
* exited 0 over an artifact that names files which do not exist. The fix that
|
|
21
|
+
* shipped moved the write behind an error check in ONE compiler — and the same
|
|
22
|
+
* three lines sat unchanged in four siblings (skill, subagent, railway,
|
|
23
|
+
* generator), where the lint-side backstop does not even reach because
|
|
24
|
+
* `spec-refs` only inspects `claude` specs. Reproduced end to end after that
|
|
25
|
+
* fix: a skill spec with a stale ref still produced a stamped `SKILL.md` and a
|
|
26
|
+
* green `lint`.
|
|
27
|
+
*
|
|
28
|
+
* Fixing five write sites leaves the class writable — the sixth compiler would
|
|
29
|
+
* be written the same way. Branding the STAMP moves the guarantee to where it is
|
|
30
|
+
* produced: `writeArtifact` accepts nothing else, and an erroring compile has no
|
|
31
|
+
* stamp to give it.
|
|
32
|
+
*/
|
|
33
|
+
export type StampedMarkdown = string & {
|
|
34
|
+
readonly __stamped: unique symbol;
|
|
35
|
+
};
|
|
36
|
+
export declare function addHash(content: string, specFile: string): StampedMarkdown;
|
|
37
|
+
/**
|
|
38
|
+
* How every compiler finishes: the rendered body, plus a STAMPED artifact that
|
|
39
|
+
* exists only when the compile is clean.
|
|
40
|
+
*
|
|
41
|
+
* `markdown` stays a plain string because callers legitimately want the draft
|
|
42
|
+
* even when it is wrong — `adoptDiff` diffs "what the spec WOULD produce"
|
|
43
|
+
* against the file on disk, and a stale ref must not stop that. `artifact` is
|
|
44
|
+
* what a writer needs, and it is `null` the moment there is an error, so the
|
|
45
|
+
* write cannot happen without narrowing.
|
|
46
|
+
*/
|
|
47
|
+
export declare function seal(body: string, errors: readonly CompileError[], specFile: string): {
|
|
48
|
+
markdown: string;
|
|
49
|
+
artifact: StampedMarkdown | null;
|
|
50
|
+
};
|
|
14
51
|
/** @internal Check if a file's hash matches its content. Returns null if no hash found. */
|
|
15
52
|
export declare function verifyHash(content: string): {
|
|
16
53
|
valid: boolean;
|
|
@@ -74,6 +111,11 @@ export interface CompileClaudeOptions {
|
|
|
74
111
|
*/
|
|
75
112
|
export declare function compileClaude(spec: ClaudeSpec, options?: CompileClaudeOptions): CompileClaudeResult;
|
|
76
113
|
export interface CompileSkillResult {
|
|
114
|
+
/**
|
|
115
|
+
* The stamped artifact — present ONLY when `errors` is empty. `null` is what
|
|
116
|
+
* makes a failed compile unwritable: `writeArtifact` accepts nothing else.
|
|
117
|
+
*/
|
|
118
|
+
artifact: StampedMarkdown | null;
|
|
77
119
|
markdown: string;
|
|
78
120
|
errors: CompileError[];
|
|
79
121
|
/** Non-blocking advisories (e.g. an over-long inline code block). */
|
|
@@ -90,6 +132,11 @@ export declare function compileSkill(spec: SkillSpec, options?: {
|
|
|
90
132
|
dialect?: HarnessDialect;
|
|
91
133
|
}): CompileSkillResult;
|
|
92
134
|
export interface CompileAgentResult {
|
|
135
|
+
/**
|
|
136
|
+
* The stamped artifact — present ONLY when `errors` is empty. `null` is what
|
|
137
|
+
* makes a failed compile unwritable: `writeArtifact` accepts nothing else.
|
|
138
|
+
*/
|
|
139
|
+
artifact: StampedMarkdown | null;
|
|
93
140
|
markdown: string;
|
|
94
141
|
errors: CompileError[];
|
|
95
142
|
/** Non-blocking advisories (e.g. an over-long inline code block). */
|
|
@@ -113,6 +160,11 @@ export interface CompileRailwayOptions {
|
|
|
113
160
|
specFile?: string;
|
|
114
161
|
}
|
|
115
162
|
export interface CompileRailwayResult {
|
|
163
|
+
/**
|
|
164
|
+
* The stamped artifact — present ONLY when `errors` is empty. `null` is what
|
|
165
|
+
* makes a failed compile unwritable: `writeArtifact` accepts nothing else.
|
|
166
|
+
*/
|
|
167
|
+
artifact: StampedMarkdown | null;
|
|
116
168
|
markdown: string;
|
|
117
169
|
errors: CompileError[];
|
|
118
170
|
}
|
package/dist/core/compile.js
CHANGED
|
@@ -11,6 +11,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.computeHash = computeHash;
|
|
13
13
|
exports.addHash = addHash;
|
|
14
|
+
exports.seal = seal;
|
|
14
15
|
exports.verifyHash = verifyHash;
|
|
15
16
|
exports.estimateTokens = estimateTokens;
|
|
16
17
|
exports.validateFileRef = validateFileRef;
|
|
@@ -53,7 +54,6 @@ const DEFAULT_TARGET = "CLAUDE.md";
|
|
|
53
54
|
function computeHash(content) {
|
|
54
55
|
return (0, hash_js_1.sha256short)((0, integrity_js_1.findIntegrityHeader)(content)?.withoutHeader ?? content);
|
|
55
56
|
}
|
|
56
|
-
/** @internal Prepend a hash comment to compiled content. */
|
|
57
57
|
function addHash(content, specFile) {
|
|
58
58
|
// 🔴 THE LAST GATE BEFORE A COMPILED FILE IS WRITTEN. Every compile path returns through here
|
|
59
59
|
// (four call sites), which makes it the one place a whole class of defect can be stopped.
|
|
@@ -72,8 +72,24 @@ function addHash(content, specFile) {
|
|
|
72
72
|
`where a string was expected, and JavaScript stringified it. Check the arguments to ` +
|
|
73
73
|
`input()/file()/cmd() and friends: they take strings, not option objects.`);
|
|
74
74
|
}
|
|
75
|
+
// The ONE mint. Every stamped artifact in the codebase originates here, which
|
|
76
|
+
// is what makes the brand meaningful rather than decorative.
|
|
75
77
|
return (0, integrity_js_1.placeIntegrityHeader)(content, computeHash(content), specFile);
|
|
76
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* How every compiler finishes: the rendered body, plus a STAMPED artifact that
|
|
81
|
+
* exists only when the compile is clean.
|
|
82
|
+
*
|
|
83
|
+
* `markdown` stays a plain string because callers legitimately want the draft
|
|
84
|
+
* even when it is wrong — `adoptDiff` diffs "what the spec WOULD produce"
|
|
85
|
+
* against the file on disk, and a stale ref must not stop that. `artifact` is
|
|
86
|
+
* what a writer needs, and it is `null` the moment there is an error, so the
|
|
87
|
+
* write cannot happen without narrowing.
|
|
88
|
+
*/
|
|
89
|
+
function seal(body, errors, specFile) {
|
|
90
|
+
const stamped = addHash(body, specFile);
|
|
91
|
+
return { markdown: stamped, artifact: errors.length === 0 ? stamped : null };
|
|
92
|
+
}
|
|
77
93
|
/** @internal Check if a file's hash matches its content. Returns null if no hash found. */
|
|
78
94
|
function verifyHash(content) {
|
|
79
95
|
const found = (0, integrity_js_1.findIntegrityHeader)(content);
|
|
@@ -859,7 +875,7 @@ function compileSkill(spec, options = {}) {
|
|
|
859
875
|
(marker ? marker + "\n\n" : "") +
|
|
860
876
|
sections.trim() +
|
|
861
877
|
"\n";
|
|
862
|
-
return {
|
|
878
|
+
return { ...seal(content, errors, specFile), errors, warnings };
|
|
863
879
|
}
|
|
864
880
|
// ---------------------------------------------------------------------------
|
|
865
881
|
// Compile a subagent spec → agents/<name>.md
|
|
@@ -1058,7 +1074,7 @@ function compileAgent(spec, options) {
|
|
|
1058
1074
|
(marker ? marker + "\n\n" : "") +
|
|
1059
1075
|
body.trim() +
|
|
1060
1076
|
"\n";
|
|
1061
|
-
return {
|
|
1077
|
+
return { ...seal(content, errors, specFile), errors, warnings };
|
|
1062
1078
|
}
|
|
1063
1079
|
/** Verify a railway: non-empty, bounded recovery, every delegate target real. */
|
|
1064
1080
|
function validateRailway(rw, knownAgents) {
|
|
@@ -1126,7 +1142,7 @@ function compileRailway(rw, options = {}) {
|
|
|
1126
1142
|
const errors = validateRailway(rw, options.knownAgents);
|
|
1127
1143
|
const specFile = options.specFile ?? `${rw.name}.railway.spec.ts`;
|
|
1128
1144
|
const content = renderRailwayMarkdown(rw) + "\n";
|
|
1129
|
-
return {
|
|
1145
|
+
return { ...seal(content, errors, specFile), errors };
|
|
1130
1146
|
}
|
|
1131
1147
|
/** Check if a generated file's hash is intact. */
|
|
1132
1148
|
function checkFileHash(filePath) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RepoRelativePath` — a path that is provably INSIDE the repository.
|
|
3
|
+
*
|
|
4
|
+
* 🔴 WHY A TYPE AND NOT A CHECK. `audit --out=/tmp/x` appended entries like
|
|
5
|
+
* `../../../../private/tmp/x/vigiles-report.json` to the user's `.gitignore`
|
|
6
|
+
* (#176.8). Those ignore nothing — `.gitignore` does not reach outside its own
|
|
7
|
+
* tree — and accumulate one dead block per output path, in a file the tool is
|
|
8
|
+
* documented as never writing. The fix that shipped was a guard at the write
|
|
9
|
+
* site, which works and leaves the bug WRITABLE: the next caller to build an
|
|
10
|
+
* entry list gets a `string[]` and no reason to think twice.
|
|
11
|
+
*
|
|
12
|
+
* This makes it unwritable instead. `ensureReportGitignored` accepts only
|
|
13
|
+
* `RepoRelativePath[]`, and the only way to obtain one is `repoRelative()`,
|
|
14
|
+
* which returns `null` for anything that escapes the root. There is no cast at
|
|
15
|
+
* the call site and no second guard to keep in sync — a path that leaves the
|
|
16
|
+
* repo cannot reach the writer, because it cannot be given the type.
|
|
17
|
+
*
|
|
18
|
+
* The brand is the pattern this codebase already uses for `VerifiedPath` and
|
|
19
|
+
* friends: a nominal marker on `string` that only a smart constructor mints.
|
|
20
|
+
*/
|
|
21
|
+
declare const REPO_RELATIVE: unique symbol;
|
|
22
|
+
/** A POSIX-separated path known to resolve inside the repository root. */
|
|
23
|
+
export type RepoRelativePath = string & {
|
|
24
|
+
readonly [REPO_RELATIVE]: true;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Mint a {@link RepoRelativePath}, or `null` when the target escapes `root`.
|
|
28
|
+
*
|
|
29
|
+
* Rejects an absolute path and anything whose relative form starts with `..`.
|
|
30
|
+
* Normalizes to POSIX separators, because `.gitignore` patterns are POSIX and a
|
|
31
|
+
* Windows `reports\x` would never match `reports/x` — a second silent-miss that
|
|
32
|
+
* lived next to the first.
|
|
33
|
+
*/
|
|
34
|
+
export declare function repoRelative(root: string, target: string, io: {
|
|
35
|
+
relative: (from: string, to: string) => string;
|
|
36
|
+
resolve: (...parts: string[]) => string;
|
|
37
|
+
isAbsolute: (p: string) => boolean;
|
|
38
|
+
sep: string;
|
|
39
|
+
}): RepoRelativePath | null;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=repo-path.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.repoRelative = repoRelative;
|
|
4
|
+
/**
|
|
5
|
+
* Mint a {@link RepoRelativePath}, or `null` when the target escapes `root`.
|
|
6
|
+
*
|
|
7
|
+
* Rejects an absolute path and anything whose relative form starts with `..`.
|
|
8
|
+
* Normalizes to POSIX separators, because `.gitignore` patterns are POSIX and a
|
|
9
|
+
* Windows `reports\x` would never match `reports/x` — a second silent-miss that
|
|
10
|
+
* lived next to the first.
|
|
11
|
+
*/
|
|
12
|
+
function repoRelative(root, target, io) {
|
|
13
|
+
const rel = io.relative(root, io.resolve(root, target));
|
|
14
|
+
if (rel === "" || rel.startsWith("..") || io.isAbsolute(rel))
|
|
15
|
+
return null;
|
|
16
|
+
const posix = io.sep === "/" ? rel : rel.split(io.sep).join("/");
|
|
17
|
+
return posix;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=repo-path.js.map
|
package/dist/run-hook.d.ts
CHANGED
|
@@ -187,6 +187,13 @@ export interface HookRunResult extends ScriptRunResult {
|
|
|
187
187
|
* a halt sets both.
|
|
188
188
|
*/
|
|
189
189
|
readonly haltsTurn: boolean;
|
|
190
|
+
/**
|
|
191
|
+
* Every block mechanism that fired, from the closed {@link BlockMechanism}
|
|
192
|
+
* set. Reported so the next mechanism needs no new boolean on this interface —
|
|
193
|
+
* `haltsTurn` exists because the halt case was added as a one-off, and this is
|
|
194
|
+
* the shape that stops the pattern repeating.
|
|
195
|
+
*/
|
|
196
|
+
readonly blockedBy: readonly BlockMechanism[];
|
|
190
197
|
/**
|
|
191
198
|
* The decision the hook expressed, preferring the structured
|
|
192
199
|
* `permissionDecision` ("allow"|"deny"|"ask") then legacy `decision`
|
|
@@ -196,14 +203,34 @@ export interface HookRunResult extends ScriptRunResult {
|
|
|
196
203
|
}
|
|
197
204
|
/** Parse stdout as a hook JSON decision (pure, testable without a process). */
|
|
198
205
|
export declare function parseHookOutput(stdout: string): HookOutput | null;
|
|
206
|
+
/**
|
|
207
|
+
* The ways a hook can stop an action — a CLOSED set, listed once.
|
|
208
|
+
*
|
|
209
|
+
* 🔴 WHY A TABLE AND NOT THREE `||` TERMS. `decideHook` used to be a boolean
|
|
210
|
+
* expression over three mechanisms while `HookOutput` declared a fourth,
|
|
211
|
+
* `continue`, that nothing read (#174). The consequence was not a missed
|
|
212
|
+
* detection but an INVERTED verdict in the flagship feature: a real guard that
|
|
213
|
+
* stopped every command in the disaster battery was reported by
|
|
214
|
+
* `assertBlocksDisasters` as blocking none of them.
|
|
215
|
+
*
|
|
216
|
+
* A `||` chain has no shape that can be incomplete — every term is optional by
|
|
217
|
+
* construction, so nothing can say "you declared a mechanism and did not handle
|
|
218
|
+
* it". `Record<BlockMechanism, …>` can: adding a member to the union without
|
|
219
|
+
* adding its row is a tsc error, the same device that keeps `RULE_META` honest.
|
|
220
|
+
*/
|
|
221
|
+
export type BlockMechanism = "exit-code" | "deny-decision" | "halt-field";
|
|
199
222
|
/**
|
|
200
223
|
* Decide whether a hook result blocked, and the normalized decision. Pure, so
|
|
201
224
|
* the policy is unit-testable independent of spawning anything.
|
|
225
|
+
*
|
|
226
|
+
* Folds the closed {@link BLOCK_MECHANISMS} table rather than testing three
|
|
227
|
+
* conditions inline, so a mechanism cannot be declared and left unhandled.
|
|
202
228
|
*/
|
|
203
229
|
export declare function decideHook(exitCode: number, json: HookOutput | null, protocol?: HookProtocol): {
|
|
204
230
|
blocked: boolean;
|
|
205
231
|
decision: HookRunResult["decision"];
|
|
206
232
|
haltsTurn: boolean;
|
|
233
|
+
blockedBy: readonly BlockMechanism[];
|
|
207
234
|
};
|
|
208
235
|
/**
|
|
209
236
|
* The hook layer over {@link runScriptWith}: serialize the event to stdin, run
|
package/dist/run-hook.js
CHANGED
|
@@ -123,23 +123,42 @@ function parseHookOutput(stdout) {
|
|
|
123
123
|
return null;
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
+
const BLOCK_MECHANISMS = {
|
|
127
|
+
"exit-code": ({ exitCode, protocol }) => exitCode === protocol.blockExitCode,
|
|
128
|
+
"deny-decision": ({ json, protocol }) => {
|
|
129
|
+
const decision = json?.hookSpecificOutput?.permissionDecision ?? json?.decision;
|
|
130
|
+
return (decision !== undefined && protocol.denyDecisionValues.includes(decision));
|
|
131
|
+
},
|
|
132
|
+
// Read from the PORT, never hard-coded: `"continue"` is a documented Claude
|
|
133
|
+
// Code fact and unverified for Codex, so the harness that has it declares it
|
|
134
|
+
// (core ⊄ adapter). `=== false` and not falsy — an absent field is not a halt.
|
|
135
|
+
"halt-field": ({ json, protocol }) => {
|
|
136
|
+
const field = protocol.haltsTurnField;
|
|
137
|
+
return field !== undefined && json?.[field] === false;
|
|
138
|
+
},
|
|
139
|
+
};
|
|
126
140
|
/**
|
|
127
141
|
* Decide whether a hook result blocked, and the normalized decision. Pure, so
|
|
128
142
|
* the policy is unit-testable independent of spawning anything.
|
|
143
|
+
*
|
|
144
|
+
* Folds the closed {@link BLOCK_MECHANISMS} table rather than testing three
|
|
145
|
+
* conditions inline, so a mechanism cannot be declared and left unhandled.
|
|
129
146
|
*/
|
|
130
147
|
function decideHook(exitCode, json, protocol = hook_protocol_js_1.claudeCodeHookProtocol) {
|
|
131
148
|
const permission = json?.hookSpecificOutput?.permissionDecision;
|
|
132
149
|
const decision = permission ?? json?.decision;
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
150
|
+
const ctx = { exitCode, json, protocol };
|
|
151
|
+
// EVERY mechanism is evaluated, not the first match: a hook may both exit 2
|
|
152
|
+
// and halt the turn, and reporting only the first would make `haltsTurn`
|
|
153
|
+
// depend on the table's order.
|
|
154
|
+
const blockedBy = Object.keys(BLOCK_MECHANISMS).filter((kind) => BLOCK_MECHANISMS[kind](ctx));
|
|
155
|
+
return {
|
|
156
|
+
blocked: blockedBy.length > 0,
|
|
157
|
+
decision,
|
|
158
|
+
// Derived, not computed twice — the next mechanism needs no new boolean.
|
|
159
|
+
haltsTurn: blockedBy.includes("halt-field"),
|
|
160
|
+
blockedBy,
|
|
161
|
+
};
|
|
143
162
|
}
|
|
144
163
|
/**
|
|
145
164
|
* The hook layer over {@link runScriptWith}: serialize the event to stdin, run
|
|
@@ -150,8 +169,8 @@ function decideHook(exitCode, json, protocol = hook_protocol_js_1.claudeCodeHook
|
|
|
150
169
|
function runHookWith(command, input, opts, deps) {
|
|
151
170
|
const res = (0, run_script_js_1.runScriptWith)(command, JSON.stringify(input), opts, deps);
|
|
152
171
|
const json = parseHookOutput(res.stdout);
|
|
153
|
-
const { blocked, decision, haltsTurn } = decideHook(res.exitCode, json);
|
|
154
|
-
return { ...res, json, blocked, decision, haltsTurn };
|
|
172
|
+
const { blocked, decision, haltsTurn, blockedBy } = decideHook(res.exitCode, json);
|
|
173
|
+
return { ...res, json, blocked, decision, haltsTurn, blockedBy };
|
|
155
174
|
}
|
|
156
175
|
/**
|
|
157
176
|
* Run a hook command, piping `input` as JSON to its stdin, and report the exit
|
package/dist/test.d.ts
CHANGED
|
@@ -54,7 +54,7 @@ export { recordCheck } from "./check-count.js";
|
|
|
54
54
|
export { runScript } from "./run-script.js";
|
|
55
55
|
export type { RunScriptOptions, ScriptRunResult } from "./run-script.js";
|
|
56
56
|
export { runHook, parseHookOutput, decideHook, propertyHook, fileToolEvents, egressRoutes, } from "./run-hook.js";
|
|
57
|
-
export type { HookRunResult, RunHookOptions, HookInput, HookOutput, HookPropertyResult, FileToolEventOptions, } from "./run-hook.js";
|
|
57
|
+
export type { HookRunResult, BlockMechanism, RunHookOptions, HookInput, HookOutput, HookPropertyResult, FileToolEventOptions, } from "./run-hook.js";
|
|
58
58
|
export * from "./harness-assert.js";
|
|
59
59
|
export { experimental_emitTool, experimental_parseEmitted, experimental_assertEmittedOk, type EmitFieldSchema, type EmitObjectSchema, type EmitPropertySchema, type EmitTrackSchema, type EmitToolDefinition, type ExperimentalEmitTool, } from "./experimental-emit.js";
|
|
60
60
|
export { loadHook } from "./load-hook.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "25.0.0",
|
|
4
4
|
"description": "Audit, test and measure the harness your AI agent runs on — grade your CLAUDE.md / AGENTS.md, skills, subagents and hooks, run them against a scripted model, and measure whether they actually fire.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|