vigiles 16.0.0 → 16.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +8 -4
- package/dist/core/adopt.js +11 -4
- package/dist/core/compile.js +50 -16
- package/dist/core/coverage.js +6 -3
- package/dist/core/frontmatter-read.js +6 -2
- package/dist/core/integrity.d.ts +24 -0
- package/dist/core/integrity.js +78 -7
- package/dist/core/spec.d.ts +19 -0
- package/dist/core/spec.js +19 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1356,12 +1356,16 @@ function vigilesDepSpec() {
|
|
|
1356
1356
|
const major = parseInt(getVersion(), 10);
|
|
1357
1357
|
return Number.isFinite(major) && major > 0 ? `^${String(major)}` : "latest";
|
|
1358
1358
|
}
|
|
1359
|
-
/** True when the file
|
|
1360
|
-
*
|
|
1359
|
+
/** True when the file carries a vigiles integrity hash (i.e. it is a compiled artifact we own,
|
|
1360
|
+
* safe to overwrite — not hand-written prose).
|
|
1361
|
+
*
|
|
1362
|
+
* 🔴 This used to read the FIRST LINE only. A compiled SKILL.md carries the header after its
|
|
1363
|
+
* frontmatter, so the first line is `---` and this returned false — vigiles would have treated
|
|
1364
|
+
* its own compiled skill as hand-written prose and refused to overwrite it. The whole-file read
|
|
1365
|
+
* is the cost of not encoding the header's position in a fourth place. */
|
|
1361
1366
|
function targetHasHash(absPath) {
|
|
1362
1367
|
try {
|
|
1363
|
-
|
|
1364
|
-
return first.includes("vigiles:sha256");
|
|
1368
|
+
return (0, integrity_js_1.findIntegrityHeader)((0, node_fs_1.readFileSync)(absPath, "utf-8")) !== null;
|
|
1365
1369
|
}
|
|
1366
1370
|
catch {
|
|
1367
1371
|
return false;
|
package/dist/core/adopt.js
CHANGED
|
@@ -205,11 +205,18 @@ function adoptMarkdown(markdown, target) {
|
|
|
205
205
|
// frontmatter-read.ts but matches past the closing fence.
|
|
206
206
|
const FRONTMATTER_CONSUME_RE = /^\uFEFF?(?:<!--[\s\S]*?-->\s*)?---\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n?/;
|
|
207
207
|
function splitFrontmatterBody(markdown) {
|
|
208
|
-
|
|
208
|
+
// Strip the integrity header FIRST, wherever it sits. FRONTMATTER_CONSUME_RE above knows
|
|
209
|
+
// only the pre-2026-08-17 placement (a comment BEFORE the frontmatter); once the header
|
|
210
|
+
// moved below the frontmatter it landed inside what this function calls "the body", so a
|
|
211
|
+
// round-trip adopt \u2192 compile \u2192 re-adopt grew a stamp into the spec's body on every pass.
|
|
212
|
+
// Delegating keeps ONE site that knows where the header can be \u2014 which is the entire point
|
|
213
|
+
// of findIntegrityHeader, and this caller is the one the move missed.
|
|
214
|
+
const source = (0, integrity_js_1.findIntegrityHeader)(markdown)?.withoutHeader ?? markdown;
|
|
215
|
+
const fm = (0, frontmatter_read_js_1.readFrontmatter)(source);
|
|
209
216
|
if (fm.block === null)
|
|
210
|
-
return { fm, body:
|
|
211
|
-
const m = FRONTMATTER_CONSUME_RE.exec(
|
|
212
|
-
return { fm, body: m ?
|
|
217
|
+
return { fm, body: source.replace(/^\uFEFF/, "") };
|
|
218
|
+
const m = FRONTMATTER_CONSUME_RE.exec(source);
|
|
219
|
+
return { fm, body: m ? source.slice(m[0].length) : source };
|
|
213
220
|
}
|
|
214
221
|
/** The first non-empty, non-heading paragraph — the CC fallback for a skill's
|
|
215
222
|
* description when its frontmatter omits one (name←dir, description←first ¶). */
|
package/dist/core/compile.js
CHANGED
|
@@ -27,6 +27,7 @@ const node_fs_1 = require("node:fs");
|
|
|
27
27
|
const glob_1 = require("glob");
|
|
28
28
|
const node_path_1 = require("node:path");
|
|
29
29
|
const hash_js_1 = require("./hash.js");
|
|
30
|
+
const integrity_js_1 = require("./integrity.js");
|
|
30
31
|
const markdown_js_1 = require("./markdown.js");
|
|
31
32
|
const symbols_js_1 = require("./symbols.js");
|
|
32
33
|
const linters_js_1 = require("./linters.js");
|
|
@@ -39,27 +40,42 @@ const DEFAULT_TARGET = "CLAUDE.md";
|
|
|
39
40
|
// ---------------------------------------------------------------------------
|
|
40
41
|
// Hash utilities
|
|
41
42
|
// ---------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
+
// The header's position and format now live in ONE place — `./integrity.js`. The local
|
|
44
|
+
// `HASH_RE` that used to sit here was one of four independent copies of "the header is the first
|
|
45
|
+
// line", and that duplication is what let the placement invariant drift unnoticed (see the block
|
|
46
|
+
// comment on FRONTMATTER_RE in integrity.ts).
|
|
43
47
|
/** @internal Compute SHA-256 hash of content (excluding any existing hash line). */
|
|
44
48
|
function computeHash(content) {
|
|
45
|
-
|
|
46
|
-
return (0, hash_js_1.sha256short)(body);
|
|
49
|
+
return (0, hash_js_1.sha256short)((0, integrity_js_1.findIntegrityHeader)(content)?.withoutHeader ?? content);
|
|
47
50
|
}
|
|
48
51
|
/** @internal Prepend a hash comment to compiled content. */
|
|
49
52
|
function addHash(content, specFile) {
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
// 🔴 THE LAST GATE BEFORE A COMPILED FILE IS WRITTEN. Every compile path returns through here
|
|
54
|
+
// (four call sites), which makes it the one place a whole class of defect can be stopped.
|
|
55
|
+
//
|
|
56
|
+
// `[object Object]` in output means a spec passed an object where the API takes a string, and
|
|
57
|
+
// JS stringified it instead of complaining — types cannot stop this for a user's spec, because
|
|
58
|
+
// `vigiles compile` runs `.spec.ts` through tsx: transpiled, types erased, never checked.
|
|
59
|
+
// Observed 2026-08-17 from `input({ name, description })`, which shipped
|
|
60
|
+
// `argument-hint: <[object Object]>` into a SKILL.md with no error anywhere.
|
|
61
|
+
//
|
|
62
|
+
// Hard error, not a warning: the string never appears legitimately (measured — zero
|
|
63
|
+
// occurrences across every `.md` in this repository), and a file carrying it is broken in a
|
|
64
|
+
// way its author cannot see by reading the spec.
|
|
65
|
+
if (content.includes("[object Object]")) {
|
|
66
|
+
throw new Error(`Compiled output for ${specFile} contains "[object Object]" — a spec value was an object ` +
|
|
67
|
+
`where a string was expected, and JavaScript stringified it. Check the arguments to ` +
|
|
68
|
+
`input()/file()/cmd() and friends: they take strings, not option objects.`);
|
|
69
|
+
}
|
|
70
|
+
return (0, integrity_js_1.placeIntegrityHeader)(content, computeHash(content), specFile);
|
|
52
71
|
}
|
|
53
72
|
/** @internal Check if a file's hash matches its content. Returns null if no hash found. */
|
|
54
73
|
function verifyHash(content) {
|
|
55
|
-
const
|
|
56
|
-
if (!
|
|
74
|
+
const found = (0, integrity_js_1.findIntegrityHeader)(content);
|
|
75
|
+
if (!found)
|
|
57
76
|
return null;
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const body = content.replace(HASH_RE, "");
|
|
61
|
-
const actualHash = (0, hash_js_1.sha256short)(body);
|
|
62
|
-
return { valid: actualHash === expectedHash, specFile };
|
|
77
|
+
const actualHash = (0, hash_js_1.sha256short)(found.withoutHeader);
|
|
78
|
+
return { valid: actualHash === found.hash, specFile: found.specFile };
|
|
63
79
|
}
|
|
64
80
|
// ---------------------------------------------------------------------------
|
|
65
81
|
// Token estimation
|
|
@@ -638,6 +654,14 @@ function renderSkillFrontmatter(spec, profile = "claude-code") {
|
|
|
638
654
|
// ambiguous scalar, so the restriction was lost on the CC round-trip. (#107)
|
|
639
655
|
fm.push(`allowed-tools: [${spec.tools.join(", ")}]`);
|
|
640
656
|
}
|
|
657
|
+
if (spec.disallowedTools && spec.disallowedTools.length > 0) {
|
|
658
|
+
// 🔴 `disallowed-tools`, HYPHENATED — that is a skill's fence. A subagent's
|
|
659
|
+
// key is `disallowedTools:` (camelCase) and a different reader parses it, so
|
|
660
|
+
// writing the agent spelling here emits a key nothing looks at: inert, and
|
|
661
|
+
// inert in the direction that reads as protection. Same class as the #107
|
|
662
|
+
// defect two lines up, where `tools:` on a skill silently lost the contract.
|
|
663
|
+
fm.push(`disallowed-tools: [${spec.disallowedTools.join(", ")}]`);
|
|
664
|
+
}
|
|
641
665
|
}
|
|
642
666
|
fm.push("", "---");
|
|
643
667
|
return fm.join("\n");
|
|
@@ -763,6 +787,15 @@ function compileSkill(spec, options = {}) {
|
|
|
763
787
|
errors.push({ type: "purity-violation", message: v.message });
|
|
764
788
|
}
|
|
765
789
|
}
|
|
790
|
+
// A fence entry that is a close typo of a real tool blocks NOTHING while reading
|
|
791
|
+
// as protection — the same high-precision check a subagent's `disallowedTools`
|
|
792
|
+
// gets. Skipped without a dialect, like the purity check above, because the tool
|
|
793
|
+
// catalog is what a typo is measured against.
|
|
794
|
+
if (spec.disallowedTools && options.dialect) {
|
|
795
|
+
for (const issue of (0, tool_contract_js_1.disallowedToolIssues)(spec.disallowedTools, options.dialect)) {
|
|
796
|
+
errors.push({ type: "unknown-tool", message: issue.message });
|
|
797
|
+
}
|
|
798
|
+
}
|
|
766
799
|
const sections = renderSkillSections(spec);
|
|
767
800
|
// Over-long inline code blocks are WARNINGS, not errors — they don't block
|
|
768
801
|
// compilation (so adoption always compiles), just nudge toward file().
|
|
@@ -1077,10 +1110,11 @@ function adoptDiff(filePath, spec, basePath, dialect) {
|
|
|
1077
1110
|
compiledContent = markdown;
|
|
1078
1111
|
}
|
|
1079
1112
|
// Simple line-based diff
|
|
1080
|
-
const currentLines =
|
|
1081
|
-
const compiledLines = (
|
|
1082
|
-
|
|
1083
|
-
.split("\n");
|
|
1113
|
+
const currentLines = ((0, integrity_js_1.findIntegrityHeader)(currentContent)?.withoutHeader ?? currentContent).split("\n");
|
|
1114
|
+
const compiledLines = (() => {
|
|
1115
|
+
const c = compiledContent ?? "";
|
|
1116
|
+
return ((0, integrity_js_1.findIntegrityHeader)(c)?.withoutHeader ?? c).split("\n");
|
|
1117
|
+
})();
|
|
1084
1118
|
const currentSet = new Set(currentLines);
|
|
1085
1119
|
const compiledSet = new Set(compiledLines);
|
|
1086
1120
|
const addedLines = currentLines.filter((l) => l.trim() && !compiledSet.has(l));
|
package/dist/core/coverage.js
CHANGED
|
@@ -17,6 +17,7 @@ exports.computeLinterRuleCoverage = computeLinterRuleCoverage;
|
|
|
17
17
|
exports.checkCoverage = checkCoverage;
|
|
18
18
|
exports.formatCoverageReport = formatCoverageReport;
|
|
19
19
|
const node_fs_1 = require("node:fs");
|
|
20
|
+
const integrity_js_1 = require("./integrity.js");
|
|
20
21
|
const node_path_1 = require("node:path");
|
|
21
22
|
const glob_1 = require("glob");
|
|
22
23
|
const compile_js_1 = require("./compile.js");
|
|
@@ -57,11 +58,13 @@ function collectDocumentedCommands(basePath, specs) {
|
|
|
57
58
|
const fullPath = (0, node_path_1.resolve)(basePath, mdFile);
|
|
58
59
|
try {
|
|
59
60
|
const content = (0, node_fs_1.readFileSync)(fullPath, "utf-8");
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
// Ask integrity.ts where the header is — a compiled SKILL.md carries it AFTER its
|
|
62
|
+
// frontmatter, so an `^`-anchored match here silently skipped every skill.
|
|
63
|
+
const found = (0, integrity_js_1.findIntegrityHeader)(content);
|
|
64
|
+
if (!found)
|
|
62
65
|
continue;
|
|
63
66
|
// Try to load the spec's compiled JS from dist/
|
|
64
|
-
const specFile =
|
|
67
|
+
const specFile = found.specFile;
|
|
65
68
|
const jsPath = (0, node_path_1.resolve)(basePath, "dist", specFile.replace(/\.ts$/, ".js"));
|
|
66
69
|
if ((0, node_fs_1.existsSync)(jsPath)) {
|
|
67
70
|
try {
|
|
@@ -24,8 +24,12 @@ const js_yaml_1 = require("js-yaml");
|
|
|
24
24
|
// Frontmatter is the very first thing in the file. Anchoring at the start — not
|
|
25
25
|
// `(?:^|\n)` — means a `---` horizontal rule in the BODY is never mistaken for
|
|
26
26
|
// frontmatter (which matters for the malformed-YAML verdict). A leading BOM is
|
|
27
|
-
// stripped first; an optional leading HTML comment is allowed too
|
|
28
|
-
//
|
|
27
|
+
// stripped first; an optional leading HTML comment is allowed too, which is what
|
|
28
|
+
// lets this reader still parse files compiled BEFORE 2026-08-17, when vigiles put
|
|
29
|
+
// the `<!-- vigiles:sha256:… -->` stamp above the `---` and thereby hid the
|
|
30
|
+
// frontmatter from every stricter reader. Since then the stamp goes BELOW the
|
|
31
|
+
// frontmatter (placeIntegrityHeader), so this branch is backward compatibility,
|
|
32
|
+
// not a description of what the compiler emits today.
|
|
29
33
|
const BLOCK_RE = /^\uFEFF?(?:<!--[\s\S]*?-->\s*)?---\r?\n([\s\S]*?)\r?\n---/;
|
|
30
34
|
/** A YAML block-scalar indicator: `>`/`|` with optional chomp (`+`/`-`) + indent digit. */
|
|
31
35
|
const BLOCK_SCALAR_RE = /^[|>][+-]?\d*$/;
|
package/dist/core/integrity.d.ts
CHANGED
|
@@ -17,6 +17,30 @@
|
|
|
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;
|
|
42
|
+
/** Render the header in its correct position for this content. */
|
|
43
|
+
export declare function placeIntegrityHeader(content: string, hash: string, specFile: string): string;
|
|
20
44
|
export interface IntegrityResult {
|
|
21
45
|
intact: boolean;
|
|
22
46
|
reason?: string;
|
package/dist/core/integrity.js
CHANGED
|
@@ -20,22 +20,93 @@
|
|
|
20
20
|
*/
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
22
|
exports.REQUIRE_INSTRUCTIONS_SPEC_DISABLE = void 0;
|
|
23
|
+
exports.splitFrontmatter = splitFrontmatter;
|
|
24
|
+
exports.findIntegrityHeader = findIntegrityHeader;
|
|
25
|
+
exports.placeIntegrityHeader = placeIntegrityHeader;
|
|
23
26
|
exports.checkIntegrity = checkIntegrity;
|
|
24
27
|
exports.parseIntegrityHeader = parseIntegrityHeader;
|
|
25
28
|
exports.ejectMarkdown = ejectMarkdown;
|
|
26
29
|
const hash_js_1 = require("./hash.js");
|
|
27
30
|
const HASH_LINE_RE = /^<!-- vigiles:sha256:([a-f0-9]+) compiled from (.+) -->\r?\n\r?\n?/;
|
|
31
|
+
/**
|
|
32
|
+
* A YAML frontmatter block at the very start of a file: `---\n … \n---\n`.
|
|
33
|
+
*
|
|
34
|
+
* 🔴 THIS EXISTS BECAUSE THE INVARIANT WAS STATED IN THIS FILE AND VIOLATED IN ANOTHER.
|
|
35
|
+
* `ejectMarkdown` (bottom of this module) has documented since it was written that a compiled
|
|
36
|
+
* SKILL.md begins with frontmatter which MUST stay in first position, or "the harness would lose
|
|
37
|
+
* the skill's name/description/tools". `addHash` in compile.ts prepended the integrity header to
|
|
38
|
+
* every compiled file unconditionally — including those same skills.
|
|
39
|
+
*
|
|
40
|
+
* Measured 2026-08-17 on five real skills: after compiling, a reader anchored to `^---` finds NO
|
|
41
|
+
* frontmatter at all. The header was the ENTIRE delta — body byte-identical, section order
|
|
42
|
+
* untouched — so one misplaced line was the whole reason a compiled skill could not be adopted.
|
|
43
|
+
*
|
|
44
|
+
* The header now goes AFTER the frontmatter when there is one. Readers must ask this module where
|
|
45
|
+
* the header is rather than anchoring their own regex at `^`: four call sites had independently
|
|
46
|
+
* encoded "first line", and that duplication is what let the invariant drift unnoticed.
|
|
47
|
+
*/
|
|
48
|
+
const FRONTMATTER_RE = /^---\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n/;
|
|
49
|
+
/** Same header, matched where it sits below a frontmatter block (blank line allowed). */
|
|
50
|
+
const HASH_LINE_IN_BODY_RE = /^\s*<!-- vigiles:sha256:([a-f0-9]+) compiled from (.+) -->\r?\n\r?\n?/;
|
|
51
|
+
/**
|
|
52
|
+
* Split off a leading frontmatter block. `head` is `""` when the file has none, so a caller can
|
|
53
|
+
* concatenate `head + body` unconditionally and get the original content back.
|
|
54
|
+
*/
|
|
55
|
+
function splitFrontmatter(content) {
|
|
56
|
+
const m = FRONTMATTER_RE.exec(content);
|
|
57
|
+
return m
|
|
58
|
+
? { head: m[0], body: content.slice(m[0].length) }
|
|
59
|
+
: { head: "", body: content };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Locate the integrity header wherever it legitimately sits — before the frontmatter (files
|
|
63
|
+
* compiled before 2026-08-17) or after it (files compiled since). Returns the hash, the spec
|
|
64
|
+
* path, and the content with the header removed and everything else intact.
|
|
65
|
+
*
|
|
66
|
+
* This is the ONLY place that knows where the header can be. Matching `/^<!-- vigiles:sha256/`
|
|
67
|
+
* by hand elsewhere is the bug this function exists to make unnecessary.
|
|
68
|
+
*/
|
|
69
|
+
function findIntegrityHeader(content) {
|
|
70
|
+
const { head, body } = splitFrontmatter(content);
|
|
71
|
+
// After the frontmatter — the current placement. The leading `\s*` is load-bearing: the header
|
|
72
|
+
// is written one blank line below the closing `---` for readability, so an anchor at position 0
|
|
73
|
+
// of the body misses it. A test caught exactly that before it shipped.
|
|
74
|
+
const inBody = body.match(HASH_LINE_IN_BODY_RE);
|
|
75
|
+
if (inBody) {
|
|
76
|
+
return {
|
|
77
|
+
hash: inBody[1],
|
|
78
|
+
specFile: inBody[2],
|
|
79
|
+
withoutHeader: head + body.replace(HASH_LINE_IN_BODY_RE, ""),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// Or at the very top, which is what a file compiled before the fix looks like. Such a file has
|
|
83
|
+
// no leading frontmatter by definition — the header displaced it — so `head` is empty here.
|
|
84
|
+
const atTop = content.match(HASH_LINE_RE);
|
|
85
|
+
if (!atTop)
|
|
86
|
+
return null;
|
|
87
|
+
return {
|
|
88
|
+
hash: atTop[1],
|
|
89
|
+
specFile: atTop[2],
|
|
90
|
+
withoutHeader: content.replace(HASH_LINE_RE, ""),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** Render the header in its correct position for this content. */
|
|
94
|
+
function placeIntegrityHeader(content, hash, specFile) {
|
|
95
|
+
const stamp = `<!-- vigiles:sha256:${hash} compiled from ${specFile} -->`;
|
|
96
|
+
const { head, body } = splitFrontmatter(content);
|
|
97
|
+
return head ? `${head}\n${stamp}\n\n${body}` : `${stamp}\n\n${body}`;
|
|
98
|
+
}
|
|
28
99
|
/**
|
|
29
100
|
* Check whether the compiled markdown's SHA-256 hash matches its body.
|
|
30
101
|
* Files without a hash header are treated as hand-written (intact).
|
|
31
102
|
*/
|
|
32
103
|
function checkIntegrity(content) {
|
|
33
|
-
const
|
|
34
|
-
if (!
|
|
104
|
+
const found = findIntegrityHeader(content);
|
|
105
|
+
if (!found) {
|
|
35
106
|
return { intact: true, reason: "No hash header (hand-written file)" };
|
|
36
107
|
}
|
|
37
|
-
const expectedHash =
|
|
38
|
-
const body =
|
|
108
|
+
const expectedHash = found.hash;
|
|
109
|
+
const body = found.withoutHeader;
|
|
39
110
|
if ((0, hash_js_1.sha256short)(body) !== expectedHash) {
|
|
40
111
|
return {
|
|
41
112
|
intact: false,
|
|
@@ -53,10 +124,10 @@ exports.REQUIRE_INSTRUCTIONS_SPEC_DISABLE = "<!-- vigiles-disable require-instru
|
|
|
53
124
|
* header; `null` when the file is plain markdown (no header).
|
|
54
125
|
*/
|
|
55
126
|
function parseIntegrityHeader(content) {
|
|
56
|
-
const
|
|
57
|
-
if (!
|
|
127
|
+
const found = findIntegrityHeader(content);
|
|
128
|
+
if (!found)
|
|
58
129
|
return null;
|
|
59
|
-
return { specFile:
|
|
130
|
+
return { specFile: found.specFile, body: found.withoutHeader };
|
|
60
131
|
}
|
|
61
132
|
/**
|
|
62
133
|
* "Eject" a compiled instruction file to plain, hand-owned markdown: strip the
|
package/dist/core/spec.d.ts
CHANGED
|
@@ -448,6 +448,25 @@ export interface SkillSpec {
|
|
|
448
448
|
* against that floor — compile rejects a tool looser than the declared level.
|
|
449
449
|
*/
|
|
450
450
|
readonly tools?: readonly string[];
|
|
451
|
+
/**
|
|
452
|
+
* Tools this skill may NEVER use — rendered to the `disallowed-tools:`
|
|
453
|
+
* frontmatter key (hyphenated for skills; a subagent's key is `disallowedTools:`,
|
|
454
|
+
* and they are read by different parsers, so the spelling is not interchangeable).
|
|
455
|
+
*
|
|
456
|
+
* 🔴 This is NOT the symmetric twin of `tools` and the asymmetry is the whole
|
|
457
|
+
* reason it exists. `allowed-tools:` on a skill is a PRE-APPROVAL — it waives the
|
|
458
|
+
* permission prompt for what it lists and removes nothing from the pool, so a
|
|
459
|
+
* skill that declares only `Read` can still call `Bash`. `disallowed-tools:` is
|
|
460
|
+
* the only key measured to actually take a tool away. A skill without it inherits
|
|
461
|
+
* every tool the session grants, which is why `audit` reports each such skill as
|
|
462
|
+
* holding all three lethal-trifecta legs — including skills whose `tools` list
|
|
463
|
+
* looks tightly scoped.
|
|
464
|
+
*
|
|
465
|
+
* Entries are checked like a subagent's: a close typo of a real tool name blocks
|
|
466
|
+
* nothing and is reported, because a fence with a misspelled name reads as
|
|
467
|
+
* protection while granting everything.
|
|
468
|
+
*/
|
|
469
|
+
readonly disallowedTools?: readonly string[];
|
|
451
470
|
/**
|
|
452
471
|
* Declare this skill's purity floor — compile rejects a tool contract looser
|
|
453
472
|
* than it. `"pure"` allows only read-only tools; `"bounded"` also allows
|
package/dist/core/spec.js
CHANGED
|
@@ -226,6 +226,25 @@ function project(role) {
|
|
|
226
226
|
* The reason is on `experimental_skill` below.
|
|
227
227
|
*/
|
|
228
228
|
function input(name, hint, opts = {}) {
|
|
229
|
+
// 🔴 The types say `string`, and for a USER's spec nothing enforces that: `vigiles compile`
|
|
230
|
+
// loads `.spec.ts` through tsx, which transpiles and erases types without checking them.
|
|
231
|
+
// (vigiles's OWN specs are cross-checked by a separate `tsc --noEmit` over a generated
|
|
232
|
+
// registry — that safety net does not travel to the people compiling their own specs.)
|
|
233
|
+
//
|
|
234
|
+
// Measured 2026-08-17: calling `input({ name, description })` — the object form a reader
|
|
235
|
+
// reasonably guesses — compiled with NO error and wrote this into the shipped SKILL.md:
|
|
236
|
+
//
|
|
237
|
+
// argument-hint: <[object Object]>
|
|
238
|
+
// - `$1` **[object Object]** — undefined
|
|
239
|
+
//
|
|
240
|
+
// A wrong call became a wrong file, silently. Refusing here makes the mistake loud at the
|
|
241
|
+
// moment it is made, and the message carries the real signature because "expected string"
|
|
242
|
+
// alone does not tell the caller what to write instead.
|
|
243
|
+
const bad = (v) => typeof v !== "string" || v.trim() === "";
|
|
244
|
+
if (bad(name) || bad(hint)) {
|
|
245
|
+
throw new TypeError(`input() takes two strings: input(name, hint, opts?) — e.g. input("pattern", "regex to search for").\n` +
|
|
246
|
+
` got name=${JSON.stringify(name)}, hint=${JSON.stringify(hint)}`);
|
|
247
|
+
}
|
|
229
248
|
return { name, hint, required: opts.required };
|
|
230
249
|
}
|
|
231
250
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "16.
|
|
3
|
+
"version": "16.1.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",
|