/** * vigiles v2 — Executable specification system. * * Specs are TypeScript files that compile to instruction files (CLAUDE.md, SKILL.md). * The spec is the source of truth. The markdown is a build artifact. * * Two rule types: * enforce() — delegated to an external linter (ESLint, Ruff, Clippy, etc.) * guidance() — prose only, no mechanical enforcement */ /** Linters and policy catalogs vigiles can cross-reference. */ /** * The built-in linter / policy catalogs vigiles cross-references — the SINGLE * source for the set. `BuiltinLinter` derives from it, the `LINTERS` registry * is keyed by it, the conformance test cross-checks it, and browser-safe * consumers (the website) import this array to list the linters without going * stale. Zero node imports here, so it is importable everywhere. */ export declare const BUILTIN_LINTERS: readonly ["eslint", "stylelint", "ruff", "clippy", "pylint", "rubocop", "detekt", "ktlint", "checkstyle", "golangci-lint", "cedar"]; export type BuiltinLinter = (typeof BUILTIN_LINTERS)[number]; /** Scoped ESLint plugin prefix (e.g., @typescript-eslint). */ type ScopedPlugin = `@${string}/${string}`; /** A linter/rule reference: "eslint/no-console", "ruff/T201", "@typescript-eslint/no-explicit-any". */ export type LinterRule = `${BuiltinLinter}/${string}` | ScopedPlugin; /** Vigiles-proven rule reference: "vigiles/". */ export type VigilesRef = `vigiles/${string}`; /** Any enforcement reference. */ export type EnforcementRef = LinterRule | VigilesRef; /** * Populated by generate-types with per-linter rule unions. * Keys are linter prefixes ("eslint", "@typescript-eslint", "ruff", etc.), * values are unions of enabled rule names. */ export interface KnownLinterRules { } /** * Populated by generate-types with project file paths. * Single key "files" maps to a union of relative paths. */ export interface KnownProjectFiles { } /** * Populated by generate-types with npm script names. * Single key "scripts" maps to a union of script names. */ export interface KnownNpmScripts { } /** * When KnownLinterRules is populated, narrows to exact rule unions. * Falls back to broad LinterRule when no generated types exist. */ export type StrictLinterRule = [keyof KnownLinterRules] extends [never] ? LinterRule : { [K in keyof KnownLinterRules]: `${K & string}/${KnownLinterRules[K] & string}`; }[keyof KnownLinterRules]; /** * When KnownProjectFiles is populated, narrows file() to known paths. * Falls back to string when no generated types exist. */ export type StrictFile = [keyof KnownProjectFiles] extends [never] ? string : KnownProjectFiles[keyof KnownProjectFiles] & string; export type StrictCmd = [keyof KnownNpmScripts] extends [never] ? string : `npm run ${KnownNpmScripts[keyof KnownNpmScripts] & string}` | `npm ${KnownNpmScripts[keyof KnownNpmScripts] & string}` | (string & {}); /** * A harness's tool vocabulary, split by the purity floor that admits it. The * read-only/side-effecting split is harness-specific (it comes from the * dialect's `builtinAgentTools` − `sideEffectingTools`), so the concrete unions * are supplied by the adapter; core only describes the SHAPE. * * - `readOnly`: tools a `pure` unit may declare (observation only). * - `bounded`: tools a `bounded` unit may declare — the read-only set PLUS the * decidable side-effecting tools (Write/Edit/NotebookEdit) AND `Bash` (its * command is decided at RUNTIME by the gate; the floor admits the tool). Bars * MCP / unknown / wildcard. * * The default (`string` at both) imposes no constraint — any tool is accepted at * every level, the historical behaviour of an untyped `experimental_agent()`/`skill()`. */ export interface ToolVocabulary { /** Union of tool names allowed under `purity: "pure"`. */ readonly readOnly: string; /** Union of tool names allowed under `purity: "bounded"`. */ readonly bounded: string; } /** The fully-open default vocabulary — no constraint at any purity level. */ export interface OpenToolVocabulary extends ToolVocabulary { readonly readOnly: string; readonly bounded: string; } /** * The tool names ALLOWED at a declared `purity`, given a vocabulary `V`: * - `"pure"` → only `V["readOnly"]`. * - `"bounded"` → `V["bounded"]` (read-only ∪ decidable side-effecting ∪ `Bash`). * - `"dangerously-unrestricted"` (or no purity) → `string` (anything). * * With the open default vocabulary every branch widens to `string`, so an * untyped surface accepts any tools. */ export type AllowedAt

