vigiles 2.1.1 → 2.2.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.
@@ -56,11 +56,11 @@ function findLine(lines, needle, fromIndex) {
56
56
  return fromIndex + 1;
57
57
  }
58
58
  /**
59
- * Navigate a parsed frontmatter document to its `vigiles.enforce` list.
60
- * Returns "none" when there's nothing for vigiles to check, "error" when a
61
- * `vigiles`/`enforce` key is present but the wrong shape, or the list.
59
+ * Navigate a parsed frontmatter document to its `vigiles` mapping. Returns
60
+ * "none" when there's nothing for vigiles to check, "error" when the
61
+ * `vigiles` key is present but not a mapping, or the mapping itself.
62
62
  */
63
- function lookupEnforce(doc, lines, startLine) {
63
+ function getVigiles(doc, lines, startLine) {
64
64
  if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
65
65
  return { kind: "none" };
66
66
  }
@@ -78,6 +78,13 @@ function lookupEnforce(doc, lines, startLine) {
78
78
  },
79
79
  };
80
80
  }
81
+ return { kind: "map", vigiles: vigiles };
82
+ }
83
+ /**
84
+ * Locate the `vigiles.enforce` list. Returns "none" when absent, "error" when
85
+ * present but not a list, or the list with its source line.
86
+ */
87
+ function lookupEnforce(vigiles, lines, startLine) {
81
88
  const enforce = vigiles.enforce;
82
89
  if (enforce === undefined)
83
90
  return { kind: "none" };
@@ -93,6 +100,45 @@ function lookupEnforce(doc, lines, startLine) {
93
100
  }
94
101
  return { kind: "list", enforce, enforceLine };
95
102
  }
103
+ /**
104
+ * Parse a `vigiles.<key>` list of plain strings (used for `files` and
105
+ * `commands`). Returns located items plus error findings for the wrong shape
106
+ * or non-string entries; an absent key yields empty results.
107
+ */
108
+ function parseStringList(vigiles, key, lines, startLine) {
109
+ const raw = vigiles[key];
110
+ if (raw === undefined)
111
+ return { items: [], errors: [] };
112
+ const keyLine = findLine(lines, `${key}:`, startLine - 1);
113
+ if (!Array.isArray(raw)) {
114
+ return {
115
+ items: [],
116
+ errors: [
117
+ {
118
+ line: keyLine,
119
+ message: `\`vigiles.${key}\` must be a list of strings.`,
120
+ },
121
+ ],
122
+ };
123
+ }
124
+ const items = [];
125
+ const errors = [];
126
+ let cursor = keyLine;
127
+ for (let i = 0; i < raw.length; i++) {
128
+ const v = raw[i];
129
+ if (typeof v !== "string" || v.trim() === "") {
130
+ errors.push({
131
+ line: keyLine,
132
+ message: `vigiles.${key}[${String(i)}] must be a non-empty string.`,
133
+ });
134
+ continue;
135
+ }
136
+ const line = findLine(lines, v, cursor);
137
+ cursor = line;
138
+ items.push({ value: v, line });
139
+ }
140
+ return { items, errors };
141
+ }
96
142
  /** Parse one `vigiles.enforce` entry into a rule or an error finding. */
97
143
  function parseEntry(entry, index, ctx) {
98
144
  if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
@@ -126,10 +172,15 @@ function parseEntry(entry, index, ctx) {
126
172
  }
127
173
  return { rule: { linterRule: rule, why, line }, nextCursor: line };
128
174
  }
