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.
Files changed (66) hide show
  1. package/README.md +3 -2
  2. package/dist/adapter-conformance.js +14 -10
  3. package/dist/adapter.d.ts +3 -1
  4. package/dist/adapters/claude-code/layout.js +3 -0
  5. package/dist/adapters/claude-code/output-style.d.ts +3 -0
  6. package/dist/adapters/claude-code/output-style.js +81 -0
  7. package/dist/adapters/claude-code/replies.d.ts +14 -0
  8. package/dist/adapters/claude-code/replies.js +78 -0
  9. package/dist/adapters/claude-code/runtime.js +70 -0
  10. package/dist/adapters/claude-code/skill-reachability.d.ts +42 -21
  11. package/dist/adapters/claude-code/skill-reachability.js +91 -28
  12. package/dist/audit-report.d.ts +5 -0
  13. package/dist/audit-report.js +29 -20
  14. package/dist/audit-report.template.html +25 -25
  15. package/dist/audit-score.js +2 -2
  16. package/dist/check.d.ts +20 -0
  17. package/dist/check.js +87 -2
  18. package/dist/cli-main.js +249 -30
  19. package/dist/core/adapter.d.ts +7 -0
  20. package/dist/core/config-schema.d.ts +9 -0
  21. package/dist/core/config-schema.js +1 -0
  22. package/dist/core/eval-driver.d.ts +10 -0
  23. package/dist/core/frontmatter-read.d.ts +31 -0
  24. package/dist/core/frontmatter-read.js +4 -2
  25. package/dist/core/harness-driver.d.ts +2 -0
  26. package/dist/core/install-reader.js +15 -0
  27. package/dist/core/layout.d.ts +17 -0
  28. package/dist/core/layout.js +17 -0
  29. package/dist/core/output-style.d.ts +84 -0
  30. package/dist/core/output-style.js +90 -0
  31. package/dist/core/rule-meta.d.ts +1 -1
  32. package/dist/core/rule-meta.js +8 -0
  33. package/dist/core/run-env.d.ts +50 -0
  34. package/dist/core/run-env.js +51 -0
  35. package/dist/core/runtime.d.ts +44 -0
  36. package/dist/core/skill-link-target.d.ts +20 -0
  37. package/dist/core/skill-link-target.js +33 -0
  38. package/dist/core/types.d.ts +2 -0
  39. package/dist/coverage-evidence.d.ts +42 -0
  40. package/dist/coverage-evidence.js +39 -0
  41. package/dist/eval.d.ts +65 -42
  42. package/dist/eval.js +116 -120
  43. package/dist/harness-test.d.ts +65 -1
  44. package/dist/harness-test.js +114 -22
  45. package/dist/optimize.js +1 -5
  46. package/dist/output-style-arms.d.ts +27 -0
  47. package/dist/output-style-arms.js +51 -0
  48. package/dist/plugin-declaration.d.ts +6 -6
  49. package/dist/plugin-declaration.js +6 -6
  50. package/dist/scan-files.js +1 -1
  51. package/dist/scan.d.ts +6 -0
  52. package/dist/scan.js +14 -4
  53. package/dist/score-core.d.ts +3 -0
  54. package/dist/score-core.js +5 -1
  55. package/dist/setup-plan.d.ts +50 -10
  56. package/dist/setup-plan.js +103 -68
  57. package/dist/skill-links.d.ts +183 -0
  58. package/dist/skill-links.js +255 -0
  59. package/dist/test-coverage-files.d.ts +3 -1
  60. package/dist/test-coverage-files.js +20 -6
  61. package/dist/test-coverage.d.ts +18 -1
  62. package/dist/test-coverage.js +101 -15
  63. package/dist/test.d.ts +3 -1
  64. package/dist/test.js +8 -2
  65. package/dist/vigilesrc.schema.json +134 -0
  66. 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")),
@@ -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
@@ -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
@@ -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`. */
@@ -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
@@ -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
@@ -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