= P extends "pure" ? V["readOnly"] : P extends "bounded" ? V["bounded"] : string; /** * Where a rule's long text lives when the compiled file carries a short form — * a `file()` reference (verified to exist, like every other one) and an * optional anchor inside it (verified to be a heading or an explicit * `` in that file). Rendered as a `[Details](path#anchor)` link. */ export interface RuleDetails { readonly file: FileRef; readonly anchor?: string; } /** * How much of an `enforce()` rule the compiled file carries (#355). * * - `"full"` (default) — the rule id and the whole `why`, as always. * - `"line"` — the rule id and the first sentence of `why`. * - `"id"` — the rule id only: the linter already holds the rule, so the prose * is a second copy that costs tokens on every request and can drift. * * Whatever is rendered, the rule stays whole in the SPEC — lint, coverage, the * audit rule map and `enforce-rules-format` read the spec, not the markdown — * and the integrity hash is taken over the RENDERED file, so it stays a * function of what the model actually sees. */ export type RuleRender = "full" | "line" | "id"; /** A rule delegated to an external tool (linter, ast-grep, dependency-cruiser, etc.) or to a vigiles-internal check. */ export interface EnforceRule { readonly _kind: "enforce"; readonly linterRule: LinterRule | VigilesRef; readonly why: string; /** Skip verification for this rule. Default: true (verify). */ readonly verify: boolean; /** How much of the rule the compiled file carries. Default `"full"`. */ readonly render?: RuleRender; /** Where the long text lives; rendered as a verified link. */ readonly details?: RuleDetails; } /** A guidance-only rule (prose, no enforcement). */ export interface GuidanceRule { readonly _kind: "guidance"; readonly text: string; /** Where the long text lives; rendered as a verified link after `text`. */ readonly details?: RuleDetails; } /** * The `details` half of a rule's options. An `anchor` is only meaningful * inside a `details` file, so the type refuses one without the other. */ export type RuleDetailsOptions = { readonly details: FileRef; readonly anchor?: string; } | { readonly details?: undefined; readonly anchor?: never; }; /** A reactive rule: runs a command when watched files change. */ export interface GuardRule { readonly _kind: "guard"; readonly watch: string | readonly string[]; readonly run: string; readonly description: string; } export type Rule = EnforceRule | GuidanceRule | GuardRule; /** * Declare a rule enforced by an external tool or vigiles itself. * * When generated types are present, the `linterRule` argument is narrowed * to only accept rules that exist in your linter configs. Vigiles-internal * checks use the `vigiles/` namespace and are dispatched to * built-in mechanical validators (e.g. `vigiles/orphan-docs`). * * enforce("eslint/no-console", "Use structured logger.") * enforce("@typescript-eslint/no-floating-promises", "Always await.") * enforce("ruff/T201", "Use logging module.") * enforce("vigiles/orphan-docs", "No docs without spec references.") */ export declare function enforce(ref: NoInfer | VigilesRef, why: string, options?: { verify?: boolean; render?: RuleRender; } & RuleDetailsOptions): EnforceRule; /** * Declare a guidance-only rule. * * guidance("Google unfamiliar APIs before implementing.") * guidance("Short statement.", { details: file("docs/rules/x.md"), anchor: "why" }) */ export declare function guidance(text: string, options?: RuleDetailsOptions): GuidanceRule; /** * Declare a reactive guard: runs a command when watched files change. * * guard({ watch: "*.spec.ts", run: "npx vigiles compile" }, "Recompile on spec change") * guard({ watch: ["eslint.config.*", "package.json"], run: "npx vigiles generate types" }, "Regen types") */ export declare function guard(options: { watch: string | readonly string[]; run: string; }, description: string): GuardRule; /** * Branded string types — these prove a reference has gone through * vigiles's verification. The compiler only accepts branded refs, * not raw strings, for path-sensitive positions. */ declare const __brand: unique symbol; export type VerifiedPath = string & { readonly [__brand]: "VerifiedPath"; }; export type VerifiedCmd = string & { readonly [__brand]: "VerifiedCmd"; }; export type VerifiedRef = string & { readonly [__brand]: "VerifiedRef"; }; export type VerifiedDir = string & { readonly [__brand]: "VerifiedDir"; }; export type VerifiedGlob = string & { readonly [__brand]: "VerifiedGlob"; }; /** A typed file reference — verified at compile time. */ export interface FileRef { readonly _ref: "file"; readonly path: VerifiedPath; } /** A typed command reference — verified at compile time. */ export interface CmdRef { readonly _ref: "cmd"; readonly command: VerifiedCmd; } /** A typed cross-reference to another instruction file/skill. */ export interface SkillRef { readonly _ref: "skill"; readonly path: VerifiedRef; } /** A typed symbol reference — the named file must define the named symbol. */ export interface SymbolRef { readonly _ref: "symbol"; readonly file: VerifiedPath; readonly symbol: string; } /** A typed directory reference — verified to exist AND be a directory. */ export interface DirRef { readonly _ref: "dir"; readonly path: VerifiedDir; } /** A typed glob reference — verified to match at least one path. */ export interface GlobRef { readonly _ref: "glob"; readonly pattern: VerifiedGlob; } export type Ref = FileRef | CmdRef | SkillRef | SymbolRef | DirRef | GlobRef; /** * Reference a file path — verified to exist at compile time. * When generated types are present, narrowed to known project files. */ export declare function file(path: NoInfer): FileRef; /** * Reference a command — verified against package.json at compile time. * When generated types are present, narrowed to known npm scripts. */ export declare function cmd(command: NoInfer): CmdRef; /** * Reference a symbol defined in a file — verified at compile time that the * named file exists AND defines the named symbol (via ast-grep, cross-language). * Compiles to the file-qualified inline form `` `file#symbol` `` so the markdown * `lint` / `refs-hook` re-verify the same reference. */ export declare function symbol(file: NoInfer, name: string): SymbolRef; /** * Reference another skill or instruction file — verified to exist. * Compiles to a markdown link: [skill name](path) */ export declare function ref(path: string): SkillRef; /** * Reference a directory — verified at compile time to exist AND be a directory * (not a file). The "architecture floats free" fix: a spec that names `src/core/` * proves the directory is really there, where a plain string in prose rots * silently. Compiles to the inline form `` `path` ``. */ export declare function dir(path: string): DirRef; /** * Reference a glob pattern — verified at compile time to match at least one path, * so `glob("src/*.test.ts")` proves tests actually exist where the instructions * claim (the pattern supports the usual `*` / `**` syntax). Compiles to the * inline form `` `pattern` ``. */ export declare function glob(pattern: string): GlobRef; /** * A marked side-effect BOUNDARY inside a skill/agent body — "side effects are * allowed ONLY inside this block." Compiles to `` … * `` markers the runtime PreToolUse gate keys on: outside * the region the unit is treated as read-only (the `"pure"` effective floor), * inside it the declared purity floor applies. The position-aware companion to * the per-call `purity` floor. See `research/effect-boundary-design.md`. * * @internal Experimental (parked P3) — NOT part of the frozen public surface; * may change or be removed without a major bump pre-1.0. */ export interface EffectRegion { readonly _ref: "effect"; readonly body: InstructionFragment[]; } export type InstructionFragment = string | Ref | EffectRegion; /** * Tagged template literal for skill instructions with typed references. * * prose` * Check ${file("eslint.config.ts")} for rules. * Run ${cmd("npm test")} to verify. * See ${ref("skills/other/SKILL.md")} for format. * ` */ export declare function prose(strings: TemplateStringsArray, ...values: InstructionFragment[]): InstructionFragment[]; /** * Tagged template literal marking a side-effect boundary — usable as an * interpolated fragment inside a body / `instructions\`\``: * * prose` * ## Apply * ${effect` * Side effects are allowed ONLY here: * - write ${file("CHANGELOG.md")} * - ${cmd("npm publish")} * `} * ` * * Returns an `EffectRegion` fragment; `compile` wraps its rendered body in * `` markers. Independent of the `doc()` authoring * surface — it does not block on it. * * @experimental Experimental (parked P3) — NOT part of the frozen public surface; * may change or be removed without a major bump pre-1.0. */ export declare function experimental_effect(strings: TemplateStringsArray, ...values: InstructionFragment[]): EffectRegion; /** * The purity an author DECLARES for a skill/agent — the floor `compile` * enforces against the tool contract (see `purityViolations` in * `core/effects.ts`). Mirrors the analysis `PurityLevel` for the two meaningful * rungs, so what you DECLARE and what `scan` REPORTS share one vocabulary: * - `"pure"`: only read-only tools — no side effects at all. * - `"bounded"`: decidable side-effecting tools (Write, Edit, …) are allowed, * but not `Bash` / unknown-effect / inherits-all (the unbounded cells). * - `"dangerously-unrestricted"`: the explicit escape hatch — no enforcement. * Deliberately loud (cf. React's `dangerouslySetInnerHTML`) so opting OUT of * the guardrail stands out in review. Omitting `purity` is the same * (unenforced) default WITHOUT typing the loud word — you write it only when * you mean to override a stricter level. */ export type AuthoredPurity = "pure" | "bounded" | "dangerously-unrestricted"; /** Known markdown instruction file targets. */ export type InstructionTarget = "CLAUDE.md" | "AGENTS.md" | (string & {}); export interface ClaudeSpec { readonly _specType: "claude"; /** * Output filename(s). Defaults to "CLAUDE.md". Also used as the h1 heading. * Pass an array to compile one spec to multiple targets (e.g., CLAUDE.md + AGENTS.md). */ readonly target?: InstructionTarget | InstructionTarget[]; /** npm scripts / shell commands → descriptions. Verified against package.json. */ readonly commands?: Record; /** * File paths → descriptions, compiled into a "## Key Files" list. Verified via existsSync. * * @deprecated Move each entry out, then remove `keyFiles`. The list loads * on every request to tell the agent where files are, which it finds itself. * For each entry: delete it if the file or the docs already say it; move a * fact about one file into that file's header comment; move a rule for one * folder into that folder's nested instruction file; keep a one-line * `file()` pointer (verified the same way) only if nothing else leads there. * Still compiled for one major — the one that ships this notice — and * removed in the major after it; `vigiles lint` reports every use as * `deprecated-key-files`. Migration: docs/rules/deprecated-key-files.md. */ readonly keyFiles?: Record; /** Named prose sections — plain strings or tagged templates with file()/cmd()/ref(). */ readonly sections?: Record; /** * Maximum lines for a single named prose section. Overrides the generous * compile-time default (200 lines) that guards every section + agent section * against an egregious content dump — set a tighter number to enforce your own * house limit, or a larger one for an intentionally long section. */ readonly maxSectionLines?: number; /** * Maximum estimated tokens for the compiled output (~4 chars per token). * Compile fails if exceeded. Matches ETH Zurich 2511.12884 finding that * files over ~300 lines / ~2000 tokens degrade agent task success. */ readonly maxTokens?: number; /** Rules: enforce(), check(), or guidance(). */ readonly rules: Record; } /** * Input type for instructionFile() — maxSectionLines is only valid when sections are provided. * TypeScript errors if you set maxSectionLines without defining sections. */ type ClaudeSpecBase = { readonly target?: InstructionTarget | InstructionTarget[]; readonly commands?: Record; /** @deprecated Move each entry out, then remove `keyFiles` — see {@link ClaudeSpec.keyFiles}. Removed in the major after the one that deprecates it. */ readonly keyFiles?: Record; readonly maxTokens?: number; readonly rules: Record; }; type ClaudeSpecSections = { readonly sections: Record; readonly maxSectionLines?: number; } | { readonly sections?: undefined; readonly maxSectionLines?: never; }; type ClaudeSpecInput = ClaudeSpecBase & ClaudeSpecSections; /** * Define a CLAUDE.md specification. * * // CLAUDE.md.spec.ts * export default instructionFile({ commands: {...}, rules: {...} }); */ export declare function instructionFile(spec: ClaudeSpecInput): ClaudeSpec; /** * A deterministic gate on a skill step or its final result. A gate is one of: * a command (exit 0), a file (must exist), or a *project role* that resolves to * the host project's real command at run time. cmd/file gates are verified * against the repo at author time; role gates are portable — a skill that runs * in other repos should prefer `project("test")` over a hard-coded `npm test`. */ export type Gate = CmdRef | FileRef | RoleGate; /** Project command roles, resolved per host project at run time. */ export type ProjectRole = "test" | "build" | "lint"; /** A portable gate that resolves to the host project's command for a role. */ export interface RoleGate { readonly _ref: "role"; readonly role: ProjectRole; } /** * A portable gate that resolves to the host project's command for a role * (e.g. `project("test")` → `npm test` / `pytest` / `cargo test`). Use this * in skills meant to run across projects, instead of hard-coding a command. */ export declare function project(role: ProjectRole): RoleGate; /** * A declared skill input. Compiles to the `argument-hint` frontmatter and a * `## Arguments` section; referenced as `$1`/`$2`/`$ARGUMENTS` in the body. */ export interface SkillInput { /** Argument name, e.g. "pattern". */ readonly name: string; /** Human-readable hint shown in argument-hint and the Arguments section. */ readonly hint: string; /** Required by default; set false to render as optional (`[]`). */ readonly required?: boolean; } /** One step of a gated skill pipeline. */ export interface SkillStep { /** What the model should do — prose, optionally with typed refs. */ readonly do: string | InstructionFragment[]; /** Deterministic check that must pass before advancing to the next step. */ readonly gate?: Gate; /** Max attempts to satisfy the gate before the step fails (default 1). */ readonly retry?: number; } /** * Declare a skill input (compiles to argument-hint + an Arguments entry). * * Deliberately NOT a standalone export — reached as `experimental_skill.input`. * The reason is on `experimental_skill` below. */ declare function input(name: string, hint: string, opts?: { required?: boolean; }): SkillInput; /** * Declare a gated pipeline step. * * Deliberately NOT a standalone export — reached as `experimental_skill.step`. */ declare function step(instr: string | InstructionFragment[], opts?: { gate?: Gate; retry?: number; }): SkillStep; export interface SkillSpec { readonly _specType: "skill"; /** Skill name (used in frontmatter). */ readonly name: string; /** Short description (used in frontmatter). */ readonly description: string; /** * Hint for the argument (frontmatter). Ignored when `inputs` is set — * `inputs` derive the argument-hint instead. */ readonly argumentHint?: string; /** Typed inputs — compile to argument-hint + a `## Arguments` section. */ readonly inputs?: readonly SkillInput[]; /** Whether to disable model invocation (frontmatter flag). */ readonly disableModelInvocation?: boolean; /** * Execution context. `"fork"` runs the skill's body as the task inside a * forked SUBAGENT (its own context window) instead of inline in the main * conversation (Anthropic's `context: fork` frontmatter). This is the ONLY * setting under which a skill gains a real call→return boundary — so it's the * prerequisite for declaring an `output` Result contract (see `output`). Omit * for the default inline execution. */ readonly context?: "fork"; /** * The subagent type a forked skill starts (`agent` frontmatter): a built-in * type (`Explore`, `Plan`, `general-purpose`) or a custom one from the * harness's agents directory. Requires `context: "fork"` — without a fork * there is no subagent to pick (compile error `agent-without-fork`). Omitted, * the harness uses `general-purpose`. */ readonly agent?: string; /** * The model the skill runs on (`model` frontmatter): an alias (`sonnet`, * `opus`, `haiku`, `fable`), a full model id, or `inherit`. With * `context: "fork"` it sets the forked subagent's model; inline, it overrides * the model for the rest of the current turn. Omitted on a forked skill, the * subagent resolves to the main conversation's model — which is the costly * default this field exists to override. */ readonly model?: string; /** * The allowed-tools contract for this skill. Each entry must be a known * built-in tool or an MCP tool (`mcp__server__tool`). Omit to inherit all * tools. When `purity` is `"pure"`/`"bounded"`, the declared tools are checked * against that floor — compile rejects a tool looser than the declared level. */ readonly tools?: readonly string[]; /** * Tools this skill may NEVER use — rendered to the `disallowed-tools:` * frontmatter key (hyphenated for skills; a subagent's key is `disallowedTools:`, * and they are read by different parsers, so the spelling is not interchangeable). * * 🔴 This is NOT the symmetric twin of `tools` and the asymmetry is the whole * reason it exists. `allowed-tools:` on a skill is a PRE-APPROVAL — it waives the * permission prompt for what it lists and removes nothing from the pool, so a * skill that declares only `Read` can still call `Bash`. `disallowed-tools:` is * the only key measured to actually take a tool away. A skill without it inherits * every tool the session grants, which is why `audit` reports each such skill as * holding all three lethal-trifecta legs — including skills whose `tools` list * looks tightly scoped. * * Entries are checked like a subagent's: a close typo of a real tool name blocks * nothing and is reported, because a fence with a misspelled name reads as * protection while granting everything. */ readonly disallowedTools?: readonly string[]; /** * Declare this skill's purity floor — compile rejects a tool contract looser * than it. `"pure"` allows only read-only tools; `"bounded"` also allows * decidable side-effecting tools (Write, Edit, …) but bars `Bash` / * unknown-effect / inherits-all; `"dangerously-unrestricted"` (or omitting it) * enforces nothing. NOTE: `"pure"`/`"bounded"` require an explicit read-only * `tools` list — an absent list inherits ALL tools and is a violation. */ readonly purity?: AuthoredPurity; /** * Gated pipeline steps. When set, the skill compiles to a `## Steps` * checklist with a deterministic gate per step. Use this OR `body`. */ readonly steps?: readonly SkillStep[]; /** * Terminal postcondition — the skill is "done" only when this gate passes. * Compiles to a `## Result` section + a `vigiles:result` marker. * * 🔴 NAMED `postcondition`, NOT `result`, because `result` already means * something else one screen down: {@link result} builds a subagent's typed * ok/err CONTRACT (and reaches a skill through `output:`). Two concepts under * one word, told apart only by whether you wrote `result:` or `output: * result(...)` — the doc comment on {@link result} had to spend a line saying * "distinct from a skill's `result:` postcondition gate", which is the tell. * A name that needs a disambiguating sentence is the wrong name. * * The compiled MARKER stays `vigiles:result` deliberately: it is the wire * format between the compiler and {@link parseSkillGates}, and every already * compiled SKILL.md on disk carries it. Renaming the authoring field is a * source-level change; renaming the marker would invalidate stamps. */ readonly postcondition?: Gate; /** * @deprecated Renamed to {@link SkillSpec.postcondition}. Removed next * major. Measured 2026-08-21: 0 of 46 specs in the consuming knowledge base * set this field, so the window costs nothing and closes the collision above. */ readonly result?: Gate; /** * The skill's typed railway outcome — the SAME `Result` contract a * subagent declares with `result(okShape, errShape)`. Valid ONLY with * `context: "fork"`: a forked skill runs as a subagent, so it has the * call→return boundary a typed outcome needs (compile errors if `output` is set * without `context: "fork"`). When valid, compiles to a `## Output contract` * with a `vigiles:ok` / `vigiles:err` block — parseable (`parseAgentResult`) and * testable (`assertAgentOk`) via the existing subagent rail. An INLINE skill has * no return, so a typed outcome there is a category error — hence the gate. See * `research/spec-syntax-and-railway-scope.md`. */ readonly output?: OutputContract; /** Freeform instruction body (linear/unstructured skills). Use this OR `steps`. */ readonly body?: string | InstructionFragment[]; /** * Max lines for an inline fenced code block before compilation errors, * forcing the script into a file referenced via `file()` (default 20). * Keeps big scripts out of the skill body (token budget + progressive * disclosure). Set 0 to disable. */ readonly maxInlineCodeLines?: number; } /** * The input to `skill()` — `SkillSpec` minus `_specType`, with `tools` * constrained by the declared `purity` and vocabulary `V` (same mechanism as * `AgentSpecInput`). The open default vocabulary leaves it unconstrained. */ export type SkillSpecInput