175
+ /** Fresh empty result — callers may push into the arrays, so never shared. */
176
+ function emptyResult() {
177
+ return { rules: [], files: [], commands: [], errors: [] };
178
+ }
129
179
  /**
130
- * Parse `vigiles.enforce` rules out of a markdown file's YAML frontmatter.
131
- * Does not touch the filesystem and does not verify rules against any
132
- * linter — callers feed the returned rules into `checkLinterRule`.
180
+ * Parse `vigiles.enforce` rules, `vigiles.files`, and `vigiles.commands` out
181
+ * of a markdown file's YAML frontmatter. Does not touch the filesystem and
182
+ * does not verify references — callers feed rules into `checkLinterRule` and
183
+ * file/command refs into `validateFileRef` / `validateCommandRef`.
133
184
  *
134
185
  * A file with no frontmatter, or frontmatter with no `vigiles` key, yields
135
186
  * empty results with no errors. Malformed YAML or a malformed `vigiles`
@@ -138,7 +189,7 @@ function parseEntry(entry, index, ctx) {
138
189
  function parseFrontmatterRules(content) {
139
190
  const fm = extractFrontmatter(content);
140
191
  if (!fm)
141
- return { rules: [], errors: [] };
192
+ return emptyResult();
142
193
  const lines = content.split("\n");
143
194
  let doc;
144
195
  try {
@@ -149,6 +200,8 @@ function parseFrontmatterRules(content) {
149
200
  const line = (err.mark?.line ?? 0) + fm.startLine;
150
201
  return {
151
202
  rules: [],
203
+ files: [],
204
+ commands: [],
152
205
  errors: [
153
206
  {
154
207
  line,
@@ -157,34 +210,54 @@ function parseFrontmatterRules(content) {
157
210
  ],
158
211
  };
159
212
  }
160
- const lookup = lookupEnforce(doc, lines, fm.startLine);
161
- if (lookup.kind === "none")
162
- return { rules: [], errors: [] };
163
- if (lookup.kind === "error")
164
- return { rules: [], errors: [lookup.error] };
213
+ const vig = getVigiles(doc, lines, fm.startLine);
214
+ if (vig.kind === "none")
215
+ return emptyResult();
216
+ if (vig.kind === "error")
217
+ return { rules: [], files: [], commands: [], errors: [vig.error] };
165
218
  const rules = [];
166
219
  const errors = [];
167
- let cursor = lookup.enforceLine; // search start: line after `enforce:`
168
- for (let i = 0; i < lookup.enforce.length; i++) {
169
- const r = parseEntry(lookup.enforce[i], i, {
170
- lines,
171
- enforceLine: lookup.enforceLine,
172
- cursor,
173
- });
174
- cursor = r.nextCursor;
175
- if (r.rule)
176
- rules.push(r.rule);
177
- if (r.error)
178
- errors.push(r.error);
220
+ const enforceLookup = lookupEnforce(vig.vigiles, lines, fm.startLine);
221
+ if (enforceLookup.kind === "error") {
222
+ errors.push(enforceLookup.error);
223
+ }
224
+ else if (enforceLookup.kind === "list") {
225
+ let cursor = enforceLookup.enforceLine; // search start: line after `enforce:`
226
+ for (let i = 0; i < enforceLookup.enforce.length; i++) {
227
+ const r = parseEntry(enforceLookup.enforce[i], i, {
228
+ lines,
229
+ enforceLine: enforceLookup.enforceLine,
230
+ cursor,
231
+ });
232
+ cursor = r.nextCursor;
233
+ if (r.rule)
234
+ rules.push(r.rule);
235
+ if (r.error)
236
+ errors.push(r.error);
237
+ }
179
238
  }
180
- return { rules, errors };
239
+ const fileList = parseStringList(vig.vigiles, "files", lines, fm.startLine);
240
+ errors.push(...fileList.errors);
241
+ const files = fileList.items.map((it) => ({
242
+ path: it.value,
243
+ line: it.line,
244
+ }));
245
+ const cmdList = parseStringList(vig.vigiles, "commands", lines, fm.startLine);
246
+ errors.push(...cmdList.errors);
247
+ const commands = cmdList.items.map((it) => ({
248
+ command: it.value,
249
+ line: it.line,
250
+ }));
251
+ return { rules, files, commands, errors };
181
252
  }
182
253
  /**
183
- * True if the content has at least one parseable `vigiles.enforce` rule in
184
- * its frontmatter. Used by `require-spec` validation to treat frontmatter
185
- * mode as spec-equivalent, mirroring `hasInlineRules`.
254
+ * True if the content has at least one parseable `vigiles` reference in its
255
+ * frontmatter an `enforce` rule, a `files` entry, or a `commands` entry.
256
+ * Used by `require-spec` validation to treat frontmatter mode as
257
+ * spec-equivalent, mirroring `hasInlineRules`.
186
258
  */
