vigiles 38.2.0 → 39.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/adapters/claude-code/event-capability.js +24 -8
- package/dist/adapters/claude-code/hook-protocol.js +2 -1
- package/dist/adapters/codex/hook-protocol.js +2 -1
- package/dist/cli-flag-check.d.ts +36 -0
- package/dist/cli-flag-check.js +72 -1
- package/dist/cli-main.d.ts +2 -2
- package/dist/cli-main.js +548 -379
- package/dist/core/bash-effects.d.ts +0 -57
- package/dist/core/bash-effects.js +0 -147
- package/dist/core/compile-generator.d.ts +6 -6
- package/dist/core/compile-generator.js +4 -4
- package/dist/core/compile.d.ts +72 -8
- package/dist/core/compile.js +276 -133
- package/dist/core/config-schema.d.ts +40 -10
- package/dist/core/config-schema.js +139 -16
- package/dist/core/coverage.d.ts +16 -0
- package/dist/core/coverage.js +22 -0
- package/dist/core/doc-refs.d.ts +3 -2
- package/dist/core/doc-refs.js +2 -2
- package/dist/core/enforce-marker.d.ts +28 -0
- package/dist/core/enforce-marker.js +34 -0
- package/dist/core/eslint-inventory.d.ts +96 -0
- package/dist/core/eslint-inventory.js +251 -0
- package/dist/core/event-capability.d.ts +5 -1
- package/dist/core/frame.d.ts +43 -2
- package/dist/core/frame.js +85 -2
- package/dist/core/generate-schema.d.ts +3 -2
- package/dist/core/generate-schema.js +2 -2
- package/dist/core/generate-types.d.ts +11 -2
- package/dist/core/generate-types.js +12 -5
- package/dist/core/hook-command.d.ts +172 -0
- package/dist/core/hook-command.js +277 -0
- package/dist/core/hook-delivery.d.ts +133 -0
- package/dist/core/hook-delivery.js +150 -0
- package/dist/core/hook-paths.d.ts +14 -0
- package/dist/core/hook-paths.js +22 -0
- package/dist/core/hook-program.d.ts +4 -4
- package/dist/core/hook-program.js +13 -25
- package/dist/core/hook-state.d.ts +1 -1
- package/dist/core/hook-state.js +1 -1
- package/dist/core/instruction-remedies.d.ts +35 -0
- package/dist/core/instruction-remedies.js +70 -1
- package/dist/core/integrity-header.d.ts +54 -0
- package/dist/core/integrity-header.js +90 -0
- package/dist/core/integrity.d.ts +1 -25
- package/dist/core/integrity.js +16 -74
- package/dist/core/linter-adapter.d.ts +27 -3
- package/dist/core/linters.d.ts +8 -1
- package/dist/core/linters.js +116 -81
- package/dist/core/orphans.d.ts +3 -5
- package/dist/core/orphans.js +9 -10
- package/dist/core/presets.d.ts +19 -7
- package/dist/core/presets.js +19 -6
- package/dist/core/rule-catalog.d.ts +11 -12
- package/dist/core/rule-catalog.js +25 -94
- package/dist/core/rule-meta.d.ts +12 -0
- package/dist/core/rule-meta.js +47 -10
- package/dist/core/source-refs.d.ts +3 -23
- package/dist/core/source-refs.js +3 -27
- package/dist/core/test-utils.d.ts +13 -0
- package/dist/core/test-utils.js +40 -1
- package/dist/core/types.d.ts +18 -26
- package/dist/core/validate.d.ts +1 -2
- package/dist/core/validate.js +60 -47
- package/dist/coverage-evidence.js +2 -3
- package/dist/deprecated-key-files.d.ts +6 -0
- package/dist/deprecated-key-files.js +7 -6
- package/dist/exclude.d.ts +3 -1
- package/dist/exclude.js +9 -33
- package/dist/harness-assert.d.ts +18 -0
- package/dist/harness-assert.js +38 -0
- package/dist/hook-check-install.js +15 -6
- package/dist/hook-check.d.ts +1 -3
- package/dist/hook-check.js +3 -5
- package/dist/hook-install.d.ts +7 -41
- package/dist/hook-install.js +29 -96
- package/dist/hook-runtime.js +2 -1
- package/dist/load-hook.d.ts +3 -2
- package/dist/load-hook.js +28 -6
- package/dist/overrides.d.ts +25 -0
- package/dist/overrides.js +51 -0
- package/dist/plugin-loader.d.ts +23 -0
- package/dist/plugin-loader.js +42 -12
- package/dist/root-globs.d.ts +9 -0
- package/dist/root-globs.js +48 -0
- package/dist/rule-routing.js +6 -3
- package/dist/run-hook.d.ts +26 -2
- package/dist/run-hook.js +48 -12
- package/dist/scan-core.d.ts +96 -20
- package/dist/scan-core.js +171 -176
- package/dist/scan-files.d.ts +0 -8
- package/dist/scan-files.js +78 -20
- package/dist/scan.d.ts +12 -12
- package/dist/scan.js +33 -10
- package/dist/setup-plan.d.ts +15 -9
- package/dist/setup-plan.js +26 -18
- package/dist/test-coverage.d.ts +3 -8
- package/dist/test-coverage.js +6 -6
- package/dist/test.d.ts +1 -1
- package/dist/vigilesrc.schema.json +1329 -118
- package/package.json +1 -1
- package/skills/adopt-spec/SKILL.md +2 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hookOutputDelivery = hookOutputDelivery;
|
|
4
|
+
const event_capability_js_1 = require("./event-capability.js");
|
|
5
|
+
const byteLength = (s) => new TextEncoder().encode(s).length;
|
|
6
|
+
const nonEmpty = (v) => typeof v === "string" && v.trim().length > 0;
|
|
7
|
+
const isRecord = (v) => typeof v === "object" && v !== null;
|
|
8
|
+
function hookSpecific(json) {
|
|
9
|
+
const h = json?.hookSpecificOutput;
|
|
10
|
+
return isRecord(h) ? h : {};
|
|
11
|
+
}
|
|
12
|
+
const NONE = { via: [], stranded: [] };
|
|
13
|
+
const reached = (channel) => ({
|
|
14
|
+
via: [channel],
|
|
15
|
+
stranded: [],
|
|
16
|
+
});
|
|
17
|
+
const strand = (stream, text, why) => ({ via: [], stranded: [{ stream, bytes: byteLength(text), why }] });
|
|
18
|
+
/** True iff the JSON or the exit code expresses a veto under this protocol. */
|
|
19
|
+
function vetoExpressed(e, f) {
|
|
20
|
+
const permission = hookSpecific(e.json).permissionDecision;
|
|
21
|
+
const decision = permission ?? e.json?.decision;
|
|
22
|
+
const denied = typeof decision === "string" && f.denyDecisionValues.includes(decision);
|
|
23
|
+
return e.exitCode === f.blockExitCode || denied;
|
|
24
|
+
}
|
|
25
|
+
/** True iff the JSON sets the protocol's halt-the-turn field to false. */
|
|
26
|
+
function haltExpressed(e, f) {
|
|
27
|
+
return f.haltsTurnField !== undefined && e.json?.[f.haltsTurnField] === false;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The hook stopped something: a veto on an event that honours one, or a halt
|
|
31
|
+
* on an event that honours THAT. Two channels, read apart: a halt is not a
|
|
32
|
+
* veto, and an event can honour either alone (Claude Code's `continue: false`
|
|
33
|
+
* ends the turn from `PostToolUse`, which vetoes nothing; `PreCompact` vetoes
|
|
34
|
+
* and discards `continue`).
|
|
35
|
+
*/
|
|
36
|
+
function stopped(e, f, honours) {
|
|
37
|
+
return ((honours.includes("veto") && vetoExpressed(e, f)) ||
|
|
38
|
+
(honours.includes("halt") && haltExpressed(e, f)));
|
|
39
|
+
}
|
|
40
|
+
/** Where a non-empty `additionalContext` goes on this event. */
|
|
41
|
+
function contextDelivery(e, honours) {
|
|
42
|
+
return honours.includes("inject")
|
|
43
|
+
? reached("additional-context")
|
|
44
|
+
: strand("stdout", e.stdout, `\`${e.event}\` does not honour additionalContext — the text is ` +
|
|
45
|
+
`dropped. Move the hook to an event that injects context.`);
|
|
46
|
+
}
|
|
47
|
+
/** `additionalContext` and a feedback-event `decision` reason, from JSON stdout. */
|
|
48
|
+
function jsonParts(e, f, honours) {
|
|
49
|
+
if (e.json === null)
|
|
50
|
+
return [];
|
|
51
|
+
const context = hookSpecific(e.json).additionalContext;
|
|
52
|
+
const contextPart = nonEmpty(context) ? contextDelivery(e, honours) : NONE;
|
|
53
|
+
const decision = e.json.decision;
|
|
54
|
+
const reasonDelivered = typeof decision === "string" &&
|
|
55
|
+
f.denyDecisionValues.includes(decision) &&
|
|
56
|
+
nonEmpty(e.json.reason) &&
|
|
57
|
+
honours.includes("feedback");
|
|
58
|
+
return [contextPart, reasonDelivered ? reached("decision-reason") : NONE];
|
|
59
|
+
}
|
|
60
|
+
/** Plain (non-JSON) stdout: context only at exit 0 on a `stdout-context` event. */
|
|
61
|
+
function plainStdoutPart(e, honours) {
|
|
62
|
+
if (e.json !== null || e.stdout.trim().length === 0)
|
|
63
|
+
return NONE;
|
|
64
|
+
if (e.exitCode !== 0) {
|
|
65
|
+
return strand("stdout", e.stdout, `plain stdout is only ever context at exit 0; this hook exited ` +
|
|
66
|
+
`${String(e.exitCode)}.`);
|
|
67
|
+
}
|
|
68
|
+
return honours.includes("stdout-context")
|
|
69
|
+
? reached("plain-stdout")
|
|
70
|
+
: strand("stdout", e.stdout, `plain stdout on \`${e.event}\` is not added to the model's context ` +
|
|
71
|
+
`(it goes to the debug log). Emit JSON ` +
|
|
72
|
+
`\`hookSpecificOutput.additionalContext\` instead.`);
|
|
73
|
+
}
|
|
74
|
+
/** Why stderr reached no channel — an instruction, not a verdict. */
|
|
75
|
+
function stderrWhy(e, f, honours) {
|
|
76
|
+
const block = String(f.blockExitCode);
|
|
77
|
+
if (e.exitCode === f.blockExitCode) {
|
|
78
|
+
return `exit ${block} on \`${e.event}\` neither stops anything nor feeds stderr to the model.`;
|
|
79
|
+
}
|
|
80
|
+
if (e.exitCode !== 0) {
|
|
81
|
+
return `exit ${String(e.exitCode)} is a non-blocking error: at most the first line of stderr is shown to the USER, never to the model.`;
|
|
82
|
+
}
|
|
83
|
+
const orExit = honours.includes("feedback")
|
|
84
|
+
? `, or exit ${block} (\`${e.event}\` feeds that stderr back to the model)`
|
|
85
|
+
: "";
|
|
86
|
+
return `stderr from a hook that exits 0 reaches no channel the model reads. Emit JSON \`hookSpecificOutput.additionalContext\` on stdout${orExit}.`;
|
|
87
|
+
}
|
|
88
|
+
/** stderr: delivered only at the block exit code on a `feedback` event. */
|
|
89
|
+
function stderrPart(e, f, honours) {
|
|
90
|
+
if (e.stderr.trim().length === 0)
|
|
91
|
+
return NONE;
|
|
92
|
+
return e.exitCode === f.blockExitCode && honours.includes("feedback")
|
|
93
|
+
? reached("exit-code-feedback")
|
|
94
|
+
: strand("stderr", e.stderr, stderrWhy(e, f, honours));
|
|
95
|
+
}
|
|
96
|
+
/** The verdict for an event the table has no row for. */
|
|
97
|
+
function unrecordedEvent(e, f) {
|
|
98
|
+
const legacy = f.table === undefined ? (f.injectableEvents ?? []) : [];
|
|
99
|
+
if (nonEmpty(hookSpecific(e.json).additionalContext) &&
|
|
100
|
+
legacy.includes(e.event)) {
|
|
101
|
+
return { kind: "delivered", via: ["additional-context"], stranded: [] };
|
|
102
|
+
}
|
|
103
|
+
const why = e.event === ""
|
|
104
|
+
? "the event carries no hook_event_name, so there is no event to judge against"
|
|
105
|
+
: `the harness's capability table has no row for \`${e.event}\`, so where ` +
|
|
106
|
+
`its output goes is not recorded — add the row (with its basis) to judge it`;
|
|
107
|
+
return { kind: "unknown", event: e.event, why };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Judge one finished hook run. Pure.
|
|
111
|
+
*
|
|
112
|
+
* Order matters and is the specification:
|
|
113
|
+
* 1. nothing printed → `silent` (true on every event, known or not);
|
|
114
|
+
* 2. no table row → `delivered` only on the one fact the legacy list proves,
|
|
115
|
+
* else `unknown`;
|
|
116
|
+
* 3. a veto on an event that honours one, or a halt on an event that
|
|
117
|
+
* honours `halt` → `blocked`;
|
|
118
|
+
* 4. otherwise collect every channel that carried text, and every stream that
|
|
119
|
+
* carried text to no channel; at least one channel → `delivered`. A JSON
|
|
120
|
+
* object on stdout is applied on any exit code (the host's exit-code table:
|
|
121
|
+
* valid JSON is "Success" at exit 0 and at any other code, and its fields
|
|
122
|
+
* are still read at exit 2); plain text is context only at exit 0.
|
|
123
|
+
*/
|
|
124
|
+
function hookOutputDelivery(e, f) {
|
|
125
|
+
if (e.stdout.trim().length === 0 && e.stderr.trim().length === 0) {
|
|
126
|
+
return { kind: "silent" };
|
|
127
|
+
}
|
|
128
|
+
const verdict = (0, event_capability_js_1.capabilityOf)(f.table, e.event);
|
|
129
|
+
if (verdict.kind === "unknown")
|
|
130
|
+
return unrecordedEvent(e, f);
|
|
131
|
+
const honours = verdict.capability.honours;
|
|
132
|
+
if (stopped(e, f, honours))
|
|
133
|
+
return { kind: "blocked", stranded: [] };
|
|
134
|
+
const parts = [
|
|
135
|
+
...jsonParts(e, f, honours),
|
|
136
|
+
plainStdoutPart(e, honours),
|
|
137
|
+
stderrPart(e, f, honours),
|
|
138
|
+
];
|
|
139
|
+
const via = parts.flatMap((p) => p.via);
|
|
140
|
+
const stranded = parts.flatMap((p) => p.stranded);
|
|
141
|
+
if (via.length > 0)
|
|
142
|
+
return { kind: "delivered", via, stranded };
|
|
143
|
+
// Control-only JSON (`permissionDecision: "allow"`, a bare `systemMessage`)
|
|
144
|
+
// printed something, but nothing addressed to the model — that is silence,
|
|
145
|
+
// not a stranded nudge.
|
|
146
|
+
return stranded.length === 0
|
|
147
|
+
? { kind: "silent" }
|
|
148
|
+
: { kind: "undelivered", stranded };
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=hook-delivery.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where compiled hooks and their stamps live — the one owner of these two
|
|
3
|
+
* paths. A leaf module, so the writer (`compile`), the runtime and the
|
|
4
|
+
* hook-line plan can all import it without importing each other.
|
|
5
|
+
*/
|
|
6
|
+
/** The directory `compile` reads hook sources from, repo-relative. */
|
|
7
|
+
export declare const HOOKS_DIR = ".vigiles/hooks";
|
|
8
|
+
/**
|
|
9
|
+
* The stamp sidecar a hook's compile writes, as a repo-relative reference
|
|
10
|
+
* (`.vigiles/hooks/<basename>.json`). `hookStampPath` resolves it; the hook-line
|
|
11
|
+
* plan names it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function hookStampRef(hookFile: string): string;
|
|
14
|
+
//# sourceMappingURL=hook-paths.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Where compiled hooks and their stamps live — the one owner of these two
|
|
4
|
+
* paths. A leaf module, so the writer (`compile`), the runtime and the
|
|
5
|
+
* hook-line plan can all import it without importing each other.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.HOOKS_DIR = void 0;
|
|
9
|
+
exports.hookStampRef = hookStampRef;
|
|
10
|
+
/** The directory `compile` reads hook sources from, repo-relative. */
|
|
11
|
+
exports.HOOKS_DIR = ".vigiles/hooks";
|
|
12
|
+
/**
|
|
13
|
+
* The stamp sidecar a hook's compile writes, as a repo-relative reference
|
|
14
|
+
* (`.vigiles/hooks/<basename>.json`). `hookStampPath` resolves it; the hook-line
|
|
15
|
+
* plan names it.
|
|
16
|
+
*/
|
|
17
|
+
function hookStampRef(hookFile) {
|
|
18
|
+
// Either separator: a Windows path names the same sidecar.
|
|
19
|
+
const base = hookFile.replace(/\\/g, "/").split("/").at(-1) ?? hookFile;
|
|
20
|
+
return `${exports.HOOKS_DIR}/${base}.json`;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=hook-paths.js.map
|
|
@@ -558,7 +558,7 @@ export interface PathView {
|
|
|
558
558
|
* True iff the path sits under at least one allowed prefix (e.g. "src/**").
|
|
559
559
|
*
|
|
560
560
|
* A prefix is matched in ITS OWN spelling: a repo-relative prefix (`"src"`,
|
|
561
|
-
* `"
|
|
561
|
+
* `"drafts/papers/"`) is compared against the path made repo-relative, an
|
|
562
562
|
* absolute prefix (`"/etc"`) against the path made absolute. Which of those is
|
|
563
563
|
* available depends on the project root — see {@link pathView}.
|
|
564
564
|
*/
|
|
@@ -578,9 +578,9 @@ export interface PathView {
|
|
|
578
578
|
* 🔴 `root` IS NOT OPTIONAL IN PRACTICE — WITHOUT IT THIS IS DEAD FOR EVERY REAL
|
|
579
579
|
* EVENT. Claude Code's Edit/Write/MultiEdit tools always send an ABSOLUTE
|
|
580
580
|
* `file_path`, and every hook in the wild writes repo-relative prefixes
|
|
581
|
-
* (`under(["
|
|
582
|
-
* never meet: the comparison ran `"/home/u/repo/
|
|
583
|
-
* against `"
|
|
581
|
+
* (`under(["drafts/papers/"])`). Before the root existed, the two could
|
|
582
|
+
* never meet: the comparison ran `"/home/u/repo/drafts/papers/x.tex"`
|
|
583
|
+
* against `"drafts/papers"` and returned `false` for all input, forever.
|
|
584
584
|
* MEASURED 2026-08-12 on the live runtime with one compiled react hook and two
|
|
585
585
|
* spellings of the same file — the relative one printed its checklist, the
|
|
586
586
|
* absolute one exited 0 in silence. Three shipped hooks in a consumer repo were
|
|
@@ -87,6 +87,7 @@ const tool_contract_js_1 = require("./tool-contract.js");
|
|
|
87
87
|
const merge_conflict_js_1 = require("./merge-conflict.js");
|
|
88
88
|
const hook_providers_js_1 = require("./hook-providers.js");
|
|
89
89
|
const hook_state_js_1 = require("./hook-state.js");
|
|
90
|
+
const hook_paths_js_1 = require("./hook-paths.js");
|
|
90
91
|
/**
|
|
91
92
|
* Does `name` match a hook's declared tool list, under the SAME semantics as the
|
|
92
93
|
* matcher the compiler emits for it?
|
|
@@ -324,8 +325,8 @@ const isAtOrUnder = (rawCandidate, rawBase, insensitive = false) => {
|
|
|
324
325
|
* Could this path be under this prefix under SOME root or working directory?
|
|
325
326
|
*
|
|
326
327
|
* The tractable stand-in for "unprovable": the prefix's segments occur as a whole
|
|
327
|
-
* contiguous run somewhere in the path. `/home/u/
|
|
328
|
-
* could be `
|
|
328
|
+
* contiguous run somewhere in the path. `/home/u/project/drafts/papers/x.tex`
|
|
329
|
+
* could be `drafts/papers/x.tex` in a repo rooted at `/home/u/project`; the
|
|
329
330
|
* leading directories are exactly what we cannot rule out without a root.
|
|
330
331
|
*
|
|
331
332
|
* A leading `/` is stripped from the prefix first, so an ABSOLUTE prefix can
|
|
@@ -335,8 +336,8 @@ const isAtOrUnder = (rawCandidate, rawBase, insensitive = false) => {
|
|
|
335
336
|
* ⚠️ WHAT THIS STILL MISSES, stated rather than implied — in both cases the
|
|
336
337
|
* verdict is `"outside"`, so the denylist misses too:
|
|
337
338
|
*
|
|
338
|
-
* 1. The segments must be PRESENT. `cd
|
|
339
|
-
* names only `papers/x.tex`, so a prefix of `
|
|
339
|
+
* 1. The segments must be PRESENT. `cd drafts && sed -i s/a/b/ papers/x.tex`
|
|
340
|
+
* names only `papers/x.tex`, so a prefix of `drafts/papers` finds nothing.
|
|
340
341
|
* Resolving a leaf's argument against a preceding `cd` is a parser change, not
|
|
341
342
|
* a matcher one; it is the blind spot both shipped guards already document.
|
|
342
343
|
* 2. An ABSOLUTE prefix against a RELATIVE token with NO root — `touches(["/r/
|
|
@@ -373,7 +374,7 @@ const mightBeUnder = (path, base, insensitive = false) => {
|
|
|
373
374
|
*
|
|
374
375
|
* Note what does NOT happen: a placed path that is not under the prefix comes
|
|
375
376
|
* back `"under"`-less but is still offered to `mightBeUnder`, so a denylist
|
|
376
|
-
* over-blocks a sibling checkout's `…/
|
|
377
|
+
* over-blocks a sibling checkout's `…/drafts/papers/…` rather than silently
|
|
377
378
|
* allowing it. That over-block is the chosen error direction, not an oversight —
|
|
378
379
|
* `under` never sees it, because `under` accepts `"under"` only.
|
|
379
380
|
*/
|
|
@@ -466,7 +467,7 @@ function writeTargetsOf(leaf) {
|
|
|
466
467
|
}
|
|
467
468
|
/**
|
|
468
469
|
* A write target as it lands on disk, given the chdir wrapper the leaf ran under
|
|
469
|
-
* (`env -C
|
|
470
|
+
* (`env -C drafts sed -i s/a/b/ papers/x.tex` writes `drafts/papers/x.tex`).
|
|
470
471
|
*
|
|
471
472
|
* Absolute targets and `~`-rooted ones already name their directory and are
|
|
472
473
|
* returned untouched. The join itself is {@link resolveRef} rather than a fresh
|
|
@@ -562,8 +563,8 @@ function commandView(raw, root) {
|
|
|
562
563
|
// REAL BYPASS. `leafCommands` keeps a word exactly as written, quotes and
|
|
563
564
|
// all, so a token arrived as `"papers/x.tex"` — with the quote characters —
|
|
564
565
|
// and matched no prefix at all. MEASURED end-to-end on a shipped guard:
|
|
565
|
-
// `sed -i s/a/b/
|
|
566
|
-
// `sed -i s/a/b/ "
|
|
566
|
+
// `sed -i s/a/b/ drafts/papers/x/paper.tex` exited 2 (blocked) while
|
|
567
|
+
// `sed -i s/a/b/ "drafts/papers/x/paper.tex"` exited 0. Quoting a path
|
|
567
568
|
// is not an exotic evasion — it is what anyone writes for a path with a
|
|
568
569
|
// space in it, so every denylist built on `touches` was one quote from open.
|
|
569
570
|
// `writesTo` was already correct precisely because it reads `normalized`;
|
|
@@ -1020,9 +1021,9 @@ function verifyHookStamp(source, stamp) {
|
|
|
1020
1021
|
* 🔴 `root` IS NOT OPTIONAL IN PRACTICE — WITHOUT IT THIS IS DEAD FOR EVERY REAL
|
|
1021
1022
|
* EVENT. Claude Code's Edit/Write/MultiEdit tools always send an ABSOLUTE
|
|
1022
1023
|
* `file_path`, and every hook in the wild writes repo-relative prefixes
|
|
1023
|
-
* (`under(["
|
|
1024
|
-
* never meet: the comparison ran `"/home/u/repo/
|
|
1025
|
-
* against `"
|
|
1024
|
+
* (`under(["drafts/papers/"])`). Before the root existed, the two could
|
|
1025
|
+
* never meet: the comparison ran `"/home/u/repo/drafts/papers/x.tex"`
|
|
1026
|
+
* against `"drafts/papers"` and returned `false` for all input, forever.
|
|
1026
1027
|
* MEASURED 2026-08-12 on the live runtime with one compiled react hook and two
|
|
1027
1028
|
* spellings of the same file — the relative one printed its checklist, the
|
|
1028
1029
|
* absolute one exited 0 in silence. Three shipped hooks in a consumer repo were
|
|
@@ -1632,11 +1633,6 @@ function resolveRef(root, ref) {
|
|
|
1632
1633
|
const leader = unc ? "//" : joined.startsWith("/") ? "/" : "";
|
|
1633
1634
|
return leader + out.join("/");
|
|
1634
1635
|
}
|
|
1635
|
-
/** The basename of a path token — the sidecar is keyed by the hook's basename. */
|
|
1636
|
-
function basenameOf(token) {
|
|
1637
|
-
const parts = token.replace(/\\/g, "/").split("/");
|
|
1638
|
-
return parts[parts.length - 1] ?? token;
|
|
1639
|
-
}
|
|
1640
1636
|
/**
|
|
1641
1637
|
* The tool names a repair may arrive under: the ones MEASURED to write a file
|
|
1642
1638
|
* and carry `file_path`. Everything else is refused, including a name this list
|
|
@@ -1702,14 +1698,6 @@ function repairTargetOf(event) {
|
|
|
1702
1698
|
const filePath = event.tool_input?.file_path;
|
|
1703
1699
|
return typeof filePath === "string" ? filePath : null;
|
|
1704
1700
|
}
|
|
1705
|
-
/**
|
|
1706
|
-
* The stamp sidecar a hook's compile writes, as a repo-relative reference.
|
|
1707
|
-
* Mirrors `hookStampPath` in cli.ts (`.vigiles/hooks/<basename>.json`); kept as a
|
|
1708
|
-
* string here because core takes no `node:path`.
|
|
1709
|
-
*/
|
|
1710
|
-
function stampSidecarRef(hookFile) {
|
|
1711
|
-
return `.vigiles/hooks/${basenameOf(hookFile)}.json`;
|
|
1712
|
-
}
|
|
1713
1701
|
/**
|
|
1714
1702
|
* True when this event IS the author repairing the hook — a FILE WRITE, and only
|
|
1715
1703
|
* a file write.
|
|
@@ -1770,7 +1758,7 @@ function isStampRepairEvent(event, hookFile, repoRoot) {
|
|
|
1770
1758
|
return false;
|
|
1771
1759
|
const target = resolveRef(repoRoot, filePath);
|
|
1772
1760
|
return (target === resolveRef(repoRoot, hookFile) ||
|
|
1773
|
-
target === resolveRef(repoRoot,
|
|
1761
|
+
target === resolveRef(repoRoot, (0, hook_paths_js_1.hookStampRef)(hookFile)));
|
|
1774
1762
|
}
|
|
1775
1763
|
/**
|
|
1776
1764
|
* True when this event is a load-path REPAIR — a file write, and only a file
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* ## The hole it fills, measured
|
|
15
15
|
*
|
|
16
|
-
* `
|
|
16
|
+
* `hook-script-exists` says a hook should be a typed program the compiler can
|
|
17
17
|
* check. In the knowledge base this repo dogfoods on, ALL SEVEN advisory hooks
|
|
18
18
|
* were still hand-written shell (measured 2026-08-12), and the reason was uniform:
|
|
19
19
|
* every one of them both READS and WRITES a stamp file.
|
package/dist/core/hook-state.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* ## The hole it fills, measured
|
|
16
16
|
*
|
|
17
|
-
* `
|
|
17
|
+
* `hook-script-exists` says a hook should be a typed program the compiler can
|
|
18
18
|
* check. In the knowledge base this repo dogfoods on, ALL SEVEN advisory hooks
|
|
19
19
|
* were still hand-written shell (measured 2026-08-12), and the reason was uniform:
|
|
20
20
|
* every one of them both READS and WRITES a stamp file.
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* What a finding about an instruction file tells the reader to DO — the one
|
|
3
|
+
* owner of that wording, so lint, audit and compile cannot drift apart.
|
|
4
|
+
*
|
|
2
5
|
* Where always-loaded text can go instead of being deleted.
|
|
3
6
|
*
|
|
4
7
|
* Every finding that says "too much is loaded on every request" — the weight
|
|
@@ -10,4 +13,36 @@
|
|
|
10
13
|
* still fires at the right moment first, deletion last.
|
|
11
14
|
*/
|
|
12
15
|
export declare const MOVE_OUT_OF_ALWAYS_LOADED: string;
|
|
16
|
+
/**
|
|
17
|
+
* The two ways to put a hand-written instruction file under a spec, each said
|
|
18
|
+
* for what it does. They are not interchangeable: `init --target` is the
|
|
19
|
+
* deterministic floor (`adoptMarkdown` — every heading becomes a verbatim prose
|
|
20
|
+
* section, only backticked paths that resolve become checked `file()` refs, no
|
|
21
|
+
* rule is inferred), while the `adopt-spec` skill is an agent reading the file
|
|
22
|
+
* and mapping rules, commands and paths to typed calls. The skill is
|
|
23
|
+
* user-invoked (`disable-model-invocation`), so it has to be asked for.
|
|
24
|
+
* Neither edits the markdown: switching it to spec-managed is a `compile` the
|
|
25
|
+
* user runs after reviewing.
|
|
26
|
+
*/
|
|
27
|
+
export declare function adoptInstructionFileRemedy(path: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* For a file the repository does not own — vendored, a corpus, a fixture: drop
|
|
30
|
+
* its folder from what vigiles reads. `exclude` is top-level in
|
|
31
|
+
* `.vigilesrc.json`, and its globs are relative to the repository root.
|
|
32
|
+
*/
|
|
33
|
+
export declare function excludeFolderRemedy(path: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* For instruction files kept hand-written on purpose (a notes or
|
|
36
|
+
* knowledge-base folder): turn the rule off for those folders with an
|
|
37
|
+
* `overrides` entry — the rule keeps guarding every other folder. Off for the
|
|
38
|
+
* whole repository is the last resort, named last.
|
|
39
|
+
*/
|
|
40
|
+
export declare function keepHandWrittenRemedy(path: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Everything a `require-instructions-spec` finding says to do, in the order to
|
|
43
|
+
* try it: adopt, else exclude what is not yours, else turn the rule off for the
|
|
44
|
+
* folders that stay hand-written. It is the first finding a new adopter sees,
|
|
45
|
+
* so it names commands and config, not concepts.
|
|
46
|
+
*/
|
|
47
|
+
export declare function handWrittenInstructionRemedy(path: string): string;
|
|
13
48
|
//# sourceMappingURL=instruction-remedies.d.ts.map
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MOVE_OUT_OF_ALWAYS_LOADED = void 0;
|
|
4
|
+
exports.adoptInstructionFileRemedy = adoptInstructionFileRemedy;
|
|
5
|
+
exports.excludeFolderRemedy = excludeFolderRemedy;
|
|
6
|
+
exports.keepHandWrittenRemedy = keepHandWrittenRemedy;
|
|
7
|
+
exports.handWrittenInstructionRemedy = handWrittenInstructionRemedy;
|
|
4
8
|
/**
|
|
9
|
+
* What a finding about an instruction file tells the reader to DO — the one
|
|
10
|
+
* owner of that wording, so lint, audit and compile cannot drift apart.
|
|
11
|
+
*
|
|
5
12
|
* Where always-loaded text can go instead of being deleted.
|
|
6
13
|
*
|
|
7
14
|
* Every finding that says "too much is loaded on every request" — the weight
|
|
@@ -18,7 +25,69 @@ exports.MOVE_OUT_OF_ALWAYS_LOADED = [
|
|
|
18
25
|
" - a step tied to one action → a hook",
|
|
19
26
|
" - a procedure for one kind of task → a skill",
|
|
20
27
|
" - a fact a machine can check → a lint rule or a test, then delete the prose",
|
|
21
|
-
" -
|
|
28
|
+
" - history → git: link the commit, do not copy its text",
|
|
29
|
+
" - a measurement → next to the check that measures it",
|
|
22
30
|
" - text another file already says → delete it",
|
|
23
31
|
].join("\n");
|
|
32
|
+
/**
|
|
33
|
+
* The two ways to put a hand-written instruction file under a spec, each said
|
|
34
|
+
* for what it does. They are not interchangeable: `init --target` is the
|
|
35
|
+
* deterministic floor (`adoptMarkdown` — every heading becomes a verbatim prose
|
|
36
|
+
* section, only backticked paths that resolve become checked `file()` refs, no
|
|
37
|
+
* rule is inferred), while the `adopt-spec` skill is an agent reading the file
|
|
38
|
+
* and mapping rules, commands and paths to typed calls. The skill is
|
|
39
|
+
* user-invoked (`disable-model-invocation`), so it has to be asked for.
|
|
40
|
+
* Neither edits the markdown: switching it to spec-managed is a `compile` the
|
|
41
|
+
* user runs after reviewing.
|
|
42
|
+
*/
|
|
43
|
+
function adoptInstructionFileRemedy(path) {
|
|
44
|
+
return [
|
|
45
|
+
`Adopt it into a spec, one of two ways:`,
|
|
46
|
+
` - the \`adopt-spec\` skill — ask your agent to run it on ${path} (it never fires by itself): the agent maps rules to guidance()/enforce(), commands to cmd() and paths to file(), and shows you the spec before writing it`,
|
|
47
|
+
` - \`npx vigiles init --target=${path}\` — no agent, immediate: copies every section verbatim as prose; only paths that resolve become checked file() refs, rules stay prose`,
|
|
48
|
+
` Neither edits ${path}. Review the spec, then run \`npx vigiles compile\`.`,
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
51
|
+
/** The folder of a repository-relative path, as a root-relative glob. */
|
|
52
|
+
function folderGlob(path) {
|
|
53
|
+
const dir = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
|
|
54
|
+
return dir === "" ? "<folder>/**" : `${dir}/**`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* For a file the repository does not own — vendored, a corpus, a fixture: drop
|
|
58
|
+
* its folder from what vigiles reads. `exclude` is top-level in
|
|
59
|
+
* `.vigilesrc.json`, and its globs are relative to the repository root.
|
|
60
|
+
*/
|
|
61
|
+
function excludeFolderRemedy(path) {
|
|
62
|
+
return [
|
|
63
|
+
`Not your file (vendored, a corpus, a fixture)? Exclude its folder in .vigilesrc.json:`,
|
|
64
|
+
` { "exclude": ["${folderGlob(path)}"] }`,
|
|
65
|
+
].join("\n");
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* For instruction files kept hand-written on purpose (a notes or
|
|
69
|
+
* knowledge-base folder): turn the rule off for those folders with an
|
|
70
|
+
* `overrides` entry — the rule keeps guarding every other folder. Off for the
|
|
71
|
+
* whole repository is the last resort, named last.
|
|
72
|
+
*/
|
|
73
|
+
function keepHandWrittenRemedy(path) {
|
|
74
|
+
return [
|
|
75
|
+
`Keeping instruction files hand-written on purpose (a notes or knowledge-base folder)? Turn the rule off for those folders in .vigilesrc.json:`,
|
|
76
|
+
` { "overrides": [{ "files": ["${folderGlob(path)}"], "rules": { "require-instructions-spec": "off" } }] }`,
|
|
77
|
+
` Only if no instruction file in the repository should have a spec: { "rules": { "require-instructions-spec": "off" } }`,
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Everything a `require-instructions-spec` finding says to do, in the order to
|
|
82
|
+
* try it: adopt, else exclude what is not yours, else turn the rule off for the
|
|
83
|
+
* folders that stay hand-written. It is the first finding a new adopter sees,
|
|
84
|
+
* so it names commands and config, not concepts.
|
|
85
|
+
*/
|
|
86
|
+
function handWrittenInstructionRemedy(path) {
|
|
87
|
+
return [
|
|
88
|
+
`1. ${adoptInstructionFileRemedy(path)}`,
|
|
89
|
+
`2. ${excludeFolderRemedy(path)}`,
|
|
90
|
+
`3. ${keepHandWrittenRemedy(path)}`,
|
|
91
|
+
].join("\n");
|
|
92
|
+
}
|
|
24
93
|
//# sourceMappingURL=instruction-remedies.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the vigiles integrity header sits in a file, and whether an
|
|
3
|
+
* instruction file is under a spec — the node-free half of `integrity.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Split out (like `assert-never.ts` from `hash.ts`) so the browser scan twin
|
|
6
|
+
* can read these facts without `node:crypto`; `integrity.ts` re-exports all of
|
|
7
|
+
* it, so callers import from there as before.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Split off a leading frontmatter block. `head` is `""` when the file has none, so a caller can
|
|
11
|
+
* concatenate `head + body` unconditionally and get the original content back.
|
|
12
|
+
*/
|
|
13
|
+
export declare function splitFrontmatter(content: string): Readonly<{
|
|
14
|
+
head: string;
|
|
15
|
+
body: string;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Locate the integrity header wherever it legitimately sits — before the frontmatter (files
|
|
19
|
+
* compiled before 2026-08-17) or after it (files compiled since). Returns the hash, the spec
|
|
20
|
+
* path, and the content with the header removed and everything else intact.
|
|
21
|
+
*
|
|
22
|
+
* This is the ONLY place that knows where the header can be. Matching `/^<!-- vigiles:sha256/`
|
|
23
|
+
* by hand elsewhere is the bug this function exists to make unnecessary.
|
|
24
|
+
*/
|
|
25
|
+
export declare function findIntegrityHeader(content: string): Readonly<{
|
|
26
|
+
hash: string;
|
|
27
|
+
specFile: string;
|
|
28
|
+
/** `content` minus the header, frontmatter still in place. */
|
|
29
|
+
withoutHeader: string;
|
|
30
|
+
}> | null;
|
|
31
|
+
/** The marker that tells `require-instructions-spec` a file is intentionally
|
|
32
|
+
* hand-owned (no `.spec.ts` expected). */
|
|
33
|
+
export declare const REQUIRE_INSTRUCTIONS_SPEC_DISABLE = "<!-- vigiles-disable require-instructions-spec -->";
|
|
34
|
+
/**
|
|
35
|
+
* Whether an instruction file is under a spec, opted out of one, or neither.
|
|
36
|
+
*
|
|
37
|
+
* 🔴 THE ONE ANSWER to "is this instruction file hand-written?". `lint`'s
|
|
38
|
+
* `require-instructions-spec`, `audit`'s adoption list and the scan's
|
|
39
|
+
* `Instructions:` line each used to decide it with their own test — a sibling
|
|
40
|
+
* spec only, a spec or a compiled header, with or without the marker — so the
|
|
41
|
+
* same repo could be one hand-written file to `audit` and six to `lint`.
|
|
42
|
+
*
|
|
43
|
+
* - `spec-managed`: a sibling `<file>.spec.ts`, or a compiled integrity header
|
|
44
|
+
* (a multi-target file such as `AGENTS.md` compiled from `CLAUDE.md.spec.ts`
|
|
45
|
+
* has no sibling spec and is still compiled);
|
|
46
|
+
* - `opted-out`: the {@link REQUIRE_INSTRUCTIONS_SPEC_DISABLE} marker — kept
|
|
47
|
+
* hand-written on purpose, and that decision is remembered;
|
|
48
|
+
* - `hand-written`: neither.
|
|
49
|
+
*
|
|
50
|
+
* Pure: the caller says whether the sibling spec exists.
|
|
51
|
+
*/
|
|
52
|
+
export type InstructionSpecState = "spec-managed" | "opted-out" | "hand-written";
|
|
53
|
+
export declare function instructionSpecState(content: string, siblingSpecExists: boolean): InstructionSpecState;
|
|
54
|
+
//# sourceMappingURL=integrity-header.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Where the vigiles integrity header sits in a file, and whether an
|
|
4
|
+
* instruction file is under a spec — the node-free half of `integrity.ts`.
|
|
5
|
+
*
|
|
6
|
+
* Split out (like `assert-never.ts` from `hash.ts`) so the browser scan twin
|
|
7
|
+
* can read these facts without `node:crypto`; `integrity.ts` re-exports all of
|
|
8
|
+
* it, so callers import from there as before.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.REQUIRE_INSTRUCTIONS_SPEC_DISABLE = void 0;
|
|
12
|
+
exports.splitFrontmatter = splitFrontmatter;
|
|
13
|
+
exports.findIntegrityHeader = findIntegrityHeader;
|
|
14
|
+
exports.instructionSpecState = instructionSpecState;
|
|
15
|
+
const HASH_LINE_RE = /^<!-- vigiles:sha256:([a-f0-9]+) compiled from (.+) -->\r?\n\r?\n?/;
|
|
16
|
+
/**
|
|
17
|
+
* A YAML frontmatter block at the very start of a file: `---\n … \n---\n`.
|
|
18
|
+
*
|
|
19
|
+
* 🔴 THIS EXISTS BECAUSE THE INVARIANT WAS STATED IN THIS FILE AND VIOLATED IN ANOTHER.
|
|
20
|
+
* `ejectMarkdown` (bottom of this module) has documented since it was written that a compiled
|
|
21
|
+
* SKILL.md begins with frontmatter which MUST stay in first position, or "the harness would lose
|
|
22
|
+
* the skill's name/description/tools". `addHash` in compile.ts prepended the integrity header to
|
|
23
|
+
* every compiled file unconditionally — including those same skills.
|
|
24
|
+
*
|
|
25
|
+
* Measured 2026-08-17 on five real skills: after compiling, a reader anchored to `^---` finds NO
|
|
26
|
+
* frontmatter at all. The header was the ENTIRE delta — body byte-identical, section order
|
|
27
|
+
* untouched — so one misplaced line was the whole reason a compiled skill could not be adopted.
|
|
28
|
+
*
|
|
29
|
+
* The header now goes AFTER the frontmatter when there is one. Readers must ask this module where
|
|
30
|
+
* the header is rather than anchoring their own regex at `^`: four call sites had independently
|
|
31
|
+
* encoded "first line", and that duplication is what let the invariant drift unnoticed.
|
|
32
|
+
*/
|
|
33
|
+
const FRONTMATTER_RE = /^---\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n/;
|
|
34
|
+
/** Same header, matched where it sits below a frontmatter block (blank line allowed). */
|
|
35
|
+
const HASH_LINE_IN_BODY_RE = /^\s*<!-- vigiles:sha256:([a-f0-9]+) compiled from (.+) -->\r?\n\r?\n?/;
|
|
36
|
+
/**
|
|
37
|
+
* Split off a leading frontmatter block. `head` is `""` when the file has none, so a caller can
|
|
38
|
+
* concatenate `head + body` unconditionally and get the original content back.
|
|
39
|
+
*/
|
|
40
|
+
function splitFrontmatter(content) {
|
|
41
|
+
const m = FRONTMATTER_RE.exec(content);
|
|
42
|
+
return m
|
|
43
|
+
? { head: m[0], body: content.slice(m[0].length) }
|
|
44
|
+
: { head: "", body: content };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Locate the integrity header wherever it legitimately sits — before the frontmatter (files
|
|
48
|
+
* compiled before 2026-08-17) or after it (files compiled since). Returns the hash, the spec
|
|
49
|
+
* path, and the content with the header removed and everything else intact.
|
|
50
|
+
*
|
|
51
|
+
* This is the ONLY place that knows where the header can be. Matching `/^<!-- vigiles:sha256/`
|
|
52
|
+
* by hand elsewhere is the bug this function exists to make unnecessary.
|
|
53
|
+
*/
|
|
54
|
+
function findIntegrityHeader(content) {
|
|
55
|
+
const { head, body } = splitFrontmatter(content);
|
|
56
|
+
// After the frontmatter — the current placement. The leading `\s*` is load-bearing: the header
|
|
57
|
+
// is written one blank line below the closing `---` for readability, so an anchor at position 0
|
|
58
|
+
// of the body misses it. A test caught exactly that before it shipped.
|
|
59
|
+
const inBody = body.match(HASH_LINE_IN_BODY_RE);
|
|
60
|
+
if (inBody) {
|
|
61
|
+
return {
|
|
62
|
+
hash: inBody[1],
|
|
63
|
+
specFile: inBody[2],
|
|
64
|
+
withoutHeader: head + body.replace(HASH_LINE_IN_BODY_RE, ""),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Or at the very top, which is what a file compiled before the fix looks like. Such a file has
|
|
68
|
+
// no leading frontmatter by definition — the header displaced it — so `head` is empty here.
|
|
69
|
+
const atTop = content.match(HASH_LINE_RE);
|
|
70
|
+
if (!atTop)
|
|
71
|
+
return null;
|
|
72
|
+
return {
|
|
73
|
+
hash: atTop[1],
|
|
74
|
+
specFile: atTop[2],
|
|
75
|
+
withoutHeader: content.replace(HASH_LINE_RE, ""),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** The marker that tells `require-instructions-spec` a file is intentionally
|
|
79
|
+
* hand-owned (no `.spec.ts` expected). */
|
|
80
|
+
exports.REQUIRE_INSTRUCTIONS_SPEC_DISABLE = "<!-- vigiles-disable require-instructions-spec -->";
|
|
81
|
+
/** The disable marker as written by hand: whitespace inside the comment varies. */
|
|
82
|
+
const REQUIRE_INSTRUCTIONS_SPEC_DISABLE_RE = /<!--\s*vigiles-disable\s+require-instructions-spec\s*-->/;
|
|
83
|
+
function instructionSpecState(content, siblingSpecExists) {
|
|
84
|
+
if (siblingSpecExists || findIntegrityHeader(content) !== null)
|
|
85
|
+
return "spec-managed";
|
|
86
|
+
if (REQUIRE_INSTRUCTIONS_SPEC_DISABLE_RE.test(content))
|
|
87
|
+
return "opted-out";
|
|
88
|
+
return "hand-written";
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=integrity-header.js.map
|
package/dist/core/integrity.d.ts
CHANGED
|
@@ -17,28 +17,7 @@
|
|
|
17
17
|
* - "Are committed compiled files actually fresh?"
|
|
18
18
|
* → CI runs `vigiles compile` then `git diff --exit-code`
|
|
19
19
|
*/
|
|
20
|
-
|
|
21
|
-
* Split off a leading frontmatter block. `head` is `""` when the file has none, so a caller can
|
|
22
|
-
* concatenate `head + body` unconditionally and get the original content back.
|
|
23
|
-
*/
|
|
24
|
-
export declare function splitFrontmatter(content: string): {
|
|
25
|
-
head: string;
|
|
26
|
-
body: string;
|
|
27
|
-
};
|
|
28
|
-
/**
|
|
29
|
-
* Locate the integrity header wherever it legitimately sits — before the frontmatter (files
|
|
30
|
-
* compiled before 2026-08-17) or after it (files compiled since). Returns the hash, the spec
|
|
31
|
-
* path, and the content with the header removed and everything else intact.
|
|
32
|
-
*
|
|
33
|
-
* This is the ONLY place that knows where the header can be. Matching `/^<!-- vigiles:sha256/`
|
|
34
|
-
* by hand elsewhere is the bug this function exists to make unnecessary.
|
|
35
|
-
*/
|
|
36
|
-
export declare function findIntegrityHeader(content: string): {
|
|
37
|
-
hash: string;
|
|
38
|
-
specFile: string;
|
|
39
|
-
/** `content` minus the header, frontmatter still in place. */
|
|
40
|
-
withoutHeader: string;
|
|
41
|
-
} | null;
|
|
20
|
+
export { splitFrontmatter, findIntegrityHeader, REQUIRE_INSTRUCTIONS_SPEC_DISABLE, instructionSpecState, type InstructionSpecState, } from "./integrity-header.js";
|
|
42
21
|
/** Render the header in its correct position for this content. */
|
|
43
22
|
export declare function placeIntegrityHeader(content: string, hash: string, specFile: string): string;
|
|
44
23
|
export interface IntegrityResult {
|
|
@@ -50,9 +29,6 @@ export interface IntegrityResult {
|
|
|
50
29
|
* Files without a hash header are treated as hand-written (intact).
|
|
51
30
|
*/
|
|
52
31
|
export declare function checkIntegrity(content: string): IntegrityResult;
|
|
53
|
-
/** The marker that tells `require-instructions-spec` a file is intentionally
|
|
54
|
-
* hand-owned (no `.spec.ts` expected). */
|
|
55
|
-
export declare const REQUIRE_INSTRUCTIONS_SPEC_DISABLE = "<!-- vigiles-disable require-instructions-spec -->";
|
|
56
32
|
/**
|
|
57
33
|
* Parse the `vigiles:sha256 … compiled from <spec>` integrity header, if the
|
|
58
34
|
* file carries one. Returns the referenced spec path and the body below the
|