= Omit & { readonly purity?: P; readonly tools?: readonly AllowedAt[]; }; /** * Define a SKILL.md specification. * * // skills/my-skill/SKILL.md.spec.ts * export default experimental_skill({ name: "my-skill", description: "…" }); * * Generic over a tool `Vocabulary` (default `OpenToolVocabulary` — no * constraint), exactly like `experimental_agent()`: a vocabulary-bound `experimental_skill` * (e.g. `vigiles/claude-code`) makes `purity: "pure"` + a side-effecting tool a * `tsc` error; the bare core one accepts any tools, as before. * * The name also disambiguates: `vigiles/testing` exports a `skill()` that is a * DIFFERENT function — a `Check` taking an id string, asking whether a * skill fired. This one authors a skill; that one observes one. * * Its helper vocabulary hangs off it — `experimental_skill.input(…)` and * `experimental_skill.step(…)` — rather than being exported beside it. Both are * used ONLY by skill specs (measured: zero uses in agent/claude specs, against * `cmd`/`file`/`ref`/`result`, which are shared and therefore stay top-level). * Hanging them here makes the experimental marking STRUCTURAL for the whole * family: you cannot reach `input()` without naming `experimental_skill` first. * The prefix convention alone could not do that — it is a habit, and it had * already leaked once when `skill()` shipped stable-named against its own docs. * * Honest limit: `const { input } = experimental_skill` strips the marker again * inside one file. What the shape actually guarantees is narrower and still * worth having — an unmarked name never crosses the package boundary. * * `Object.assign` rather than `export namespace`: the latter is banned by this * repo's own lint (`no-namespace: error`, inherited from strict-type-checked). * * @experimental */ declare function skillSpec(spec: SkillSpecInput): SkillSpec; /** * @experimental */ export declare const experimental_skill: typeof skillSpec & { input: typeof input; step: typeof step; }; /** * A subagent definition (compiles to `agents/.md`). Unlike a skill — * reference material the model reads on activation — a subagent is a *delegated * worker with a contract*: a dispatch `description`, an allowed-`tools` rail, an * optional `model`, a system-prompt `body`, and the `rules` it must follow. That * tool contract + those rules are the "railway" a subagent runs on, and they're * exactly the compile-time-verifiable surface vigiles owns: the body's * `file()`/`cmd()`/`symbol()` marks are checked like any instruction file, and * the tools list is verified against the real tool set. */ export interface AgentSpec { readonly _specType: "agent"; /** Subagent name (frontmatter + dispatch handle). */ readonly name: string; /** When to dispatch this subagent — the trigger (frontmatter). */ readonly description: string; /** Model alias (e.g. "sonnet", "opus", "haiku", "inherit"). Optional. */ readonly model?: string; /** Subagent UI colour (Claude Code frontmatter, e.g. "pink", "blue"). Optional. */ readonly color?: string; /** * The allowed-tools contract — the rails the worker runs on. Each entry must be * a known built-in tool (Read/Write/Edit/Bash/Grep/Glob/WebSearch/WebFetch/ * NotebookEdit/TodoWrite/Task/Skill) or an MCP tool (`mcp__server__tool`). * Omit to inherit all tools. Verified at compile time. */ readonly tools?: readonly string[]; /** * The DENY-side contract — tools the worker may NOT use. Use this INSTEAD OF * `tools`, not with it: `tools` is an allowlist (only these), so a tool not * listed is already unavailable and a `disallowedTools` entry would be * redundant. `disallowedTools` earns its place only when there's NO allowlist * (the agent inherits ALL tools) and you want to subtract a few — e.g. * `disallowedTools: ["Bash"]` on an otherwise-unrestricted worker. Rendered to * the `disallowedTools:` frontmatter; close-typos are flagged (a typo'd entry * blocks nothing). For a read-only floor prefer a tight `tools` list + `purity`. */ readonly disallowedTools?: readonly string[]; /** * The lead/intro prose of the system prompt (the "You are…" opener), before any * sections. Carries verified `file()`/`cmd()`/`symbol()`/`ref()` marks. No * markdown headers — use `sections` for those. */ readonly body?: string | InstructionFragment[]; /** * Named `##` sections of the system prompt (e.g. Purpose, Core Principles, * Capabilities) — the shape real subagents actually take. Same verified-ref + * no-nested-`##` rules as a CLAUDE.md spec's sections. Use `body` for the intro * and `sections` for the structured rest. */ readonly sections?: Record; /** Rules the worker must follow — rendered as a `## Rules` section. */ readonly rules?: Record; /** * The typed result contract — what this worker returns on success/error. When * set, compiles to an `## Output contract` section instructing the worker to * end with a `vigiles:ok` / `vigiles:err` block, so its outcome is parseable * and testable (see `result()`, `parseAgentResult`, `assertAgentOk`). */ readonly output?: OutputContract; /** * Declare this agent's purity floor — compile rejects a tool contract looser * than it. `"pure"` allows only read-only tools; `"bounded"` also allows * decidable side-effecting tools (Write, Edit, …) but bars `Bash` / * unknown-effect / inherits-all; `"dangerously-unrestricted"` (or omitting it) * enforces nothing. `"pure"`/`"bounded"` require an explicit `tools` list — a * wildcard or absent-tools (inherits-all) is always a violation. */ readonly purity?: AuthoredPurity; } /** * The input to `experimental_agent()` — `AgentSpec` minus the internal `_specType`, with the * `tools` list constrained by the declared `purity` and the tool vocabulary `V`. * `P` is inferred from the literal `purity` field (`const` inference), and * `tools` is then typed `AllowedAt[]`: * - `purity: "pure"` → `tools` may list only `V["readOnly"]` tools. * - `purity: "bounded"`→ `tools` may list `V["bounded"]` tools (admits `Bash`). * - no `purity` / `"dangerously-unrestricted"` → `tools` is `string[]` (open). * * With the open default vocabulary (core `experimental_agent()`) every level widens to * `string`, so any tools compile — backwards-compatible. */ export type AgentSpecInput