187
259
  function hasFrontmatterRules(content) {
188
- return parseFrontmatterRules(content).rules.length > 0;
260
+ const r = parseFrontmatterRules(content);
261
+ return r.rules.length + r.files.length + r.commands.length > 0;
189
262
  }
190
263
  //# sourceMappingURL=frontmatter.js.map
@@ -101,6 +101,16 @@ function generateSchema(options = {}) {
101
101
  },
102
102
  },
103
103
  },
104
+ files: {
105
+ type: "array",
106
+ description: "File paths referenced by this instruction file, verified to exist by `vigiles audit`.",
107
+ items: { type: "string" },
108
+ },
109
+ commands: {
110
+ type: "array",
111
+ description: "Commands (npm scripts / script-runner invocations) referenced here, verified by `vigiles audit`.",
112
+ items: { type: "string" },
113
+ },
104
114
  },
105
115
  },
106
116
  },
@@ -0,0 +1,38 @@
1
+ import { type ModelTurn } from "./mock-model.js";
2
+ export { scriptModel, type ModelTurn } from "./mock-model.js";
3
+ export interface HarnessTestSpec {
4
+ /** Fixture files to write in a fresh temp working dir (path → contents). */
5
+ readonly files?: Record<string, string>;
6
+ /** `.claude/settings.json` contents — the hooks/permissions under test. */
7
+ readonly settings?: unknown;
8
+ /** The scripted model turns the agent will take. */
9
+ readonly model: readonly ModelTurn[];
10
+ /** The user prompt. Default: "go". */
11
+ readonly prompt?: string;
12
+ /** Tools the agent may use. Default: Read Edit Write Bash. */
13
+ readonly allowedTools?: readonly string[];
14
+ /** Per-run wall-clock timeout in ms. Default 60000. */
15
+ readonly timeoutMs?: number;
16
+ }
17
+ export interface HarnessTestResult {
18
+ readonly exitCode: number;
19
+ readonly stdout: string;
20
+ /** Hook block messages and diagnostics land here. */
21
+ readonly stderr: string;
22
+ /** The temp working dir (inspect or clean it up). */
23
+ readonly cwd: string;
24
+ /** Number of model turns the agent took (mock turns served). */
25
+ readonly turns: number;
26
+ /** Final contents of a file under the working dir, or null if absent. */
27
+ file(path: string): string | null;
28
+ /** Remove the temp working dir. */
29
+ cleanup(): void;
30
+ }
31
+ /** Whether the `claude` CLI is available — harness tests need it. */
32
+ export declare function claudeAvailable(): boolean;
33
+ /**
34
+ * Run the real `claude` CLI against a scripted mock model, with the given
35
+ * fixture and settings (hooks). Deterministic — same script, same result.
36
+ */
37
+ export declare function runHarnessTest(spec: HarnessTestSpec): Promise<HarnessTestResult>;
38
+ //# sourceMappingURL=harness-test.d.ts.map
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scriptModel = void 0;
4
+ exports.claudeAvailable = claudeAvailable;
5
+ exports.runHarnessTest = runHarnessTest;
6
+ /**
7
+ * vigiles — deterministic Claude Code harness testing.
8
+ *
9
+ * Test what your *harness* does — hooks, settings, skills, instruction files —
10
+ * without paying for or depending on a real model. `runHarnessTest` spins up the
11
+ * real `claude` CLI (so your real hooks and settings fire exactly as in
12
+ * production) but points it at a scripted mock model (`src/mock-model.ts`), so
13
+ * the agent's turns are fixed and the outcome is reproducible. No API key, no
14
+ * cost, CI-friendly.
15
+ *
16
+ * const r = await runHarnessTest({
17
+ * settings: { hooks: { Stop: [{ hooks: [{ type: "command",
18
+ * command: "test -f DONE || { echo 'not done' >&2; exit 2; }" }] }] } },
19
+ * model: scriptModel([
20
+ * { text: "I'm done" }, // tries to stop → blocked
21
+ * { tool: "Bash", input: { command: "touch DONE" } },
22
+ * { text: "now done" },
23
+ * ]),
24
+ * });
25
+ * assert(JSON.parse(r.stdout).num_turns > 1); // the Stop hook fired
26
+ *
27
+ * The "steps" are the scripted model turns — their real home is deterministic
28
+ * harness testing, not production enforcement.
29
+ *
30
+ * Note: the simple mock drives the Bash tool and Stop hooks reliably; the
31
+ * Edit/Write tools are gated in headless mode and don't fire via the mock —
32
+ * drive file actions through Bash, or use the real-model eval tier (`eval.ts`)
33
+ * for Edit/Write hooks.
34
+ */
35
+ const node_child_process_1 = require("node:child_process");
36
+ const node_fs_1 = require("node:fs");
37
+ const node_os_1 = require("node:os");
38
+ const node_path_1 = require("node:path");
39
+ const mock_model_js_1 = require("./mock-model.js");
40
+ var mock_model_js_2 = require("./mock-model.js");
41
+ Object.defineProperty(exports, "scriptModel", { enumerable: true, get: function () { return mock_model_js_2.scriptModel; } });
42
+ /** Whether the `claude` CLI is available — harness tests need it. */
43
+ function claudeAvailable() {
44
+ try {
45
+ return (0, node_child_process_1.spawnSync)("claude", ["--version"], { stdio: "ignore" }).status === 0;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ function writeFixture(cwd, spec) {
52
+ for (const [p, content] of Object.entries(spec.files ?? {})) {
53
+ const full = (0, node_path_1.resolve)(cwd, p);
54
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(full), { recursive: true });
55
+ (0, node_fs_1.writeFileSync)(full, content);
56
+ }
57
+ if (spec.settings !== undefined) {
58
+ // `{cwd}` in any hook command is substituted with the working dir, so a
59
+ // hook can reference an absolute path inside it (hooks don't run with the
60
+ // project dir as cwd).
61
+ const json = JSON.stringify(spec.settings, null, 2).replaceAll("{cwd}", cwd);
62
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(cwd, "settings.json"), json);
63
+ }
64
+ }
65
+ function spawnClaude(args, cwd, baseUrl, timeoutMs) {
66
+ return new Promise((resolvePromise) => {
67
+ const child = (0, node_child_process_1.spawn)("claude", args, {
68
+ cwd,
69
+ env: {
70
+ ...process.env,
71
+ ANTHROPIC_BASE_URL: baseUrl,
72
+ // Any value works — the mock ignores auth; this avoids needing a real key.
73
+ ANTHROPIC_API_KEY: "sk-vigiles-mock",
74
+ },
75
+ stdio: ["ignore", "pipe", "pipe"],
76
+ });
77
+ let stdout = "";
78
+ let stderr = "";
79
+ child.stdout.on("data", (d) => (stdout += d.toString()));
80
+ child.stderr.on("data", (d) => (stderr += d.toString()));
81
+ const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
82
+ child.on("close", (code) => {
83
+ clearTimeout(timer);
84
+ resolvePromise({ code: code ?? 0, stdout, stderr });
85
+ });
86
+ });
87
+ }
88
+ /**
89
+ * Run the real `claude` CLI against a scripted mock model, with the given
90
+ * fixture and settings (hooks). Deterministic — same script, same result.
91
+ */
92
+ async function runHarnessTest(spec) {
93
+ const cwd = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "vigiles-harness-"));
94
+ writeFixture(cwd, spec);
95
+ const mock = await (0, mock_model_js_1.startMock)(spec.model);
96
+ try {
97
+ const tools = spec.allowedTools ?? ["Read", "Edit", "Write", "Bash"];
98
+ const args = [
99
+ "-p",
100
+ spec.prompt ?? "go",
101
+ "--output-format",
102
+ "json",
103
+ "--model",
104
+ "claude-sonnet-4-5",
105
+ ...(spec.settings !== undefined ? ["--settings", "settings.json"] : []),
106
+ "--allowedTools",
107
+ ...tools,
108
+ ];
109
+ const out = await spawnClaude(args, cwd, mock.url, spec.timeoutMs ?? 60000);
110
+ return {
111
+ exitCode: out.code,
112
+ stdout: out.stdout,
113
+ stderr: out.stderr,
114
+ cwd,
115
+ turns: mock.count,
116
+ file: (p) => {
117
+ const f = (0, node_path_1.resolve)(cwd, p);
118
+ return (0, node_fs_1.existsSync)(f) ? (0, node_fs_1.readFileSync)(f, "utf-8") : null;
119
+ },
120
+ cleanup: () => {
121
+ (0, node_fs_1.rmSync)(cwd, { recursive: true, force: true });
122
+ },
123
+ };
124
+ }
125
+ finally {
126
+ mock.close();
127
+ }
128
+ }
129
+ //# sourceMappingURL=harness-test.js.map
package/dist/inline.d.ts CHANGED
@@ -25,8 +25,24 @@ export interface InlineRule {
25
25
  /** 1-based line number of the comment in the source file. */
26
26
  line: number;
27
27
  }
