vigiles 6.0.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +189 -88
  2. package/dist/action-gate.js +1 -1
  3. package/dist/adapters/claude-code/agent-runtime.d.ts +46 -11
  4. package/dist/adapters/claude-code/agent-runtime.js +95 -24
  5. package/dist/adapters/claude-code/effect-region.js +1 -1
  6. package/dist/adapters/claude-code/skill-runtime.d.ts +1 -1
  7. package/dist/adapters/claude-code/skill-runtime.js +1 -1
  8. package/dist/adapters/codex/hook-protocol.js +3 -0
  9. package/dist/adapters/codex/mock-model.js +1 -1
  10. package/dist/cli-commands.d.ts +19 -0
  11. package/dist/cli-commands.js +47 -0
  12. package/dist/cli.d.ts +1 -1
  13. package/dist/cli.js +1054 -201
  14. package/dist/core/adopt.d.ts +65 -0
  15. package/dist/core/adopt.js +199 -0
  16. package/dist/core/bash-effects.d.ts +12 -0
  17. package/dist/core/bash-effects.js +31 -0
  18. package/dist/core/capability-diff.d.ts +46 -0
  19. package/dist/core/capability-diff.js +97 -0
  20. package/dist/core/compose.d.ts +1 -1
  21. package/dist/core/compose.js +1 -1
  22. package/dist/core/evolve.d.ts +4 -0
  23. package/dist/core/evolve.js +4 -0
  24. package/dist/core/frontmatter.d.ts +8 -7
  25. package/dist/core/frontmatter.js +8 -7
  26. package/dist/core/generate-harness.d.ts +1 -1
  27. package/dist/core/generate-harness.js +3 -3
  28. package/dist/core/generate-schema.js +1 -1
  29. package/dist/core/guards.d.ts +126 -0
  30. package/dist/core/guards.js +309 -0
  31. package/dist/core/harness-driver.d.ts +1 -1
  32. package/dist/core/hook-program.d.ts +459 -0
  33. package/dist/core/hook-program.js +468 -0
  34. package/dist/core/hook-protocol.d.ts +7 -0
  35. package/dist/core/hook-providers.d.ts +138 -0
  36. package/dist/core/hook-providers.js +155 -0
  37. package/dist/core/hook-spec.d.ts +74 -0
  38. package/dist/core/hook-spec.js +130 -0
  39. package/dist/core/inline.d.ts +6 -6
  40. package/dist/core/inline.js +7 -7
  41. package/dist/core/integrity.d.ts +31 -0
  42. package/dist/core/integrity.js +45 -0
  43. package/dist/core/mcp-tool.d.ts +12 -0
  44. package/dist/core/mcp-tool.js +20 -0
  45. package/dist/core/mcp.d.ts +13 -0
  46. package/dist/core/mcp.js +67 -0
  47. package/dist/core/orphans.js +1 -1
  48. package/dist/core/spec.d.ts +40 -2
  49. package/dist/core/spec.js +16 -1
  50. package/dist/core/types.d.ts +37 -5
  51. package/dist/core/validate.js +26 -26
  52. package/dist/dialect-drift.d.ts +65 -0
  53. package/dist/dialect-drift.js +216 -0
  54. package/dist/eval.d.ts +40 -5
  55. package/dist/eval.js +59 -5
  56. package/dist/guardrail-check.d.ts +85 -0
  57. package/dist/guardrail-check.js +152 -0
  58. package/dist/harness-assert.d.ts +10 -0
  59. package/dist/harness-assert.js +30 -0
  60. package/dist/hook-install.d.ts +43 -0
  61. package/dist/hook-install.js +91 -0
  62. package/dist/hook.d.ts +52 -0
  63. package/dist/hook.js +98 -0
  64. package/dist/leaderboard.d.ts +6 -0
  65. package/dist/leaderboard.js +43 -1
  66. package/dist/linting.d.ts +9 -5
  67. package/dist/linting.js +17 -5
  68. package/dist/optimize.js +1 -1
  69. package/dist/scaffold-test.js +21 -7
  70. package/dist/scan-behavioral.d.ts +60 -0
  71. package/dist/scan-behavioral.js +239 -1
  72. package/dist/scan-trigger-suggest.d.ts +54 -0
  73. package/dist/scan-trigger-suggest.js +70 -0
  74. package/dist/scan.d.ts +31 -1
  75. package/dist/scan.js +65 -3
  76. package/dist/score-explainer.js +1 -1
  77. package/dist/self-command-refs.d.ts +21 -0
  78. package/dist/self-command-refs.js +125 -0
  79. package/dist/setup-plan.d.ts +59 -1
  80. package/dist/setup-plan.js +103 -5
  81. package/dist/testing.d.ts +5 -3
  82. package/dist/testing.js +37 -23
  83. package/dist/tool-intercept.d.ts +4 -4
  84. package/dist/tool-intercept.js +5 -5
  85. package/dist/unit.d.ts +2 -0
  86. package/dist/unit.js +8 -1
  87. package/hooks/post-edit.sh +1 -1
  88. package/hooks/refs-nudge.sh +1 -1
  89. package/package.json +5 -3
  90. package/skills/adopt-spec/SKILL.md +7 -7
  91. package/skills/linter-docs/eslint.md +1 -1
  92. package/skills/strengthen/SKILL.md +1 -1
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BUILTIN_PROVIDERS = exports.provider = exports.defineProvider = exports.dangerously = exports.provide = void 0;
4
+ exports.unknownProviders = unknownProviders;
5
+ exports.unsafeInlineProviders = unsafeInlineProviders;
6
+ exports.unsafeProvider = unsafeProvider;
7
+ exports.gatherContext = gatherContext;
8
+ /**
9
+ * Hook CONTEXT PROVIDERS — let a compiled gate decide on EXTERNAL STATE (git
10
+ * branch, working-tree dirtiness, cwd, OS) WITHOUT breaking `capability = API
11
+ * surface`. The design (research/hook-context-providers.md, grounded in
12
+ * Cedar/OPA/Gatekeeper): the pure `decide` never fetches; the TRUSTED runtime
13
+ * gathers a DECLARED set of read-only facts and hands them in as `e.ctx`. A hook
14
+ * declares `needs: [...]`, so the dependency is explicit + auditable, and the
15
+ * fact-gathering I/O lives here (the trusted host), never inside the hook.
16
+ *
17
+ * Two tiers of `needs` entry, both DECLARED (the opt-out ladder):
18
+ * - a built-in provider NAME (`"git.branch"`) — the curated, famous-80% set;
19
+ * - an INLINE one-off — `provide(name, cmd)` (read-only, compile-rejected if not)
20
+ * or `dangerously(name, cmd)` (the loud, greppable escape for a command that
21
+ * isn't provably read-only) — for the long tail, no registration ceremony.
22
+ *
23
+ * Every built-in command is provably read-only (asserted in the test via
24
+ * bash-effects); a `provide()` command is checked the same way at compile. The
25
+ * gathering is parameterized over an injected `ProviderIO`, so the registry is
26
+ * testable with a fake exec and core depends on no child_process.
27
+ */
28
+ const bash_effects_js_1 = require("./bash-effects.js");
29
+ /**
30
+ * Declare an INLINE read-only fact: `provide("k8sCtx", "kubectl config current-context")`.
31
+ * The command MUST be provably read-only (compile rejects it otherwise — use
32
+ * {@link dangerously} to acknowledge a side-effecting/undecidable one). Its stdout
33
+ * is `e.ctx[name]`.
34
+ */
35
+ const provide = (name, run) => ({ kind: "inline", name, run, dangerous: false });
36
+ exports.provide = provide;
37
+ /**
38
+ * Declare an INLINE fact whose command ISN'T provably read-only — the loud,
39
+ * greppable escape hatch (the `dangerouslySetInnerHTML` / `unsafe` / `http.send`
40
+ * convention; sibling of `purity:'dangerously-unrestricted'`). A security review
41
+ * searches for this one word.
42
+ */
43
+ const dangerously = (name, run) => ({ kind: "inline", name, run, dangerous: true });
44
+ exports.dangerously = dangerously;
45
+ /** Author a registered provider: `export default defineProvider({ name, run })`. */
46
+ const defineProvider = (p) => ({
47
+ kind: "provider-def",
48
+ name: p.name,
49
+ run: p.run,
50
+ dangerous: p.dangerous ?? false,
51
+ });
52
+ exports.defineProvider = defineProvider;
53
+ /** Reference a registered provider (from `.vigiles/providers/`) by name in `needs`. */
54
+ const provider = (name) => ({ kind: "provider-ref", name });
55
+ exports.provider = provider;
56
+ /** Run a command and trim, returning "" on any failure (total). */
57
+ function tryExec(io, command) {
58
+ try {
59
+ return io.exec(command).trim();
60
+ }
61
+ catch {
62
+ return "";
63
+ }
64
+ }
65
+ /** The closed built-in registry. Each `run` is provably read-only (see the test). */
66
+ exports.BUILTIN_PROVIDERS = {
67
+ "git.branch": {
68
+ run: "git branch --show-current",
69
+ gather: (io) => tryExec(io, "git branch --show-current"),
70
+ },
71
+ "git.isDirty": {
72
+ run: "git status --porcelain",
73
+ gather: (io) => tryExec(io, "git status --porcelain").length > 0,
74
+ },
75
+ "git.root": {
76
+ run: "git rev-parse --show-toplevel",
77
+ gather: (io) => tryExec(io, "git rev-parse --show-toplevel"),
78
+ },
79
+ cwd: {
80
+ gather: (io) => io.cwd,
81
+ },
82
+ "os.platform": {
83
+ gather: (io) => io.platform,
84
+ },
85
+ "env.isCI": {
86
+ gather: (io) => io.isCI,
87
+ },
88
+ };
89
+ /** True iff a `needs` entry is an inline `provide`/`dangerously`. */
90
+ function isInline(need) {
91
+ return typeof need !== "string" && need.kind === "inline";
92
+ }
93
+ /** True iff a `needs` entry is a `provider()` reference to a registered provider. */
94
+ function isRef(need) {
95
+ return typeof need !== "string" && need.kind === "provider-ref";
96
+ }
97
+ /**
98
+ * `needs` entries that don't resolve: a built-in NAME that isn't a built-in (a
99
+ * typo), or a `provider()` ref whose name isn't in `registeredNames` (a dangling
100
+ * ref). Inline `provide`/`dangerously` are always self-defined, so never flagged.
101
+ */
102
+ function unknownProviders(needs, registeredNames = []) {
103
+ const registered = new Set(registeredNames);
104
+ const out = [];
105
+ for (const n of needs) {
106
+ if (isInline(n))
107
+ continue;
108
+ if (isRef(n)) {
109
+ if (!registered.has(n.name))
110
+ out.push(n.name);
111
+ }
112
+ else if (!(n in exports.BUILTIN_PROVIDERS))
113
+ out.push(n);
114
+ }
115
+ return out;
116
+ }
117
+ /**
118
+ * Inline `provide()` entries whose command ISN'T provably read-only — these must
119
+ * switch to `dangerously()` (the acknowledged escape) or compile is rejected. A
120
+ * `dangerously()` entry is never flagged (it already acknowledged the risk).
121
+ */
122
+ function unsafeInlineProviders(needs) {
123
+ return needs
124
+ .filter(isInline)
125
+ .filter((n) => !n.dangerous && !(0, bash_effects_js_1.isReadOnlyBash)(n.run));
126
+ }
127
+ /**
128
+ * Is a REGISTERED provider's command unsafe (not read-only and not acknowledged
129
+ * `dangerous`)? Reused when compiling a `.vigiles/providers/` file.
130
+ */
131
+ function unsafeProvider(def) {
132
+ return !def.dangerous && !(0, bash_effects_js_1.isReadOnlyBash)(def.run);
133
+ }
134
+ /**
135
+ * Gather the DECLARED facts into a context object (the trusted-host step). Only
136
+ * the names in `needs` are gathered, each at most once; a built-in, inline, or
137
+ * registered provider that can't resolve yields its default ("" / false), never
138
+ * throws. Pure over the injected `io` (CLI passes a real execSync; tests a fake)
139
+ * and the `registry` (the loaded `.vigiles/providers/`, for `provider()` refs).
140
+ */
141
+ function gatherContext(needs, io, registry = {}) {
142
+ const ctx = {};
143
+ for (const need of needs) {
144
+ if (isInline(need))
145
+ ctx[need.name] = tryExec(io, need.run);
146
+ else if (isRef(need)) {
147
+ const def = registry[need.name];
148
+ ctx[need.name] = def ? tryExec(io, def.run) : "";
149
+ }
150
+ else
151
+ ctx[need] = exports.BUILTIN_PROVIDERS[need].gather(io);
152
+ }
153
+ return ctx;
154
+ }
155
+ //# sourceMappingURL=hook-providers.js.map
@@ -0,0 +1,74 @@
1
+ /** A hook's declared side-effect posture. */
2
+ export type HookEffect = "observe" | "mutate";
3
+ /** A declared hook (the typed source the compiler reads). */
4
+ export interface HookSpec {
5
+ /** The harness event, e.g. "PreToolUse" | "PostToolUse". */
6
+ readonly event: string;
7
+ /** The tool name(s) the hook matches, e.g. ["Bash"] or ["Edit", "Write"]. */
8
+ readonly match: readonly string[];
9
+ /** The `tool_input` fields the hook extracts (must exist on the matched tools). */
10
+ readonly reads: readonly string[];
11
+ /** Observe-only (a gate/checker) or allowed to mutate (an action runner). */
12
+ readonly effect: HookEffect;
13
+ /** Optional command the hook runs — classified against {@link effect}. */
14
+ readonly run?: string;
15
+ }
16
+ /** Build a hook spec (the untyped on-ramp; see `hookFor` for the typed one). */
17
+ export declare const hook: (spec: HookSpec) => HookSpec;
18
+ /** tool name → the `tool_input` fields it carries (injected; harness-specific). */
19
+ export type ToolFieldCatalog = Record<string, readonly string[]>;
20
+ export interface HookIssue {
21
+ readonly severity: "error" | "warning";
22
+ readonly message: string;
23
+ }
24
+ export interface ValidateHookOptions {
25
+ /** The per-tool `tool_input` field catalog for the active harness. */
26
+ readonly toolFields: ToolFieldCatalog;
27
+ /** The harness's known event names (optional — skips the event check if absent). */
28
+ readonly events?: readonly string[];
29
+ }
30
+ /**
31
+ * Validate a hook spec — the compiler half. Flags wrong-field extraction (Check 1)
32
+ * and effect misdeclaration (Check 2), plus an unknown event when `events` is given.
33
+ * Pure; the same checks `compileHook` enforces before emitting.
34
+ */
35
+ export declare function validateHook(spec: HookSpec, opts: ValidateHookOptions): HookIssue[];
36
+ /** A settings `hooks` block keyed by the spec's event (the generated artifact). */
37
+ export interface CompiledHook {
38
+ readonly hooks: Record<string, readonly {
39
+ readonly matcher: string;
40
+ readonly hooks: readonly {
41
+ readonly type: "command";
42
+ readonly command: string;
43
+ }[];
44
+ }[]>;
45
+ /** field → the extraction expression a generated hook would use (the typed read). */
46
+ readonly extractions: Record<string, string>;
47
+ }
48
+ export declare class HookCompileError extends Error {
49
+ }
50
+ /**
51
+ * Compile a validated hook to its settings block. Refuses (throws) on any error-level
52
+ * issue — "an unsafe hook doesn't compile" — so a wrong-field read or a mutating
53
+ * observe-hook never ships. The `extractions` map shows the correctly-typed field
54
+ * reads the generated hook uses (here as jq paths) — never a hand-typed `jq` string.
55
+ */
56
+ export declare function compileHook(spec: HookSpec, opts: ValidateHookOptions & {
57
+ gateCommand?: string;
58
+ }): CompiledHook;
59
+ /**
60
+ * The typed source for a hook over a field map `M` (tool → field-union) and the
61
+ * matched tool(s) `T`. `reads` is constrained to the fields of `T` — a field absent
62
+ * from the matched tool is a tsc error. `M` stays unconstrained so a plain interface
63
+ * (no index signature) works as the field map.
64
+ */
65
+ export interface TypedHookSpec<M, T extends keyof M & string> {
66
+ readonly event: string;
67
+ readonly match: readonly T[];
68
+ readonly reads: readonly Extract<M[T], string>[];
69
+ readonly effect: HookEffect;
70
+ readonly run?: string;
71
+ }
72
+ /** Build a typed hook; `reads` outside the matched tool's field-union won't compile. */
73
+ export declare function hookFor<M, T extends keyof M & string>(spec: TypedHookSpec<M, T>): HookSpec;
74
+ //# sourceMappingURL=hook-spec.d.ts.map
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HookCompileError = exports.hook = void 0;
4
+ exports.validateHook = validateHook;
5
+ exports.compileHook = compileHook;
6
+ exports.hookFor = hookFor;
7
+ /**
8
+ * SPIKE — typed, effect-classified HOOKS (the "type-safe bash" angle).
9
+ *
10
+ * A Claude Code / Codex hook today is hand-written shell in settings.json. Two
11
+ * silent footguns dominate, and both are exactly what a COMPILER catches:
12
+ *
13
+ * 1. WRONG-FIELD EXTRACTION (silent no-op). A PreToolUse event's `tool_input`
14
+ * shape depends on the matched tool — `Bash` carries `command`, `Edit`/`Write`
15
+ * carry `file_path`. A hook that matches `Bash` but does
16
+ * `jq '.tool_input.file_path'` extracts EMPTY forever and never fires; nothing
17
+ * tells you. `validateHook` makes "read a field the matched tool never provides"
18
+ * an error, and the typed `hookFor` builder makes it a tsc error at edit time.
19
+ *
20
+ * 2. EFFECT MISDECLARATION ("type-safe bash"). A hook declared observe-only (a
21
+ * read-only gate/checker) that actually RUNS a mutating command — `eslint --fix`,
22
+ * `git push` — is a side effect masquerading as an observation. We already own a
23
+ * deterministic Bash-effect classifier (`bash-effects.ts`), so the command's
24
+ * effect class becomes a TYPE: an `effect: "observe"` hook whose `run` command
25
+ * classifies as side-effecting/undecidable does NOT compile. No other plugin
26
+ * tool has the classifier to make that judgment.
27
+ *
28
+ * Pure core, harness-agnostic: the per-tool field catalog is INJECTED (a Codex hook
29
+ * has different fields), and the Bash classifier is the harness-neutral one. The CC
30
+ * field catalog + the typed edit-time builder live in the test (no CC literal in core).
31
+ *
32
+ * NOT wired to the CLI/public API — a spike to see whether the compile step earns its
33
+ * keep on the hook surface. See research/harness-protocol-flow-moat.md.
34
+ */
35
+ const bash_effects_js_1 = require("./bash-effects.js");
36
+ /** Build a hook spec (the untyped on-ramp; see `hookFor` for the typed one). */
37
+ const hook = (spec) => spec;
38
+ exports.hook = hook;
39
+ /** Which matched tools actually carry `field`. */
40
+ function toolsProviding(field, match, toolFields) {
41
+ return match.filter((t) => (toolFields[t] ?? []).includes(field));
42
+ }
43
+ /** Check 1 — every read field exists on the matched tool(s). */
44
+ function fieldIssues(spec, toolFields) {
45
+ const out = [];
46
+ for (const field of spec.reads) {
47
+ const providers = toolsProviding(field, spec.match, toolFields);
48
+ if (providers.length === 0) {
49
+ out.push({
50
+ severity: "error",
51
+ message: `reads "${field}" but no matched tool (${spec.match.join("|")}) provides it — the extraction is always empty (silent no-op)`,
52
+ });
53
+ }
54
+ else if (providers.length < spec.match.length) {
55
+ const missing = spec.match.filter((t) => !providers.includes(t));
56
+ out.push({
57
+ severity: "warning",
58
+ message: `reads "${field}", which ${missing.join("/")} does not carry — empty on those events`,
59
+ });
60
+ }
61
+ }
62
+ return out;
63
+ }
64
+ /** Check 2 — an observe-only hook's command must be provably read-only. */
65
+ function effectIssues(spec) {
66
+ if (spec.effect !== "observe" || spec.run === undefined)
67
+ return [];
68
+ const cls = (0, bash_effects_js_1.classifyBashCommand)(spec.run);
69
+ if (cls === "read-only")
70
+ return [];
71
+ return [
72
+ {
73
+ severity: "error",
74
+ message: `declared observe-only but its command is ${cls}: "${spec.run}" — an observe hook must not mutate (use effect:"mutate" or a read-only command)`,
75
+ },
76
+ ];
77
+ }
78
+ /**
79
+ * Validate a hook spec — the compiler half. Flags wrong-field extraction (Check 1)
80
+ * and effect misdeclaration (Check 2), plus an unknown event when `events` is given.
81
+ * Pure; the same checks `compileHook` enforces before emitting.
82
+ */
83
+ function validateHook(spec, opts) {
84
+ const out = [];
85
+ if (opts.events && !opts.events.includes(spec.event)) {
86
+ out.push({
87
+ severity: "error",
88
+ message: `unknown event "${spec.event}" — it will never fire`,
89
+ });
90
+ }
91
+ out.push(...fieldIssues(spec, opts.toolFields));
92
+ out.push(...effectIssues(spec));
93
+ return out;
94
+ }
95
+ class HookCompileError extends Error {
96
+ }
97
+ exports.HookCompileError = HookCompileError;
98
+ /**
99
+ * Compile a validated hook to its settings block. Refuses (throws) on any error-level
100
+ * issue — "an unsafe hook doesn't compile" — so a wrong-field read or a mutating
101
+ * observe-hook never ships. The `extractions` map shows the correctly-typed field
102
+ * reads the generated hook uses (here as jq paths) — never a hand-typed `jq` string.
103
+ */
104
+ function compileHook(spec, opts) {
105
+ const issues = validateHook(spec, opts);
106
+ const errors = issues.filter((i) => i.severity === "error");
107
+ if (errors.length > 0) {
108
+ throw new HookCompileError(`hook does not compile:\n ${errors.map((e) => e.message).join("\n ")}`);
109
+ }
110
+ const command = opts.gateCommand ?? "npx vigiles hook-runtime guard";
111
+ const extractions = {};
112
+ for (const field of spec.reads)
113
+ extractions[field] = `.tool_input.${field}`;
114
+ return {
115
+ hooks: {
116
+ [spec.event]: [
117
+ {
118
+ matcher: spec.match.join("|"),
119
+ hooks: [{ type: "command", command }],
120
+ },
121
+ ],
122
+ },
123
+ extractions,
124
+ };
125
+ }
126
+ /** Build a typed hook; `reads` outside the matched tool's field-union won't compile. */
127
+ function hookFor(spec) {
128
+ return spec;
129
+ }
130
+ //# sourceMappingURL=hook-spec.js.map
@@ -64,13 +64,13 @@ export declare function parseInlineRules(content: string): InlineParseResult;
64
64
  /**
65
65
  * True if the content contains at least one parseable vigiles inline marker —
66
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.
67
+ * code blocks and malformed markers). A utility for detecting whether a file is
68
+ * inline-managed (Level-0 mode). Deliberately delegates to `parseInlineRules` so
69
+ * a loose prefix regex can't report a malformed marker that produces no real
70
+ * reference.
70
71
  *
71
- * Deliberately delegates to `parseInlineRules` so a loose prefix regex
72
- * can't satisfy require-spec with a malformed marker that produces no
73
- * real reference.
72
+ * NB `require-instructions-spec` is NARROW — it is satisfied only by a `.spec.ts`,
73
+ * not by inline markers — so this no longer feeds that rule.
74
74
  */