= Omit & { readonly purity?: P; readonly tools?: readonly AllowedAt[]; /** The typed result contract — `result(ok, err)`. Its literal field shapes are * captured into the returned `TypedAgentSpec` so a typed `pipe` can check them. */ readonly output?: OutputContract; }; declare const __outcome: unique symbol; /** Phantom carrier of an agent's success/error result shapes (type-level only). */ export interface TypedOutcome { readonly [__outcome]: { readonly ok: Ok; readonly err: Err; }; } /** An `AgentSpec` that REMEMBERS its `result()` ok/err shapes at the type level * (via a phantom field). Still an `AgentSpec`, so it flows everywhere one does. * The input to a typed `pipe` / `then`. */ export type TypedAgentSpec = AgentSpec & TypedOutcome; /** * Extract a typed agent's success (`result().ok`) SHAPE at the type level. The * phantom `__outcome` symbol is module-private, so this is the exported reader * the whole-harness registry uses: `OkOf` is the * literal `ok` shape `planner` produces, the producer side of a cross-file * `Handoff<>` check. A plain `AgentSpec` (no `result()` contract) carries no * phantom, so `OkOf` widens to the erased `Shape` — a no-op handoff, additive. */ export type OkOf = T extends TypedOutcome ? Ok : Shape; /** * Define a subagent specification (compiles to `agents/.md`). * * // agents/reviewer.md.spec.ts * export default experimental_agent({ * name: "reviewer", * description: "Review a diff for correctness. Dispatch PROACTIVELY after edits.", * model: "sonnet", * tools: ["Read", "Grep", "Bash"], * body: prose`Review the diff. Run ${cmd("npm test")} first.`, * rules: { * "no-floating": enforce("@typescript-eslint/no-floating-promises", "Await promises."), * }, * }); * * Generic over a tool `Vocabulary` (default `OpenToolVocabulary` — no * constraint). A harness adapter re-exports a vocabulary-bound `agent` (e.g. * `vigiles/claude-code`) so `purity: "pure"` + a side-effecting tool is a `tsc` * error at edit time; the bare core `experimental_agent()` accepts any tools, as before. * * Also generic over the result's `Ok`/`Err` shapes, inferred from `output: * result(...)`. The returned value is a `TypedAgentSpec` — an * `AgentSpec` that carries those shapes at the type level, so a typed `pipe` * can cross-reference the handoff. With no `output` the shapes default to the * erased `Shape`, and the value is still a plain `AgentSpec` — backwards-compatible. * * @experimental The SHAPE is not settled — the author is unsure of the design, * which is exactly what this marker promises: the form may change. It is NOT a * claim that the surface is unproven. Measured 2026-06-20: real Claude Code * loaded a compiled `agents/code-reviewer.md`, dispatched to it and read it, * 100% of trials — stronger end-to-end evidence than `experimental_skill` has. */ declare function agentSpec(spec: AgentSpecInput): TypedAgentSpec; /** * The field types a result contract can declare (kept tiny + dependency-free). * * The literal-array member is an ENUM: `["CUT", "MERGE", "KEEP"] as const` declares a * field whose value must be one of those strings. It is the ONLY extension to this union, * and it was added because a measured failure had no other cure: across 14 real payloads * a `verdict: "string"` field held 3 mutually incomparable invented categories over 3 * runs, and vocabulary compliance was 3/19 — 16%. `string` cannot express "one of these", * so nothing downstream could notice. * * Nothing RELATIONAL follows it — no `object[]`, no tuples, no per-element enums. * Declaring one throws rather than rendering an unsatisfiable schema, because the body of * a result is prose by decision: on those same 14 payloads, 23 scalar values carried * every assertion anyone made while 80,981 characters of prose carried none. */ export type OutputFieldType = "string" | "number" | "boolean" | "string[]" | readonly [string, ...string[]]; /** A field SHAPE — a record of field-name → field-type, kept in the TYPE so a * typed pipeline can cross-reference one agent's `ok` against the next agent's * `needs`. The erased runtime form is `Record`. */ export type Shape = Readonly>; /** * A subagent's typed result contract: the shape it must return on success * (`ok`) and on failure (`err`). Rich on both tracks — an error is structured * detail, not a bare pass/fail bit. Compiles into the worker's system prompt * (the `vigiles:ok` / `vigiles:err` block it must emit) and is the schema the * `parseAgentResult` parser + the `assertAgentOk/Err` test helpers validate. * * Generic over its `ok`/`err` field shapes so a typed value REMEMBERS them at * the type level (the basis of typed composition — see `pipe`). The default * type parameters widen to the historical erased `Shape`, so an `OutputContract` * named with no arguments behaves exactly as before — backwards-compatible. */ export interface OutputContract { readonly _ref: "output"; readonly ok: Ok; readonly err: Err; } /** * Declare a subagent's success/error result contract. * * result( * { files: "string[]", summary: "string" }, // rich success * { reason: "string", retryable: "boolean" }, // rich error * ) * * (Distinct from a skill's `result:` postcondition gate — this types a * subagent's *return value*, the success/error tracks of the railway.) * * The literal field shapes are PRESERVED in the return type (`const` inference), * not erased to `Record` — this is what lets `pipe` * cross-reference one agent's `ok` against the next agent's needs at `tsc` time. * The return is still an `OutputContract`, so every existing consumer (the * `output:` field, `renderOutputContract`, `parseAgentResult`) is unchanged. */ declare function result(ok: Ok, err: Err): OutputContract; /** One step on a railway: dispatch a flat subagent (the "activity"). */ export interface RailwayStep { readonly _step: "delegate"; /** The subagent to dispatch — resolved against compiled agent names. */ readonly agent: string; /** Optional task hint passed to the worker. */ readonly task?: string; /** * Optional input contract the step reads from its predecessor's `result().ok`. * When present, the whole-harness registry (`generate-harness`) emits a * per-edge `Handoff<>` assertion so a CROSS-FILE handoff mismatch (a missing * field or wrong type vs the prior step's `ok`) is a `tsc` error naming the * field. Absent `needs` = no handoff check (today's behavior, the string-path * backstop). Built by `needs(...)`, the same builder a typed `pipeStep` uses. */ readonly needs?: Shape; } /** * Build a railway step that dispatches `agent`. * * delegate("planner") // no task, no handoff * delegate("implementer", "implement the plan") // task hint only * delegate("reviewer", undefined, needs({ diff: "string" })) // + handoff check * * The optional 3rd argument carries the step's input `needs` (built by * `needs(...)`). When present, the whole-harness registry asserts that the * PREVIOUS success-track step's `result().ok` SUPPLIES it — a cross-file * handoff that doesn't line up is a `tsc` error naming the offending field. * Omitting it (the historical 1-/2-arg call) keeps the exact string-path * behavior — fully backwards-compatible. */ declare function delegate(agent: string, task?: string, needsContract?: Shape): RailwayStep; /** * A railway over flat subagents. `steps` run in order on the success track; the * first step that returns an error short-circuits to `onError`. `recover` * optionally retries the failing step a *bounded* number of times before the * error track. There is intentionally no loop combinator — the value is a finite * tree, so it always terminates and is fully verifiable at compile time. */ export interface Railway { readonly _specType: "railway"; readonly name: string; readonly steps: readonly RailwayStep[]; /** Error track — runs with the failing step's error payload. */ readonly onError?: RailwayStep; /** Bounded recovery: retry the failing step up to `max` times (finite). */ readonly recover?: { readonly step: RailwayStep; readonly max: number; }; } /** * Compose flat subagents into a railway (compiles to an orchestrator command). * * railway({ * name: "ship", * steps: [delegate("planner"), delegate("coder"), delegate("reviewer")], * onError: delegate("reporter"), * recover: { step: delegate("fixer"), max: 2 }, * }) */ declare function railway(spec: Omit): Railway; /** * A subagent's INPUT contract — the fields it reads from the prior step's `ok`. * Declared via `needs(...)` and threaded into the typed agent so `pipe` can * cross-reference it. Independent of `result()` (the output) — an agent both * `needs` an input shape and produces an `ok`/`err` output shape. * * @internal Experimental typed-composition surface — NOT part of the frozen * public API (pre-1.0); may change without a major bump. */ export type NeedsContract = N; /** * Declare the input fields a step reads from its predecessor's success payload. * Pass it as `needs:` on a typed pipeline step. `needs({})` (the default) is a * step with no upstream requirement — valid as the FIRST step of a pipeline. * * needs({ plan: "string", files: "string[]" }) * * @experimental Experimental typed-composition surface — NOT part of the frozen * public API (pre-1.0); may change without a major bump. */ declare function experimental_needs(shape: N): NeedsContract; /** * A typed pipeline step: a `TypedAgentSpec` paired with the input `needs` it * reads from the prior step's `ok`. `step()` builds one; `pipe` checks that the * prior step's `ok` shape supplies this step's `needs`. * * @internal Experimental typed-composition surface — NOT part of the frozen * public API (pre-1.0); may change without a major bump. */ export interface PipeStep { readonly _step: "typed-delegate"; readonly agent: TypedAgentSpec; readonly needs: Needs; } /** * Pair a typed agent with the input it `needs` from the previous step. The first * argument is an `experimental_agent()` VALUE (which carries its `result()` shape); the * second is the `needs(...)` input contract. * * pipeStep(implementer, needs({ plan: "string", files: "string[]" })) * * @experimental Experimental typed-composition surface — NOT part of the frozen * public API (pre-1.0); may change without a major bump. */ declare function experimental_pipeStep(a: TypedAgentSpec, needsContract?: Needs): PipeStep; /** * True iff `Producer` provides EVERY field `Consumer` needs, with matching * field types. When satisfiable it is `true`; otherwise it collapses to a * descriptive error object naming the offending field (`__missing` / * `__mismatch`), which surfaces at the mismatched call. Shallow (a per-field * mapped type, not a recursion) to avoid TS2589. * * @internal Experimental typed-composition surface — NOT part of the frozen * public API (pre-1.0); may change without a major bump. */ export type Supplies = { [K in keyof Consumer]: K extends keyof Producer ? Producer[K] extends Consumer[K] ? true : { readonly __mismatch: K; readonly expected: Consumer[K]; readonly got: Producer[K]; } : { readonly __missing: K; readonly required: Consumer[K]; }; }[keyof Consumer]; /** * Per-edge CROSS-FILE handoff check — the registry-scale form of `Supplies<>`, * mirroring `KnownAgentName` (the dangling-delegate per-edge check). `Producer` * is the prior success-track step's `result().ok` shape (read off the registry * via `OkOf`); `Consumer` is THIS step's `needs(...)` input contract. Collapses * to `true` when the producer supplies every field the consumer needs (matching * types), else to a descriptive error object naming the offending field * (`__handoff_error` wrapping `Supplies`'s `__missing`/`__mismatch`), so * assigning `true` to it is a `tsc` error at edit time. Shallow (one wrap over * the per-field `Supplies` mapped type, no recursion); the generator emits one * assertion per consecutive step pair (O(N)), keeping clear of TS2589. * * @internal Experimental typed-composition surface — NOT part of the frozen * public API (pre-1.0); may change without a major bump. */ export type Handoff = Supplies extends true ? true : { readonly __handoff_error: Supplies; }; /** A typed pipeline value — carries the LAST step's `ok` and the UNION of every * step's `err` (any step can short-circuit to the error track). * @internal Experimental typed-composition surface — NOT part of the frozen * public API (pre-1.0); may change without a major bump. */ export interface Pipeline { readonly _specType: "pipeline"; /** Ordered agent names — the resolved compose order. */ readonly agents: readonly string[]; /** The final step's success shape. */ readonly ok: Ok; /** The union of every step's error shape. */ readonly err: Err; /** The underlying string-path railway, for `compileRailway` reuse. */ readonly railway: Railway; } /** * Begin a typed pipeline from its first step. The first step has no upstream, so * its `needs` must be empty (`needs({})` or omitted). Returns a `Pipeline` * carrying that step's `ok`/`err` forward. * * @experimental Experimental typed-composition surface — NOT part of the frozen * public API (pre-1.0); may change without a major bump. */ declare function experimental_start(first: PipeStep, Ok, Err> | TypedAgentSpec): Pipeline; /** * Append a step to a typed pipeline. The handoff is CHECKED: the constraint * `Supplies` must be `true`, else the `next` parameter's type * collapses to a `__HANDOFF_ERROR` object and `tsc` rejects the call, naming the * missing/mismatched field. Carries the new step's `ok` forward and accumulates * the error track. Shallow per-call check — no recursive chain type. * * Named `andThen` (Wlaschin's railway `bind`/`andThen`), NOT `then`: a module * exporting a function called `then` becomes a thenable, so `await import()` of * any barrel re-exporting it would invoke it — a footgun the rename avoids. * * @experimental Experimental typed-composition surface — NOT part of the frozen * public API (pre-1.0); may change without a major bump. */ declare function experimental_andThen(prior: Pipeline, next: Supplies extends true ? PipeStep : { readonly __HANDOFF_ERROR: Supplies; }): Pipeline; /** * Compose a typed pipeline in one call — the ergonomic form of * `andThen(andThen(start(a), b), c)`. Each adjacent handoff is checked * left-to-right: the FIRST step is the producer, the rest are * `pipeStep(agent, needs(...))` consumers, and the compiler rejects the whole * expression if ANY handoff's producer `ok` does not supply the consumer's * `needs` (variadic chains hit TS2589 quickly, so `pipe` is a fixed set of * overloads over the shallow `start`/`andThen` fold rather than a recursive * variadic type — keep chains to a handful of steps; for longer ones, fold * `andThen` explicitly). * * pipe( * planner, // produces ok * pipeStep(implementer, needs({ plan: "string", files: "string[]" })), * pipeStep(reviewer, needs({ diff: "string" })), * ) // ← won't compile if a handoff doesn't line up * * @experimental Experimental typed-composition surface — NOT part of the frozen * public API (pre-1.0); may change without a major bump. */ declare function experimental_pipe(a: TypedAgentSpec): Pipeline; declare function experimental_pipe(a: TypedAgentSpec, b: Supplies extends true ? PipeStep : { readonly __HANDOFF_ERROR: Supplies; }): Pipeline; declare function experimental_pipe(a: TypedAgentSpec, b: Supplies extends true ? PipeStep : { readonly __HANDOFF_ERROR: Supplies; }, c: Supplies extends true ? PipeStep : { readonly __HANDOFF_ERROR: Supplies; }): Pipeline; declare function experimental_pipe(a: TypedAgentSpec, b: Supplies extends true ? PipeStep : { readonly __HANDOFF_ERROR: Supplies; }, c: Supplies extends true ? PipeStep : { readonly __HANDOFF_ERROR: Supplies; }, d: Supplies extends true ? PipeStep : { readonly __HANDOFF_ERROR: Supplies; }): Pipeline; /** * Per-edge dangling-`delegate` check. `Target` is a delegate target NAME (a * string literal the generator reads off a `railway()` value); `Names` is the * literal union of every agent name in the harness (emitted by the generator). * Collapses to `true` when the target resolves, else to a descriptive error * object naming the dangling target + the railway it came from — so assigning * `true` to it is a `tsc` error at edit time. Shallow (one conditional, no * recursion); the generator emits one assertion per edge (O(N)). * * @internal Experimental whole-harness-codegen surface — NOT part of the frozen * public API (pre-1.0); may change without a major bump. */ export type KnownAgentName = [Target] extends [Names] ? true : { readonly __dangling_delegate: Target; readonly from: From; }; /** Derive the spec filename from an output filename. */ export type SpecPath = `${Output}.spec.ts`; /** Extract the output filename from a spec filename. */ export type OutputPath = Spec extends `${infer Base}.spec.ts` ? Base : never; declare const __stage: unique symbol; /** A spec that hasn't been validated yet. */ export type RawSpec = T & { readonly [__stage]: "raw"; }; /** A spec whose file/cmd/ref references have been validated. */ export type RefsValidated = T & { readonly [__stage]: "refs-validated"; }; /** A spec whose linter rules have been cross-referenced. */ export type LintersVerified = T & { readonly [__stage]: "linters-verified"; }; /** A fully validated spec, ready for markdown emission. */ export type ReadyToEmit = T & { readonly [__stage]: "ready"; }; /** Per-linter verification mode. */ export type LinterMode = boolean | "catalog-only"; export interface VigilesV2Config { /** Glob pattern to discover spec files. Default: "**\/*.spec.ts" */ readonly specs?: string; /** Auto-discover linter rules for coverage reporting. */ readonly discover?: boolean; /** Maximum rules per spec file. */ readonly maxRules?: number; /** Maximum estimated tokens for compiled output. ~4 chars per token. */ readonly maxTokens?: number; /** Maximum lines per prose section. Forces splitting into named sections. */ readonly maxSectionLines?: number; /** Global kill switch: skip ALL linter verification during compile. */ readonly verifyLinters?: boolean; /** Per-linter verification mode: true (full), "catalog-only", or false (skip). */ readonly linters?: Record; } export declare function defineConfig(config: VigilesV2Config): VigilesV2Config; /** * @deprecated Renamed to {@link instructionFile}. The builder compiles to * `CLAUDE.md` **and** `AGENTS.md` (see `InstructionTarget`), so a name taken from * one of the two harnesses was never right. Removed one major AFTER the one that introduces it. */ export declare const claude: typeof instructionFile; /** * @deprecated Renamed to {@link prose}. It builds a prose FRAGMENT with typed * refs; the plural read as "the instruction file", which is what * {@link instructionFile} builds. Removed one major AFTER the one that introduces it. */ export declare const instructions: typeof prose; /** * Define a subagent — and the ROOT of the subagent vocabulary. * * Everything that only makes sense for subagents hangs off this one symbol: * `experimental_agent.result()` (the outcome contract), `.railway()` / `.delegate()` * (the flat orchestrator), and `.pipe()` / `.start()` / `.andThen()` / `.pipeStep()` / * `.needs()` (the typed pipeline). Same chokepoint as `experimental_skill.input()`. * * WHY a root and not a prefix on each name: `railway()` and `delegate()` are * meaningless without a subagent, yet they shipped under STABLE names while the * builder they depend on is experimental — a stable name resting on an unstable * one. Prefixing each would carry the warning but multiply the vocabulary; a * namespace OBJECT was rejected for the reason the `vigiles/experimental` subpath * was retired in #169 — it marks the import line, out of view by the time anyone * reads the call. A member reached through the marked root cannot be destructured * free of its warning without renaming it, so the warning rides every call site. * * The rule this establishes: ONE experimental root per feature, everything else a * member of it, TYPES excluded (a type annotation is not a call site). * * @experimental */ export declare const experimental_agent: typeof agentSpec & { result: typeof result; delegate: typeof delegate; railway: typeof railway; needs: typeof experimental_needs; pipeStep: typeof experimental_pipeStep; start: typeof experimental_start; andThen: typeof experimental_andThen; pipe: typeof experimental_pipe; }; /** * @deprecated Renamed to {@link experimental_agent} — the shape is not settled. * Removed one major AFTER the one that introduces it. * * @experimental * vigiles:experimental-name-ok this IS the old spelling — prefixing a deprecated * alias would defeat the alias, which exists precisely so code written against * the unprefixed name keeps compiling for one major. It carries the tag because * it is the same function, and the tag is what the deprecation notice points at. */ export declare const agent: typeof agentSpec & { result: typeof result; delegate: typeof delegate; railway: typeof railway; needs: typeof experimental_needs; pipeStep: typeof experimental_pipeStep; start: typeof experimental_start; andThen: typeof experimental_andThen; pipe: typeof experimental_pipe; }; export {}; //# sourceMappingURL=spec.d.ts.map