28
+ /** A `<!-- vigiles:file <path> -->` reference (verified to exist). */
29
+ export interface InlineFileRef {
30
+ /** Project-relative path to verify exists. */
31
+ path: string;
32
+ /** 1-based line number of the comment in the source file. */
33
+ line: number;
34
+ }
35
+ /** A `<!-- vigiles:cmd "<command>" -->` reference (npm scripts verified). */
36
+ export interface InlineCmdRef {
37
+ /** Command to verify (npm scripts checked against package.json). */
38
+ command: string;
39
+ /** 1-based line number of the comment in the source file. */
40
+ line: number;
41
+ }
28
42
  export interface InlineParseResult {
29
43
  rules: InlineRule[];
44
+ files: InlineFileRef[];
45
+ commands: InlineCmdRef[];
30
46
  /** Lines that look like vigiles: markers but failed to parse. */
31
47
  errors: {
32
48
  line: number;
@@ -46,13 +62,15 @@ export interface InlineParseResult {
46
62
  */
47
63
  export declare function parseInlineRules(content: string): InlineParseResult;
48
64
  /**
49
- * True if the content contains at least one parseable vigiles:enforce
50
- * rule (ignoring fenced code blocks and malformed markers). Used by
51
- * `require-spec` validation to treat inline mode as spec-equivalent.
65
+ * True if the content contains at least one parseable vigiles inline marker —
66
+ * an `enforce` rule, a `file` reference, or a `cmd` reference (ignoring fenced
67
+ * code blocks and malformed markers). Used by `require-spec` validation to
68
+ * treat inline mode as spec-equivalent: a file that pins even a single path is
69
+ * meaningfully managed.
52
70
  *
53
71
  * Deliberately delegates to `parseInlineRules` so a loose prefix regex
54
72
  * can't satisfy require-spec with a malformed marker that produces no
55
- * real enforceable rule.
73
+ * real reference.
56
74
  */
57
75
  export declare function hasInlineRules(content: string): boolean;
58
76
  //# sourceMappingURL=inline.d.ts.map
package/dist/inline.js CHANGED
@@ -29,6 +29,17 @@ exports.hasInlineRules = hasInlineRules;
29
29
  * embedded quotes, they can move to spec mode.
30
30
  */
31
31
  const ENFORCE_RE = /<!--\s*vigiles:enforce\s+([@A-Za-z0-9_/:.-]+)\s+"([^"\n]*)"\s*-->/;
32
+ /**
33
+ * Match `<!-- vigiles:file <path> -->`. The path is a single whitespace-free
34
+ * token (project-relative); spaces in paths are vanishingly rare and would be
35
+ * ambiguous against the closing `-->`.
36
+ */
37
+ const FILE_RE = /<!--\s*vigiles:file\s+(\S+)\s*-->/;
38
+ /**
39
+ * Match `<!-- vigiles:cmd "<command>" -->`. The command is quoted because
40
+ * commands contain spaces (e.g. `npm run build`).
41
+ */
42
+ const CMD_RE = /<!--\s*vigiles:cmd\s+"([^"\n]*)"\s*-->/;
32
43
  /**
33
44
  * Detects any `<!-- vigiles:<kind> -->` comment (valid or not) so we can
34
45
  * surface errors for typos and reserved-but-unrecognized kinds. Uses a
@@ -36,6 +47,16 @@ const ENFORCE_RE = /<!--\s*vigiles:enforce\s+([@A-Za-z0-9_/:.-]+)\s+"([^"\n]*)"\
36
47
  * circuit the pattern.
37
48
  */
