vigiles 33.1.0 → 33.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/adapter-conformance.js +14 -10
- package/dist/adapter.d.ts +3 -1
- package/dist/adapters/claude-code/layout.js +3 -0
- package/dist/adapters/claude-code/output-style.d.ts +3 -0
- package/dist/adapters/claude-code/output-style.js +81 -0
- package/dist/adapters/claude-code/replies.d.ts +14 -0
- package/dist/adapters/claude-code/replies.js +78 -0
- package/dist/adapters/claude-code/runtime.js +70 -0
- package/dist/adapters/claude-code/skill-reachability.d.ts +42 -21
- package/dist/adapters/claude-code/skill-reachability.js +91 -28
- package/dist/audit-report.d.ts +5 -0
- package/dist/audit-report.js +29 -20
- package/dist/audit-report.template.html +25 -25
- package/dist/audit-score.js +2 -2
- package/dist/check.d.ts +20 -0
- package/dist/check.js +87 -2
- package/dist/cli-main.js +249 -30
- package/dist/core/adapter.d.ts +7 -0
- package/dist/core/config-schema.d.ts +9 -0
- package/dist/core/config-schema.js +1 -0
- package/dist/core/eval-driver.d.ts +10 -0
- package/dist/core/frontmatter-read.d.ts +31 -0
- package/dist/core/frontmatter-read.js +4 -2
- package/dist/core/harness-driver.d.ts +2 -0
- package/dist/core/install-reader.js +15 -0
- package/dist/core/layout.d.ts +17 -0
- package/dist/core/layout.js +17 -0
- package/dist/core/output-style.d.ts +84 -0
- package/dist/core/output-style.js +90 -0
- package/dist/core/rule-meta.d.ts +1 -1
- package/dist/core/rule-meta.js +8 -0
- package/dist/core/run-env.d.ts +50 -0
- package/dist/core/run-env.js +51 -0
- package/dist/core/runtime.d.ts +44 -0
- package/dist/core/skill-link-target.d.ts +20 -0
- package/dist/core/skill-link-target.js +33 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/coverage-evidence.d.ts +42 -0
- package/dist/coverage-evidence.js +39 -0
- package/dist/eval.d.ts +65 -42
- package/dist/eval.js +116 -120
- package/dist/harness-test.d.ts +65 -1
- package/dist/harness-test.js +114 -22
- package/dist/optimize.js +1 -5
- package/dist/output-style-arms.d.ts +27 -0
- package/dist/output-style-arms.js +51 -0
- package/dist/plugin-declaration.d.ts +6 -6
- package/dist/plugin-declaration.js +6 -6
- package/dist/scan-files.js +1 -1
- package/dist/scan.d.ts +6 -0
- package/dist/scan.js +14 -4
- package/dist/score-core.d.ts +3 -0
- package/dist/score-core.js +5 -1
- package/dist/setup-plan.d.ts +50 -10
- package/dist/setup-plan.js +103 -68
- package/dist/skill-links.d.ts +183 -0
- package/dist/skill-links.js +255 -0
- package/dist/test-coverage-files.d.ts +3 -1
- package/dist/test-coverage-files.js +20 -6
- package/dist/test-coverage.d.ts +18 -1
- package/dist/test-coverage.js +101 -15
- package/dist/test.d.ts +3 -1
- package/dist/test.js +8 -2
- package/dist/vigilesrc.schema.json +134 -0
- package/package.json +1 -1
|
@@ -130,6 +130,8 @@ export interface ParsedRun {
|
|
|
130
130
|
readonly hooks: readonly HookFire[];
|
|
131
131
|
/** The agent's final answer text, or "". */
|
|
132
132
|
readonly output: string;
|
|
133
|
+
/** Every reply the agent ended a turn with; absent when the output is not a stream. */
|
|
134
|
+
readonly replies?: readonly string[];
|
|
133
135
|
}
|
|
134
136
|
/**
|
|
135
137
|
* What a harness brings so `runHarnessTest` can drive it. Implemented in each
|
|
@@ -30,6 +30,18 @@ function readOrNull(absolute) {
|
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
/** A symlink's stored target, or null when it is not a link. Never throws. */
|
|
34
|
+
function linkOrNull(absolute) {
|
|
35
|
+
try {
|
|
36
|
+
return (0, node_fs_1.lstatSync)(absolute, { throwIfNoEntry: false })?.isSymbolicLink() ===
|
|
37
|
+
true
|
|
38
|
+
? (0, node_fs_1.readlinkSync)(absolute)
|
|
39
|
+
: null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
33
45
|
/** Directory names directly under `dir`, or [] when it is not readable. */
|
|
34
46
|
function dirNames(dir) {
|
|
35
47
|
try {
|
|
@@ -80,6 +92,9 @@ function buildInstallReader(adapter, root, opts = {}) {
|
|
|
80
92
|
repo: (repoRelative) => adapter.claims(repoRelative)
|
|
81
93
|
? readOrNull((0, node_path_1.resolve)(root, repoRelative))
|
|
82
94
|
: null,
|
|
95
|
+
repoLink: (repoRelative) => adapter.claims(repoRelative)
|
|
96
|
+
? linkOrNull((0, node_path_1.resolve)(root, repoRelative))
|
|
97
|
+
: null,
|
|
83
98
|
home: (homeRelative) => readOrNull((0, node_path_1.join)(home, homeRelative)),
|
|
84
99
|
repoDependsOnVigiles: pkgJson !== null && declaresVigilesDependency(pkgJson),
|
|
85
100
|
vendoredSkillNames: dirNames((0, node_path_1.join)(root, "node_modules", "vigiles", "skills")),
|
package/dist/core/layout.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Paths are repo-relative (POSIX-style, `join`-friendly). The Claude Code
|
|
11
11
|
* implementation is `claudeCodeLayout` in `src/adapters/claude-code/layout.ts`.
|
|
12
12
|
*/
|
|
13
|
+
import type { OutputStyleRules } from "./output-style.js";
|
|
13
14
|
import type { InstructionChain } from "./instruction-chain.js";
|
|
14
15
|
import type { SettingsCodec } from "./settings-codec.js";
|
|
15
16
|
/**
|
|
@@ -127,6 +128,12 @@ export interface PluginLayout {
|
|
|
127
128
|
* docblock there for the count behind that.
|
|
128
129
|
*/
|
|
129
130
|
readonly rulesDir?: string;
|
|
131
|
+
/**
|
|
132
|
+
* Where this harness keeps output styles and how it switches one on (absent =
|
|
133
|
+
* this harness has none). Not a {@link SurfaceKind}: like a rules file, a
|
|
134
|
+
* style is read into the session, not invoked. See `output-style.ts`.
|
|
135
|
+
*/
|
|
136
|
+
readonly outputStyles?: OutputStyleRules;
|
|
130
137
|
/**
|
|
131
138
|
* The WORD a per-machine settings sibling inserts before the extension, e.g.
|
|
132
139
|
* `local` → `.claude/settings.local.json` (absent = this harness has no
|
|
@@ -234,6 +241,16 @@ export declare function executableSourceDirs(layout: PluginLayout): readonly str
|
|
|
234
241
|
* written a layout where they differed.
|
|
235
242
|
*/
|
|
236
243
|
export declare function materializePrefix(layout: PluginLayout): string;
|
|
244
|
+
/**
|
|
245
|
+
* Where a REPOSITORY keeps its skills for this harness, repo-relative — the
|
|
246
|
+
* skill surface under {@link materializePrefix}: `.claude/skills` on Claude
|
|
247
|
+
* Code, `.agents/skills` on Codex. `null` when the layout has no skill surface.
|
|
248
|
+
*
|
|
249
|
+
* `vigiles init` links the package's shipped skills here, so it must be the
|
|
250
|
+
* directory the harness actually scans for a project; deriving it from the two
|
|
251
|
+
* fields that already say so keeps a third spelling from appearing.
|
|
252
|
+
*/
|
|
253
|
+
export declare function skillsHome(layout: Pick<PluginLayout, "surfaces" | "userSurfaceRoot">): string | null;
|
|
237
254
|
/**
|
|
238
255
|
* How DEEP a harness reads its {@link PluginLayout.agentDir} — the one statement
|
|
239
256
|
* of that rule, as a RegExp source fragment matching the part of a path AFTER
|
package/dist/core/layout.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.RULE_FILE_LEAF_RE = exports.AGENT_FILE_LEAF_RE = exports.SURFACE_KINDS =
|
|
|
4
4
|
exports.surfaceDirs = surfaceDirs;
|
|
5
5
|
exports.executableSourceDirs = executableSourceDirs;
|
|
6
6
|
exports.materializePrefix = materializePrefix;
|
|
7
|
+
exports.skillsHome = skillsHome;
|
|
7
8
|
exports.rulesHome = rulesHome;
|
|
8
9
|
exports.ruleFileRe = ruleFileRe;
|
|
9
10
|
exports.agentSurfaceName = agentSurfaceName;
|
|
@@ -46,6 +47,22 @@ function executableSourceDirs(layout) {
|
|
|
46
47
|
function materializePrefix(layout) {
|
|
47
48
|
return layout.userSurfaceRoot ?? "";
|
|
48
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Where a REPOSITORY keeps its skills for this harness, repo-relative — the
|
|
52
|
+
* skill surface under {@link materializePrefix}: `.claude/skills` on Claude
|
|
53
|
+
* Code, `.agents/skills` on Codex. `null` when the layout has no skill surface.
|
|
54
|
+
*
|
|
55
|
+
* `vigiles init` links the package's shipped skills here, so it must be the
|
|
56
|
+
* directory the harness actually scans for a project; deriving it from the two
|
|
57
|
+
* fields that already say so keeps a third spelling from appearing.
|
|
58
|
+
*/
|
|
59
|
+
function skillsHome(layout) {
|
|
60
|
+
const skill = layout.surfaces.skill;
|
|
61
|
+
if (skill === undefined || skill === "")
|
|
62
|
+
return null;
|
|
63
|
+
const root = layout.userSurfaceRoot;
|
|
64
|
+
return root === undefined || root === "" ? skill : `${root}/${skill}`;
|
|
65
|
+
}
|
|
49
66
|
/**
|
|
50
67
|
* How DEEP a harness reads its {@link PluginLayout.agentDir} — the one statement
|
|
51
68
|
* of that rule, as a RegExp source fragment matching the part of a path AFTER
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output styles: files that set how the agent talks for a whole session,
|
|
3
|
+
* chosen by a name in the harness settings.
|
|
4
|
+
*
|
|
5
|
+
* The core knows only that idea. Where the files live, how a name is read from
|
|
6
|
+
* one, which setting selects it and how a run shows that it arrived are each
|
|
7
|
+
* harness's own rules, given by its layout as `outputStyles`. A layout without
|
|
8
|
+
* them is a harness that has no output styles.
|
|
9
|
+
*/
|
|
10
|
+
import type { ModelRequest } from "./harness-driver.js";
|
|
11
|
+
import type { PluginLayout } from "./layout.js";
|
|
12
|
+
/** One style, read by its harness's rules. */
|
|
13
|
+
export interface OutputStyle {
|
|
14
|
+
/**
|
|
15
|
+
* Relative to the directory the caller scanned (a bundle), `/`-separated,
|
|
16
|
+
* exactly as the caller's file map keyed it. A plain string by the same
|
|
17
|
+
* contract as every scan-core path; `Bundle.scanned` in `frame.ts` gives it
|
|
18
|
+
* a frame. Not the `BundlePath` brand: that type is internal and must not
|
|
19
|
+
* reach this public interface.
|
|
20
|
+
*/
|
|
21
|
+
readonly path: string;
|
|
22
|
+
/** The name a setting must spell, or null when the harness cannot tell. */
|
|
23
|
+
readonly name: string | null;
|
|
24
|
+
/** The text the harness delivers, frontmatter removed. */
|
|
25
|
+
readonly body: string;
|
|
26
|
+
}
|
|
27
|
+
/** How one harness keeps output styles and switches one on. */
|
|
28
|
+
export interface OutputStyleRules {
|
|
29
|
+
/** The folder of style files, e.g. `output-styles`. */
|
|
30
|
+
readonly dir: string;
|
|
31
|
+
/** Whether a path inside that folder is a file the harness loads. */
|
|
32
|
+
readonly isStyleFile: (pathInDir: string) => boolean;
|
|
33
|
+
/** Reads one style file as this harness does. */
|
|
34
|
+
readonly read: (path: string, text: string) => OutputStyle;
|
|
35
|
+
/** The settings that switch the named style on. */
|
|
36
|
+
readonly select: (name: string) => Readonly<Record<string, unknown>>;
|
|
37
|
+
/**
|
|
38
|
+
* The settings that switch every style off, so a run without the style does
|
|
39
|
+
* not pick up one the user turned on for all their projects.
|
|
40
|
+
*/
|
|
41
|
+
readonly selectNone: Readonly<Record<string, unknown>>;
|
|
42
|
+
/** Whether one model request carries the style. */
|
|
43
|
+
readonly reached: (style: OutputStyle, request: ModelRequest) => boolean;
|
|
44
|
+
}
|
|
45
|
+
/** What a repository holds, or that its harness has no such thing. */
|
|
46
|
+
export type OutputStyleScan = {
|
|
47
|
+
readonly kind: "not-supported";
|
|
48
|
+
} | {
|
|
49
|
+
readonly kind: "found";
|
|
50
|
+
readonly styles: readonly OutputStyle[];
|
|
51
|
+
};
|
|
52
|
+
type StyleLayout = Pick<PluginLayout, "outputStyles" | "userSurfaceRoot">;
|
|
53
|
+
/** The folders a harness reads styles from: a plugin's root and the user surface root. */
|
|
54
|
+
export declare function outputStyleHomes(layout: StyleLayout): readonly string[];
|
|
55
|
+
/** Every style among `files` (repository path → text). Pure: the caller reads the disk. */
|
|
56
|
+
export declare function findOutputStyles(layout: StyleLayout, files: ReadonlyMap<string, string>): OutputStyleScan;
|
|
57
|
+
/** A run with one style switched on, or why it cannot be set up. */
|
|
58
|
+
export type StyleRunPlan = {
|
|
59
|
+
readonly kind: "refused";
|
|
60
|
+
readonly reason: string;
|
|
61
|
+
} | {
|
|
62
|
+
readonly kind: "planned";
|
|
63
|
+
/** The style as the harness will read it, at its place in the work dir. */
|
|
64
|
+
readonly style: OutputStyle;
|
|
65
|
+
readonly files: Readonly<Record<string, string>>;
|
|
66
|
+
readonly settings: Readonly<Record<string, unknown>>;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Put one style file into a test's fixture and switch it on BY THE NAME THE
|
|
70
|
+
* HARNESS READS from it — never a name the caller types. Pure: the caller reads
|
|
71
|
+
* the file. Refuses instead of guessing when the harness has no styles, would
|
|
72
|
+
* not load such a file, cannot tell its name, or the fixture already decides.
|
|
73
|
+
*/
|
|
74
|
+
export declare function planStyleRun(layout: StyleLayout, source: {
|
|
75
|
+
readonly path: string;
|
|
76
|
+
readonly text: string;
|
|
77
|
+
}, fixture: {
|
|
78
|
+
readonly files: Readonly<Record<string, string>>;
|
|
79
|
+
readonly settings: unknown;
|
|
80
|
+
}): StyleRunPlan;
|
|
81
|
+
/** Whether any request of a run carried the style, by its harness's own rule. */
|
|
82
|
+
export declare function styleReached(rules: OutputStyleRules, style: OutputStyle, requests: readonly ModelRequest[]): boolean;
|
|
83
|
+
export {};
|
|
84
|
+
//# sourceMappingURL=output-style.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.outputStyleHomes = outputStyleHomes;
|
|
4
|
+
exports.findOutputStyles = findOutputStyles;
|
|
5
|
+
exports.planStyleRun = planStyleRun;
|
|
6
|
+
exports.styleReached = styleReached;
|
|
7
|
+
/** The folders a harness reads styles from: a plugin's root and the user surface root. */
|
|
8
|
+
function outputStyleHomes(layout) {
|
|
9
|
+
const dir = layout.outputStyles?.dir;
|
|
10
|
+
if (dir === undefined)
|
|
11
|
+
return [];
|
|
12
|
+
const root = layout.userSurfaceRoot;
|
|
13
|
+
return root === undefined || root === "" ? [dir] : [dir, `${root}/${dir}`];
|
|
14
|
+
}
|
|
15
|
+
const pathInside = (home, path) => path.startsWith(`${home}/`) ? path.slice(home.length + 1) : null;
|
|
16
|
+
/** Every style among `files` (repository path → text). Pure: the caller reads the disk. */
|
|
17
|
+
function findOutputStyles(layout, files) {
|
|
18
|
+
const rules = layout.outputStyles;
|
|
19
|
+
if (rules === undefined)
|
|
20
|
+
return { kind: "not-supported" };
|
|
21
|
+
const homes = outputStyleHomes(layout);
|
|
22
|
+
const isStyle = (path) => homes.some((home) => {
|
|
23
|
+
const rest = pathInside(home, path);
|
|
24
|
+
return rest !== null && rules.isStyleFile(rest);
|
|
25
|
+
});
|
|
26
|
+
const styles = [...files]
|
|
27
|
+
.filter(([path]) => isStyle(path))
|
|
28
|
+
.map(([path, text]) => rules.read(path, text));
|
|
29
|
+
return {
|
|
30
|
+
kind: "found",
|
|
31
|
+
styles: [...styles].sort((a, b) => a.path.localeCompare(b.path)),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
35
|
+
/** Where the style goes and what it is called there, or why it cannot go. */
|
|
36
|
+
function placeStyle(layout, source) {
|
|
37
|
+
const rules = layout.outputStyles;
|
|
38
|
+
const home = outputStyleHomes(layout).at(-1);
|
|
39
|
+
if (rules === undefined || home === undefined)
|
|
40
|
+
return "this harness has no output styles";
|
|
41
|
+
// Either separator: a Windows path from `path.resolve` uses backslashes.
|
|
42
|
+
const file = source.path.split(/[\\/]/).at(-1) ?? source.path;
|
|
43
|
+
if (!rules.isStyleFile(file))
|
|
44
|
+
return `this harness would not load "${file}" as an output style`;
|
|
45
|
+
const style = rules.read(`${home}/${file}`, source.text);
|
|
46
|
+
return style.name === null
|
|
47
|
+
? `cannot tell which name selects "${source.path}"`
|
|
48
|
+
: style;
|
|
49
|
+
}
|
|
50
|
+
/** The fixture's settings with the selection added, or why they clash. */
|
|
51
|
+
function selectIn(settings, selection) {
|
|
52
|
+
const base = settings ?? {};
|
|
53
|
+
if (!isPlainObject(base))
|
|
54
|
+
return "the fixture's settings are not an object";
|
|
55
|
+
const clash = Object.keys(selection).find((key) => key in base);
|
|
56
|
+
return clash === undefined
|
|
57
|
+
? { ...base, ...selection }
|
|
58
|
+
: `the fixture's settings already set "${clash}"; the style option sets it`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Put one style file into a test's fixture and switch it on BY THE NAME THE
|
|
62
|
+
* HARNESS READS from it — never a name the caller types. Pure: the caller reads
|
|
63
|
+
* the file. Refuses instead of guessing when the harness has no styles, would
|
|
64
|
+
* not load such a file, cannot tell its name, or the fixture already decides.
|
|
65
|
+
*/
|
|
66
|
+
function planStyleRun(layout, source, fixture) {
|
|
67
|
+
const style = placeStyle(layout, source);
|
|
68
|
+
if (typeof style === "string")
|
|
69
|
+
return { kind: "refused", reason: style };
|
|
70
|
+
if (style.path in fixture.files)
|
|
71
|
+
return {
|
|
72
|
+
kind: "refused",
|
|
73
|
+
reason: `the fixture already has a file at ${style.path}`,
|
|
74
|
+
};
|
|
75
|
+
const selection = layout.outputStyles?.select(style.name ?? "") ?? {};
|
|
76
|
+
const settings = selectIn(fixture.settings, selection);
|
|
77
|
+
return typeof settings === "string"
|
|
78
|
+
? { kind: "refused", reason: settings }
|
|
79
|
+
: {
|
|
80
|
+
kind: "planned",
|
|
81
|
+
style,
|
|
82
|
+
files: { ...fixture.files, [style.path]: source.text },
|
|
83
|
+
settings,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Whether any request of a run carried the style, by its harness's own rule. */
|
|
87
|
+
function styleReached(rules, style, requests) {
|
|
88
|
+
return requests.some((request) => rules.reached(style, request));
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=output-style.js.map
|
package/dist/core/rule-meta.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ import type { RulesConfig } from "./types.js";
|
|
|
37
37
|
*/
|
|
38
38
|
export type RuleBucket = "structural-closed" | "external-decidable" | "heuristic-behavioral";
|
|
39
39
|
/** The artifact surface a rule reads (coarse; a rule may span several). */
|
|
40
|
-
export type RuleSurface = "instruction" | "skill" | "subagent" | "hook" | "mcp" | "plugin" | "docs";
|
|
40
|
+
export type RuleSurface = "instruction" | "skill" | "subagent" | "hook" | "output-style" | "mcp" | "plugin" | "docs";
|
|
41
41
|
/** Where a rule sits by default — `"off"` is the normalized form of `false`. */
|
|
42
42
|
export type RuleDefaultSeverity = "error" | "warn" | "off";
|
|
43
43
|
/** Every named rule: the `RulesConfig` keys plus the built-in `orphan-docs`. */
|
package/dist/core/rule-meta.js
CHANGED
|
@@ -69,6 +69,14 @@ exports.RULE_META = {
|
|
|
69
69
|
summary: "A file-backed hook script ships with a test or eval.",
|
|
70
70
|
detector: "findUntestedSurfaces",
|
|
71
71
|
},
|
|
72
|
+
"untested-output-style": {
|
|
73
|
+
id: "untested-output-style",
|
|
74
|
+
bucket: "external-decidable",
|
|
75
|
+
surface: ["output-style"],
|
|
76
|
+
defaultSeverity: "warn",
|
|
77
|
+
summary: "An output style ships with a test or eval.",
|
|
78
|
+
detector: "findUntestedSurfaces",
|
|
79
|
+
},
|
|
72
80
|
// --- Reference marking ----------------------------------------------------
|
|
73
81
|
"unmarked-refs": {
|
|
74
82
|
id: "unmarked-refs",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment a child agent run inherits — the one place the runners turn
|
|
3
|
+
* a caller's `process.env` into a spawn env.
|
|
4
|
+
*
|
|
5
|
+
* Two shapes, both driven by the harness's declared {@link RunEnvPolicy}:
|
|
6
|
+
*
|
|
7
|
+
* - SCRUBBED (`scrubbedRunEnv`): a throwaway HOME/TMPDIR, the OS essentials, the
|
|
8
|
+
* harness's declared auth, and the caller's own named extras. Everything else
|
|
9
|
+
* is dropped. The eval tier's `ephemeralEnv` and the harness-tier preflight of
|
|
10
|
+
* `experimental_outputStyleArms` use it.
|
|
11
|
+
* - INHERITED (`withoutSessionIdentity`): the caller's env as-is, minus the
|
|
12
|
+
* harness's declared session identity. The default spawn of both tiers.
|
|
13
|
+
*
|
|
14
|
+
* In both, the parent session's identity never reaches the child. The core
|
|
15
|
+
* names no harness: which variables are auth and which are identity is the
|
|
16
|
+
* adapter's declaration.
|
|
17
|
+
*
|
|
18
|
+
* Pure — no fs, no spawn.
|
|
19
|
+
*/
|
|
20
|
+
import type { RunEnvPolicy } from "./runtime.js";
|
|
21
|
+
type EnvLike = Readonly<Record<string, string | undefined>>;
|
|
22
|
+
/**
|
|
23
|
+
* What ANY spawned process needs to function, whatever the harness: resolve
|
|
24
|
+
* binaries (`PATH`) and keep the locale and terminal (`LANG`, `LC_*`, `TERM`).
|
|
25
|
+
* Mirrors what `bwrapArgs` sets back after `--clearenv` in `src/sandbox.ts`.
|
|
26
|
+
*/
|
|
27
|
+
export declare const OS_ESSENTIAL_ENV: readonly string[];
|
|
28
|
+
/**
|
|
29
|
+
* Build a SCRUBBED run environment: a NEW env with `HOME`/`TMPDIR` set to the
|
|
30
|
+
* throwaway `opts.home`, the {@link OS_ESSENTIAL_ENV}, the harness's
|
|
31
|
+
* `policy.keep`, and the caller's `opts.allow`, taken from `base`. Everything
|
|
32
|
+
* else is dropped, and `policy.sessionIdentity` is dropped even when listed in
|
|
33
|
+
* `keep` or `allow`.
|
|
34
|
+
*
|
|
35
|
+
* Without a policy only the essentials pass: right for a run against a scripted
|
|
36
|
+
* mock, which needs no auth.
|
|
37
|
+
*/
|
|
38
|
+
export declare function scrubbedRunEnv(base: EnvLike, opts: {
|
|
39
|
+
readonly home: string;
|
|
40
|
+
readonly policy?: RunEnvPolicy;
|
|
41
|
+
readonly allow?: readonly string[];
|
|
42
|
+
}): Readonly<Record<string, string>>;
|
|
43
|
+
/**
|
|
44
|
+
* The caller's env with the harness's declared session identity removed — the
|
|
45
|
+
* INHERITED shape. Unset (`undefined`) values are dropped too, so the result is
|
|
46
|
+
* a plain spawn env. Without a policy nothing is removed.
|
|
47
|
+
*/
|
|
48
|
+
export declare function withoutSessionIdentity(base: EnvLike, policy?: RunEnvPolicy): Readonly<Record<string, string>>;
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=run-env.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OS_ESSENTIAL_ENV = void 0;
|
|
4
|
+
exports.scrubbedRunEnv = scrubbedRunEnv;
|
|
5
|
+
exports.withoutSessionIdentity = withoutSessionIdentity;
|
|
6
|
+
/**
|
|
7
|
+
* What ANY spawned process needs to function, whatever the harness: resolve
|
|
8
|
+
* binaries (`PATH`) and keep the locale and terminal (`LANG`, `LC_*`, `TERM`).
|
|
9
|
+
* Mirrors what `bwrapArgs` sets back after `--clearenv` in `src/sandbox.ts`.
|
|
10
|
+
*/
|
|
11
|
+
exports.OS_ESSENTIAL_ENV = ["PATH", "LANG", "TERM"];
|
|
12
|
+
const OS_ESSENTIAL_PREFIXES = ["LC_"];
|
|
13
|
+
/**
|
|
14
|
+
* Build a SCRUBBED run environment: a NEW env with `HOME`/`TMPDIR` set to the
|
|
15
|
+
* throwaway `opts.home`, the {@link OS_ESSENTIAL_ENV}, the harness's
|
|
16
|
+
* `policy.keep`, and the caller's `opts.allow`, taken from `base`. Everything
|
|
17
|
+
* else is dropped, and `policy.sessionIdentity` is dropped even when listed in
|
|
18
|
+
* `keep` or `allow`.
|
|
19
|
+
*
|
|
20
|
+
* Without a policy only the essentials pass: right for a run against a scripted
|
|
21
|
+
* mock, which needs no auth.
|
|
22
|
+
*/
|
|
23
|
+
function scrubbedRunEnv(base, opts) {
|
|
24
|
+
const identity = new Set(opts.policy?.sessionIdentity ?? []);
|
|
25
|
+
const exact = new Set([
|
|
26
|
+
...exports.OS_ESSENTIAL_ENV,
|
|
27
|
+
...(opts.policy?.keep ?? []),
|
|
28
|
+
...(opts.allow ?? []),
|
|
29
|
+
]);
|
|
30
|
+
const passes = (k) => !identity.has(k) &&
|
|
31
|
+
(exact.has(k) || OS_ESSENTIAL_PREFIXES.some((p) => k.startsWith(p)));
|
|
32
|
+
return {
|
|
33
|
+
...defined(base, passes),
|
|
34
|
+
// Last, so they win over anything passed through.
|
|
35
|
+
HOME: opts.home,
|
|
36
|
+
TMPDIR: opts.home,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The caller's env with the harness's declared session identity removed — the
|
|
41
|
+
* INHERITED shape. Unset (`undefined`) values are dropped too, so the result is
|
|
42
|
+
* a plain spawn env. Without a policy nothing is removed.
|
|
43
|
+
*/
|
|
44
|
+
function withoutSessionIdentity(base, policy) {
|
|
45
|
+
const identity = new Set(policy?.sessionIdentity ?? []);
|
|
46
|
+
return defined(base, (k) => !identity.has(k));
|
|
47
|
+
}
|
|
48
|
+
function defined(base, keep) {
|
|
49
|
+
return Object.fromEntries(Object.entries(base).filter((e) => e[1] !== undefined && keep(e[0])));
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=run-env.js.map
|
package/dist/core/runtime.d.ts
CHANGED
|
@@ -54,5 +54,49 @@ export interface HarnessRuntime {
|
|
|
54
54
|
* caller resolves the raw string via `agentBinary --version`); unit-testable.
|
|
55
55
|
*/
|
|
56
56
|
versionKey(raw: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Which of the caller's environment a child run may inherit — see
|
|
59
|
+
* {@link RunEnvPolicy}. Absent: a scrubbed run inherits no harness variable,
|
|
60
|
+
* and nothing is removed from an inherited env.
|
|
61
|
+
*/
|
|
62
|
+
readonly runEnv?: RunEnvPolicy;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Which of the caller's environment a CHILD run of this harness may inherit.
|
|
66
|
+
*
|
|
67
|
+
* A run spawned from inside a live session of the same harness (an eval started
|
|
68
|
+
* by an agent, a harness test run from a hook) inherits that session's process
|
|
69
|
+
* environment, and a harness marks its own session there. Measured on Claude
|
|
70
|
+
* Code 2.1.292 (2026-10-07): a child `claude -p` started with the parent's
|
|
71
|
+
* session variables ran AS the parent session — a task it created landed in the
|
|
72
|
+
* parent's live task list. Which variables are identity and which are auth is a
|
|
73
|
+
* fact about one harness, so the adapter declares it and the runners apply it
|
|
74
|
+
* (`src/core/run-env.ts`) without spelling a single name.
|
|
75
|
+
*
|
|
76
|
+
* Optional, so an adapter written before this field still type-checks. An
|
|
77
|
+
* adapter without one gets the strict reading: a scrubbed run inherits no
|
|
78
|
+
* harness variable at all, and an inherited one has nothing removed.
|
|
79
|
+
*/
|
|
80
|
+
export interface RunEnvPolicy {
|
|
81
|
+
/**
|
|
82
|
+
* Exact names a SCRUBBED run (fresh HOME, cleared env) inherits: the
|
|
83
|
+
* harness's auth and backend selection, nothing else. An allowlist on purpose —
|
|
84
|
+
* a harness's session variables are open-ended (the Claude Code 2.1.292
|
|
85
|
+
* binary references several dozen `*SESSION*` / `*SOCKET*` / `*TOKEN*` names),
|
|
86
|
+
* so a prefix with a few exceptions would keep passing whichever ones nobody
|
|
87
|
+
* listed.
|
|
88
|
+
*/
|
|
89
|
+
readonly keep: readonly string[];
|
|
90
|
+
/**
|
|
91
|
+
* Auth FILES under HOME (relative paths) that a scrubbed run's throwaway HOME
|
|
92
|
+
* is seeded with, by copy. The file half of `keep`.
|
|
93
|
+
*/
|
|
94
|
+
readonly keepHomeFiles: readonly string[];
|
|
95
|
+
/**
|
|
96
|
+
* The PARENT session's identity: names no child run ever inherits — not a
|
|
97
|
+
* scrubbed run, and not a run that otherwise inherits the whole caller env.
|
|
98
|
+
* Wins over `keep` and over any name a caller allows.
|
|
99
|
+
*/
|
|
100
|
+
readonly sessionIdentity: readonly string[];
|
|
57
101
|
}
|
|
58
102
|
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is a symlink's stored target one that `vigiles init` writes — a path ending
|
|
3
|
+
* in `node_modules/vigiles/skills/<name>`, for the SAME skill name?
|
|
4
|
+
*
|
|
5
|
+
* Two readers ask, and must agree, so the answer lives here once:
|
|
6
|
+
*
|
|
7
|
+
* - `init` (`../skill-links.ts`): an existing link of this shape that points
|
|
8
|
+
* somewhere other than where `init` would link now was made by an earlier
|
|
9
|
+
* `init` (for example before a workspace install hoisted the package), so it
|
|
10
|
+
* is replaced instead of being refused as the user's;
|
|
11
|
+
* - `audit` (`../adapters/claude-code/skill-reachability.ts`): only a dangling
|
|
12
|
+
* link of this shape is "vigiles's link, run npm install". A dangling link
|
|
13
|
+
* that merely shares a skill's NAME is the user's, and the advice would send
|
|
14
|
+
* them to fix the wrong thing.
|
|
15
|
+
*
|
|
16
|
+
* The test is on path SEGMENTS (either separator, so a link written on Windows
|
|
17
|
+
* reads the same), not on a substring: `my-node_modules/vigiles/...` is not it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isVigilesSkillTarget(target: string, name: string): boolean;
|
|
20
|
+
//# sourceMappingURL=skill-link-target.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isVigilesSkillTarget = isVigilesSkillTarget;
|
|
4
|
+
/**
|
|
5
|
+
* Is a symlink's stored target one that `vigiles init` writes — a path ending
|
|
6
|
+
* in `node_modules/vigiles/skills/<name>`, for the SAME skill name?
|
|
7
|
+
*
|
|
8
|
+
* Two readers ask, and must agree, so the answer lives here once:
|
|
9
|
+
*
|
|
10
|
+
* - `init` (`../skill-links.ts`): an existing link of this shape that points
|
|
11
|
+
* somewhere other than where `init` would link now was made by an earlier
|
|
12
|
+
* `init` (for example before a workspace install hoisted the package), so it
|
|
13
|
+
* is replaced instead of being refused as the user's;
|
|
14
|
+
* - `audit` (`../adapters/claude-code/skill-reachability.ts`): only a dangling
|
|
15
|
+
* link of this shape is "vigiles's link, run npm install". A dangling link
|
|
16
|
+
* that merely shares a skill's NAME is the user's, and the advice would send
|
|
17
|
+
* them to fix the wrong thing.
|
|
18
|
+
*
|
|
19
|
+
* The test is on path SEGMENTS (either separator, so a link written on Windows
|
|
20
|
+
* reads the same), not on a substring: `my-node_modules/vigiles/...` is not it.
|
|
21
|
+
*/
|
|
22
|
+
function isVigilesSkillTarget(target, name) {
|
|
23
|
+
const tail = target
|
|
24
|
+
.split(/[\\/]+/)
|
|
25
|
+
.filter((s) => s !== "")
|
|
26
|
+
.slice(-4);
|
|
27
|
+
return (tail.length === 4 &&
|
|
28
|
+
tail[0] === "node_modules" &&
|
|
29
|
+
tail[1] === "vigiles" &&
|
|
30
|
+
tail[2] === "skills" &&
|
|
31
|
+
tail[3] === name);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=skill-link-target.js.map
|
package/dist/core/types.d.ts
CHANGED
|
@@ -167,6 +167,8 @@ export interface RulesConfig {
|
|
|
167
167
|
"untested-subagent"?: RuleWithOptions<TestCoverageConfig>;
|
|
168
168
|
/** Flag a hook script that ships with no test or eval. Default: "warn". */
|
|
169
169
|
"untested-hook"?: RuleWithOptions<TestCoverageConfig>;
|
|
170
|
+
/** Flag an output style that ships with no test or eval. Default: "warn". */
|
|
171
|
+
"untested-output-style"?: RuleWithOptions<TestCoverageConfig>;
|
|
170
172
|
/**
|
|
171
173
|
* Nudge (or block) when an instruction file has code-shaped references that
|
|
172
174
|
* aren't expressed as vigiles marks (so the lint can't verify them), or a
|
|
@@ -197,4 +197,46 @@ export declare function formatEvidence(counts: EvidenceCounts): string;
|
|
|
197
197
|
* (`basename` of the audited dir on disk, the repo name in the browser).
|
|
198
198
|
*/
|
|
199
199
|
export declare function declaredSurfaceName(content: string): string | null;
|
|
200
|
+
/**
|
|
201
|
+
* One output style as a coverage surface — shared by the disk detector and the
|
|
202
|
+
* browser twin, so the two name and exempt a style the same way.
|
|
203
|
+
*
|
|
204
|
+
* Named by FILE (`status-block.md` → `status-block`), not by the name a
|
|
205
|
+
* harness selects it by: the surface name is what a colocated test is spelled
|
|
206
|
+
* after, and a declared name may hold spaces or colons no file name should.
|
|
207
|
+
*/
|
|
208
|
+
export declare function outputStyleSurface(path: string, content: string): {
|
|
209
|
+
readonly kind: "output-style";
|
|
210
|
+
readonly path: string;
|
|
211
|
+
readonly name: string;
|
|
212
|
+
readonly tokens: readonly string[];
|
|
213
|
+
readonly ignored: boolean;
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* How many output styles a scan found, or that the harness has none. Shared by
|
|
217
|
+
* the disk and browser scan engines, so the inventory reads the one discovery
|
|
218
|
+
* pass both already ran. Exempt styles (`vigiles:ignore-test`) are counted: the
|
|
219
|
+
* marker waives the test, not the style, and a style-only repository with an
|
|
220
|
+
* exempt style is not an empty machine.
|
|
221
|
+
*/
|
|
222
|
+
export declare function outputStyleCount(layouts: readonly {
|
|
223
|
+
readonly outputStyles?: unknown;
|
|
224
|
+
}[], tier: {
|
|
225
|
+
readonly covered: readonly {
|
|
226
|
+
readonly kind: string;
|
|
227
|
+
}[];
|
|
228
|
+
readonly untested: readonly {
|
|
229
|
+
readonly kind: string;
|
|
230
|
+
}[];
|
|
231
|
+
}, exempt: readonly {
|
|
232
|
+
readonly kind: string;
|
|
233
|
+
}[]): number | "not-supported";
|
|
234
|
+
/** The inventory counts both scan engines report the same way. */
|
|
235
|
+
export declare function inventoryCounts(files: Readonly<Record<string, string>>, isCommand: (path: string) => boolean, layouts: Parameters<typeof outputStyleCount>[0], coverage: {
|
|
236
|
+
readonly harness: Parameters<typeof outputStyleCount>[1];
|
|
237
|
+
readonly exemptSurfaces: Parameters<typeof outputStyleCount>[2];
|
|
238
|
+
}): {
|
|
239
|
+
readonly commands: number;
|
|
240
|
+
readonly outputStyles: number | "not-supported";
|
|
241
|
+
};
|
|
200
242
|
//# sourceMappingURL=coverage-evidence.d.ts.map
|
|
@@ -12,6 +12,9 @@ exports.matchesSurfaceGlob = matchesSurfaceGlob;
|
|
|
12
12
|
exports.countEvidence = countEvidence;
|
|
13
13
|
exports.formatEvidence = formatEvidence;
|
|
14
14
|
exports.declaredSurfaceName = declaredSurfaceName;
|
|
15
|
+
exports.outputStyleSurface = outputStyleSurface;
|
|
16
|
+
exports.outputStyleCount = outputStyleCount;
|
|
17
|
+
exports.inventoryCounts = inventoryCounts;
|
|
15
18
|
/**
|
|
16
19
|
* HOW a surface was decided to be covered — the provenance of every coverage
|
|
17
20
|
* decision.
|
|
@@ -390,4 +393,40 @@ function declaredSurfaceName(content) {
|
|
|
390
393
|
const name = (0, frontmatter_read_js_1.frontmatterScalar)((0, frontmatter_read_js_1.readFrontmatter)(content), "name");
|
|
391
394
|
return name !== undefined && name.trim() ? name.trim() : null;
|
|
392
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* One output style as a coverage surface — shared by the disk detector and the
|
|
398
|
+
* browser twin, so the two name and exempt a style the same way.
|
|
399
|
+
*
|
|
400
|
+
* Named by FILE (`status-block.md` → `status-block`), not by the name a
|
|
401
|
+
* harness selects it by: the surface name is what a colocated test is spelled
|
|
402
|
+
* after, and a declared name may hold spaces or colons no file name should.
|
|
403
|
+
*/
|
|
404
|
+
function outputStyleSurface(path, content) {
|
|
405
|
+
return {
|
|
406
|
+
kind: "output-style",
|
|
407
|
+
path,
|
|
408
|
+
name: (0, posix_path_js_1.basename)(path).replace(/\.[^.]+$/, ""),
|
|
409
|
+
tokens: [path],
|
|
410
|
+
ignored: content.includes("vigiles:ignore-test"),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* How many output styles a scan found, or that the harness has none. Shared by
|
|
415
|
+
* the disk and browser scan engines, so the inventory reads the one discovery
|
|
416
|
+
* pass both already ran. Exempt styles (`vigiles:ignore-test`) are counted: the
|
|
417
|
+
* marker waives the test, not the style, and a style-only repository with an
|
|
418
|
+
* exempt style is not an empty machine.
|
|
419
|
+
*/
|
|
420
|
+
function outputStyleCount(layouts, tier, exempt) {
|
|
421
|
+
if (layouts.every((l) => l.outputStyles === undefined))
|
|
422
|
+
return "not-supported";
|
|
423
|
+
return [...tier.covered, ...tier.untested, ...exempt].filter((s) => s.kind === "output-style").length;
|
|
424
|
+
}
|
|
425
|
+
/** The inventory counts both scan engines report the same way. */
|
|
426
|
+
function inventoryCounts(files, isCommand, layouts, coverage) {
|
|
427
|
+
return {
|
|
428
|
+
commands: Object.keys(files).filter(isCommand).length,
|
|
429
|
+
outputStyles: outputStyleCount(layouts, coverage.harness, coverage.exemptSurfaces),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
393
432
|
//# sourceMappingURL=coverage-evidence.js.map
|