75
75
  export declare function hasInlineRules(content: string): boolean;
76
76
  //# sourceMappingURL=inline.d.ts.map
@@ -53,7 +53,7 @@ const KNOWN_NON_RULE_MARKERS = new Set([
53
53
  "disable",
54
54
  "ignore",
55
55
  "ignore-file",
56
- "gate", // skill step gate (src/skill-runtime.ts)
56
+ "gate", // skill step gate (src/adapters/claude-code/skill-runtime.ts)
57
57
  "result", // skill result gate
58
58
  "symbol", // symbol reference mark (src/refs.ts)
59
59
  ]);
@@ -174,13 +174,13 @@ function parseInlineRules(content) {
174
174
  /**
175
175
  * True if the content contains at least one parseable vigiles inline marker —
176
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.
177
+ * code blocks and malformed markers). A utility for detecting whether a file is
178
+ * inline-managed (Level-0 mode). Deliberately delegates to `parseInlineRules` so
179
+ * a loose prefix regex can't report a malformed marker that produces no real
180
+ * reference.
180
181
  *
181
- * Deliberately delegates to `parseInlineRules` so a loose prefix regex
182
- * can't satisfy require-spec with a malformed marker that produces no
183
- * real reference.
182
+ * NB `require-instructions-spec` is NARROW — it is satisfied only by a `.spec.ts`,
183
+ * not by inline markers — so this no longer feeds that rule.
184
184
  */
185
185
  function hasInlineRules(content) {
186
186
  const r = parseInlineRules(content);
@@ -26,4 +26,35 @@ export interface IntegrityResult {
26
26
  * Files without a hash header are treated as hand-written (intact).
27
27
  */
28
28
  export declare function checkIntegrity(content: string): IntegrityResult;
29
+ /** The marker that tells `require-instructions-spec` a file is intentionally
30
+ * hand-owned (no `.spec.ts` expected). */
31
+ export declare const REQUIRE_INSTRUCTIONS_SPEC_DISABLE = "<!-- vigiles-disable require-instructions-spec -->";
32
+ /**
33
+ * Parse the `vigiles:sha256 … compiled from <spec>` integrity header, if the
34
+ * file carries one. Returns the referenced spec path and the body below the
35
+ * header; `null` when the file is plain markdown (no header).
36
+ */
37
+ export declare function parseIntegrityHeader(content: string): {
38
+ specFile: string;
39
+ body: string;
40
+ } | null;
41
+ /**
42
+ * "Eject" a compiled instruction file to plain, hand-owned markdown: strip the
43
+ * integrity header so the file is no longer spec-managed, and prepend a
44
+ * `require-instructions-spec` disable marker so `vigiles lint` won't ask for a
45
+ * spec back. Pure — the caller writes the file and removes the spec. Returns
46
+ * `null` when there is no header to strip (nothing to eject). Idempotent: a body
47
+ * that already carries the marker is not double-marked.
48
+ *
49
+ * The disable marker is added ONLY for instruction-file bodies. A compiled
50
+ * SKILL.md / subagent body begins with YAML frontmatter (`---`) that MUST stay in
51
+ * first position — prepending an HTML comment there would push the frontmatter
52
+ * out of the lead block and the harness would lose the skill's name/description/
53
+ * tools. The marker is also meaningless for those surfaces (require-instructions-
54
+ * spec doesn't apply to them), so a frontmatter-led body is ejected as-is.
55
+ */
56
+ export declare function ejectMarkdown(content: string): {
57
+ markdown: string;
58
+ specFile: string;
59
+ } | null;
29
60
  //# sourceMappingURL=integrity.d.ts.map
@@ -19,7 +19,10 @@
19
19
  * → CI runs `vigiles compile` then `git diff --exit-code`
20
20
  */
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.REQUIRE_INSTRUCTIONS_SPEC_DISABLE = void 0;
22
23
  exports.checkIntegrity = checkIntegrity;
24
+ exports.parseIntegrityHeader = parseIntegrityHeader;
25
+ exports.ejectMarkdown = ejectMarkdown;
23
26
  const hash_js_1 = require("./hash.js");
24
27
  const HASH_LINE_RE = /^<!-- vigiles:sha256:([a-f0-9]+) compiled from (.+) -->\r?\n\r?\n?/;
25
28
  /**
@@ -41,4 +44,46 @@ function checkIntegrity(content) {
41
44
  }
42
45
  return { intact: true };
43
46
  }
47
+ /** The marker that tells `require-instructions-spec` a file is intentionally
48
+ * hand-owned (no `.spec.ts` expected). */
49
+ exports.REQUIRE_INSTRUCTIONS_SPEC_DISABLE = "<!-- vigiles-disable require-instructions-spec -->";
50
+ /**
51
+ * Parse the `vigiles:sha256 … compiled from <spec>` integrity header, if the
52
+ * file carries one. Returns the referenced spec path and the body below the
53
+ * header; `null` when the file is plain markdown (no header).
54
+ */
55
+ function parseIntegrityHeader(content) {
56
+ const match = content.match(HASH_LINE_RE);
57
+ if (!match)
58
+ return null;
59
+ return { specFile: match[2], body: content.replace(HASH_LINE_RE, "") };
60
+ }
61
+ /**
62
+ * "Eject" a compiled instruction file to plain, hand-owned markdown: strip the
63
+ * integrity header so the file is no longer spec-managed, and prepend a
64
+ * `require-instructions-spec` disable marker so `vigiles lint` won't ask for a
65
+ * spec back. Pure — the caller writes the file and removes the spec. Returns
66
+ * `null` when there is no header to strip (nothing to eject). Idempotent: a body
67
+ * that already carries the marker is not double-marked.
68
+ *
69
+ * The disable marker is added ONLY for instruction-file bodies. A compiled
70
+ * SKILL.md / subagent body begins with YAML frontmatter (`---`) that MUST stay in
71
+ * first position — prepending an HTML comment there would push the frontmatter
72
+ * out of the lead block and the harness would lose the skill's name/description/
73
+ * tools. The marker is also meaningless for those surfaces (require-instructions-
74
+ * spec doesn't apply to them), so a frontmatter-led body is ejected as-is.
75
+ */
76
+ function ejectMarkdown(content) {
77
+ const parsed = parseIntegrityHeader(content);
78
+ if (!parsed)
79
+ return null;
80
+ // A frontmatter-led body is a skill/agent — strip the header, add nothing.
81
+ if (/^---\r?\n/.test(parsed.body)) {
82
+ return { markdown: parsed.body, specFile: parsed.specFile };
83
+ }
84
+ const markdown = parsed.body.startsWith(exports.REQUIRE_INSTRUCTIONS_SPEC_DISABLE)
85
+ ? parsed.body
86
+ : `${exports.REQUIRE_INSTRUCTIONS_SPEC_DISABLE}\n\n${parsed.body}`;
87
+ return { markdown, specFile: parsed.specFile };
88
+ }
44
89
  //# sourceMappingURL=integrity.js.map
@@ -40,6 +40,18 @@ export interface McpToolIssue {
40
40
  * A `Tool(restriction)` suffix is stripped first.
41
41
  */
42
42
  export declare function mcpToolServer(raw: string, dialect: HarnessDialect): string | null;
43
+ /**
44
+ * Split a direct `mcp__<server>__<tool>` reference into its `{ server, tool }`
45
+ * parts, or null when {@link mcpToolServer} declines it (non-MCP, plugin-namespaced,
46
+ * malformed). The tool segment is everything after `mcp__<server>__` (MCP tool
47
+ * names may themselves contain `__`). A `Tool(restriction)` suffix is stripped.
48
+ * Used by the LIVE contract-tool resolution (`verifyMcpContractTools` in mcp.ts) to
49
+ * check the tool actually exists on the server, not just that the server resolves.
50
+ */
51
+ export declare function mcpToolParts(raw: string, dialect: HarnessDialect): {
52
+ server: string;
53
+ tool: string;
54
+ } | null;
43
55
  /**
44
56
  * Verify the MCP tool references in a contract against the plugin's declared MCP
45
57
  * servers. Returns one {@link McpToolIssue} per direct `mcp__<server>__<tool>`
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.mcpToolServer = mcpToolServer;
4
+ exports.mcpToolParts = mcpToolParts;
4
5
  exports.verifyMcpToolServers = verifyMcpToolServers;
5
6
  /**
6
7
  * The server segment of a direct `mcp__<server>__<tool>` reference, or null when
@@ -23,6 +24,25 @@ function mcpToolServer(raw, dialect) {
23
24
  return null;
24
25
  return server;
25
26
  }
27
+ /**
28
+ * Split a direct `mcp__<server>__<tool>` reference into its `{ server, tool }`
29
+ * parts, or null when {@link mcpToolServer} declines it (non-MCP, plugin-namespaced,
30
+ * malformed). The tool segment is everything after `mcp__<server>__` (MCP tool
31
+ * names may themselves contain `__`). A `Tool(restriction)` suffix is stripped.
32
+ * Used by the LIVE contract-tool resolution (`verifyMcpContractTools` in mcp.ts) to
33
+ * check the tool actually exists on the server, not just that the server resolves.
34
+ */
35
+ function mcpToolParts(raw, dialect) {
36
+ const clean = raw.split("(")[0].trim();
37
+ const server = mcpToolServer(clean, dialect);
38
+ if (server === null)
39
+ return null;
40
+ const prefix = `mcp__${server}__`;
41
+ if (!clean.startsWith(prefix))
42
+ return null;
43
+ const tool = clean.slice(prefix.length);
44
+ return tool ? { server, tool } : null;
45
+ }
26
46
  /**
27
47
  * Verify the MCP tool references in a contract against the plugin's declared MCP
28
48
  * servers. Returns one {@link McpToolIssue} per direct `mcp__<server>__<tool>`
@@ -43,6 +43,19 @@ export declare function loadMcpServers(cwd: string): Record<string, McpServerCon
43
43
  * to an undeclared server, an unreachable server, or a missing tool is an error.
44
44
  */
45
45
  export declare function verifyMcpRefs(markdown: string, mcpServers: Record<string, McpServerConfig>, timeoutMs?: number): Promise<McpRefError[]>;
46
+ export type McpContractToolReason = "server-unreachable" | "tool-missing";
47
+ export interface McpContractToolError {
48
+ /** The full `mcp__server__tool` reference (restriction suffix stripped). */
49
+ readonly tool: string;
50
+ readonly server: string;
51
+ /** The tool segment (what's looked up on the server). */
52
+ readonly toolName: string;
53
+ readonly reason: McpContractToolReason;
54
+ readonly suggestions: string[];
55
+ }
56
+ export declare function verifyMcpContractTools(tools: readonly string[], servers: Record<string, McpServerConfig>, dialect: import("./dialect.js").HarnessDialect, timeoutMs?: number): Promise<McpContractToolError[]>;
57
+ /** Human-readable message for a contract-tool error (with "did you mean"). */
58
+ export declare function mcpContractToolMessage(e: McpContractToolError): string;
46
59
  /** Human-readable message for an MCP reference error (with "did you mean"). */
47
60
  export declare function mcpRefMessage(e: McpRefError): string;
48
61
  //# sourceMappingURL=mcp.d.ts.map
package/dist/core/mcp.js CHANGED
@@ -5,6 +5,8 @@ exports.verifyMcpTool = verifyMcpTool;
5
5
  exports.parseMcpRefs = parseMcpRefs;
6
6
  exports.loadMcpServers = loadMcpServers;
7
7
  exports.verifyMcpRefs = verifyMcpRefs;
8
+ exports.verifyMcpContractTools = verifyMcpContractTools;
9
+ exports.mcpContractToolMessage = mcpContractToolMessage;
8
10
  exports.mcpRefMessage = mcpRefMessage;
9
11
  /**
10
12
  * Minimal MCP client over stdio — start a server, do the JSON-RPC handshake, and
@@ -21,6 +23,7 @@ const node_fs_1 = require("node:fs");
21
23
  const node_path_1 = require("node:path");
22
24
  const refs_js_1 = require("./refs.js");
23
25
  const hash_js_1 = require("./hash.js");
26
+ const mcp_tool_js_1 = require("./mcp-tool.js");
24
27
  function dispatch(line, pending) {
25
28
  let msg;
26
29
  try {
@@ -229,6 +232,70 @@ async function verifyMcpRefs(markdown, mcpServers, timeoutMs = 10000) {
229
232
  }
230
233
  return all;
231
234
  }
235
+ /** Group the direct, declared-server `mcp__server__tool` refs by server (de-duped). */
236
+ function groupContractTools(tools, servers, dialect) {
237
+ const byServer = new Map();
238
+ const seen = new Set();
239
+ for (const raw of tools) {
240
+ const parts = (0, mcp_tool_js_1.mcpToolParts)(raw, dialect);
241
+ if (!parts || !(parts.server in servers))
242
+ continue; // skip non-MCP/undeclared
243
+ const full = raw.split("(")[0].trim();
244
+ if (seen.has(full))
245
+ continue;
246
+ seen.add(full);
247
+ const arr = byServer.get(parts.server) ?? [];
248
+ arr.push({ full, toolName: parts.tool });
249
+ byServer.set(parts.server, arr);
250
+ }
251
+ return byServer;
252
+ }
253
+ /** Start one server and check its group of referenced tools against the live list. */
254
+ async function checkServerGroup(server, group, cfg, timeoutMs) {
255
+ let available;
256
+ try {
257
+ available = (await listMcpTools(cfg, timeoutMs)).map((t) => t.name);
258
+ }
259
+ catch {
260
+ return group.map((g) => ({
261
+ tool: g.full,
262
+ server,
263
+ toolName: g.toolName,
264
+ reason: "server-unreachable",
265
+ suggestions: [],
266
+ }));
267
+ }
268
+ return group
269
+ .filter((g) => !available.includes(g.toolName))
270
+ .map((g) => ({
271
+ tool: g.full,
272
+ server,
273
+ toolName: g.toolName,
274
+ reason: "tool-missing",
275
+ suggestions: closest(g.toolName, available),
276
+ }));
277
+ }
278
+ async function verifyMcpContractTools(tools, servers, dialect, timeoutMs = 10000) {
279
+ const byServer = groupContractTools(tools, servers, dialect);
280
+ const errors = [];
281
+ for (const [server, group] of byServer) {
282
+ errors.push(...(await checkServerGroup(server, group, servers[server], timeoutMs)));
283
+ }
284
+ return errors;
285
+ }
286
+ /** Human-readable message for a contract-tool error (with "did you mean"). */
287
+ function mcpContractToolMessage(e) {
288
+ switch (e.reason) {
289
+ case "server-unreachable":
290
+ return `MCP tool "${e.tool}" — server "${e.server}" failed to start`;
291
+ case "tool-missing":
292
+ return `MCP tool "${e.tool}" not found on server "${e.server}"${e.suggestions.length > 0
293
+ ? ` — did you mean ${e.suggestions.map((s) => `"${s}"`).join(", ")}?`
294
+ : ""}`;
295
+ default:
296
+ return (0, hash_js_1.assertNever)(e.reason);
297
+ }
298
+ }
232
299
  /** Human-readable message for an MCP reference error (with "did you mean"). */
233
300
  function mcpRefMessage(e) {
234
301
  switch (e.reason) {