38
49
  const MARKER_RE = /<!--\s*vigiles:([A-Za-z_-]+)[^]*?-->/;
50
+ // vigiles markers handled by other subsystems (skill runtime gates, opt-outs).
51
+ // The inline *rule* parser skips them rather than flagging them as unknown.
52
+ const KNOWN_NON_RULE_MARKERS = new Set([
53
+ "disable",
54
+ "ignore",
55
+ "ignore-file",
56
+ "gate", // skill step gate (src/skill-runtime.ts)
57
+ "result", // skill result gate
58
+ "symbol", // symbol reference mark (src/refs.ts)
59
+ ]);
39
60
  /**
40
61
  * Parse inline vigiles rules out of a markdown file's contents.
41
62
  * Does not touch the filesystem and does not verify the rules against
@@ -48,6 +69,8 @@ const MARKER_RE = /<!--\s*vigiles:([A-Za-z_-]+)[^]*?-->/;
48
69
  */
49
70
  function parseInlineRules(content) {
50
71
  const rules = [];
72
+ const files = [];
73
+ const commands = [];
51
74
  const errors = [];
52
75
  const lines = content.split("\n");
53
76
  let fenceChar = null;
@@ -97,15 +120,24 @@ function parseInlineRules(content) {
97
120
  });
98
121
  continue;
99
122
  }
123
+ const fileMatch = FILE_RE.exec(scannable);
124
+ if (fileMatch) {
125
+ files.push({ path: fileMatch[1], line: i + 1 });
126
+ continue;
127
+ }
128
+ const cmdMatch = CMD_RE.exec(scannable);
129
+ if (cmdMatch) {
130
+ commands.push({ command: cmdMatch[1], line: i + 1 });
131
+ continue;
132
+ }
100
133
  // Skip the compiled-file hash header (`<!-- vigiles:sha256:... -->`)
101
134
  // entirely — it's not a rule marker and should not be reported.
102
135
  if (/<!--\s*vigiles:sha\d+:/.test(scannable))
103
136
  continue;
104
137
  const markerMatch = MARKER_RE.exec(scannable);
105
138
  if (markerMatch) {
106
- // Looks like a vigiles marker but didn't parse as enforce
107
- // surface it so users catch typos like "vigile:enforce" or
108
- // unquoted why.
139
+ // Looks like a vigiles marker but didn't parse surface it so users
140
+ // catch typos like "vigile:enforce" or an unquoted why/command.
109
141
  const kind = markerMatch[1];
110
142
  if (kind === "enforce") {
111
143
  errors.push({
@@ -114,29 +146,44 @@ function parseInlineRules(content) {
114
146
  raw: line.trim(),
115
147
  });
116
148
  }
117
- else if (kind !== "disable" && kind !== "ignore") {
118
- // `vigiles:disable ...` / `vigiles:ignore ...` are reserved for
119
- // future disable-comment support; don't complain about them.
149
+ else if (kind === "file") {
150
+ errors.push({
151
+ line: i + 1,
152
+ message: "Malformed vigiles:file — expected `<!-- vigiles:file <path> -->`",
153
+ raw: line.trim(),
154
+ });
155
+ }
156
+ else if (kind === "cmd") {
157
+ errors.push({
158
+ line: i + 1,
159
+ message: 'Malformed vigiles:cmd — expected `<!-- vigiles:cmd "<command>" -->`',
160
+ raw: line.trim(),
161
+ });
162
+ }
163
+ else if (!KNOWN_NON_RULE_MARKERS.has(kind)) {
120
164
  errors.push({
121
165
  line: i + 1,
122
- message: `Unknown vigiles marker "${kind}". Only \`vigiles:enforce\` is supported.`,
166
+ message: `Unknown vigiles marker "${kind}". Only \`vigiles:enforce\`, \`vigiles:file\`, and \`vigiles:cmd\` are supported.`,
123
167
  raw: line.trim(),
124
168
  });
125
169
  }
126
170
  }
127
171
  }
128
- return { rules, errors };
172
+ return { rules, files, commands, errors };
129
173
  }
130
174
  /**
131
- * True if the content contains at least one parseable vigiles:enforce
132
- * rule (ignoring fenced code blocks and malformed markers). Used by
133
- * `require-spec` validation to treat inline mode as spec-equivalent.
175
+ * True if the content contains at least one parseable vigiles inline marker —
176
+ * an `enforce` rule, a `file` reference, or a `cmd` reference (ignoring fenced
177
+ * code blocks and malformed markers). Used by `require-spec` validation to
178
+ * treat inline mode as spec-equivalent: a file that pins even a single path is
179
+ * meaningfully managed.
134
180
  *
135
181
  * Deliberately delegates to `parseInlineRules` so a loose prefix regex
136
182
  * can't satisfy require-spec with a malformed marker that produces no
137
- * real enforceable rule.
183
+ * real reference.
138
184
  */
139
185
  function hasInlineRules(content) {
140
- return parseInlineRules(content).rules.length > 0;
186
+ const r = parseInlineRules(content);
187
+ return r.rules.length + r.files.length + r.commands.length > 0;
141
188
  }
142
189
  //# sourceMappingURL=inline.js.map
package/dist/linters.js CHANGED
@@ -20,6 +20,34 @@ const node_path_1 = require("node:path");
20
20
  const node_child_process_1 = require("node:child_process");
21
21
  const node_module_1 = require("node:module");
22
22
  const glob_1 = require("glob");
23
+ /**
24
+ * Prepend version-manager shim directories (rbenv / asdf / rvm) to PATH so
25
+ * gem/pip-installed linters are found in non-login shells and CI, where the
26
+ * shims dir is often missing from PATH even though the tool is installed.
27
+ * Runs once; only adds directories that exist and aren't already present.
28
+ */
29
+ function augmentToolPath() {
30
+ const home = process.env.HOME ?? "";
31
+ // rbenv/asdf shims don't resolve without a selected version, so add the
32
+ // concrete per-version `bin` dirs (where the gem executables actually live).
33
+ const candidates = [
34
+ ...(0, glob_1.globSync)("/opt/rbenv/versions/*/bin", { nodir: false }),
35
+ ...(home
36
+ ? (0, glob_1.globSync)(`${home}/.rbenv/versions/*/bin`, { nodir: false })
37
+ : []),
38
+ ...(home
39
+ ? (0, glob_1.globSync)(`${home}/.asdf/installs/*/*/bin`, { nodir: false })
40
+ : []),
41
+ `${home}/.rvm/bin`,
42
+ `${home}/.local/bin`,
43
+ ];
44
+ const current = (process.env.PATH ?? "").split(":");
45
+ const additions = candidates.filter((d) => d && (0, node_fs_1.existsSync)(d) && !current.includes(d));
46
+ if (additions.length > 0) {
47
+ process.env.PATH = [...current, ...additions].join(":");
48
+ }
49
+ }
50
+ augmentToolPath();
23
51
  // ---------------------------------------------------------------------------
24
52
  // Parsing enforcement references
25
53
  // ---------------------------------------------------------------------------
@@ -0,0 +1,31 @@
1
+ /** One scripted assistant turn: a final text answer, or a tool call. */
2
+ export interface ModelTurn {
3
+ /** Final text answer (stops the turn). */
4
+ readonly text?: string;
5
+ /** A tool to invoke, e.g. "Bash" | "Write" | "Edit". */
6
+ readonly tool?: string;
7
+ /** The tool input, e.g. `{ file_path, content }` or `{ command }`. */
8
+ readonly input?: Record<string, unknown>;
9
+ }
10
+ /** Build a scripted model from an ordered list of turns. */
11
+ export declare function scriptModel(turns: readonly ModelTurn[]): ModelTurn[];
12
+ export interface TurnInfo {
13
+ readonly n: number;
14
+ readonly stream: boolean;
15
+ readonly hasToolResult: boolean;
16
+ }
17
+ export interface MockHandle {
18
+ readonly url: string;
19
+ close(): void;
20
+ /** Number of model turns served so far. */
21
+ readonly count: number;
22
+ }
23
+ /**
24
+ * Start the scripted mock on a free port. Each `/v1/messages` POST consumes the
25
+ * next turn (the last turn repeats if the client asks for more). Resolves to a
26
+ * handle with the base `url` and a `close()`.
27
+ */
28
+ export declare function startMock(script: readonly ModelTurn[], opts?: {
29
+ onTurn?: (info: TurnInfo) => void;
30
+ }): Promise<MockHandle>;
31
+ //# sourceMappingURL=mock-model.d.ts.map