/** * Pure decision logic for `vigiles init`. * * Turns CLI args + whether a human's at a TTY into a concrete setup PLAN, so the * IO-heavy `setup()` in cli.ts stays a thin shell and every choice is unit-tested. * Best-practice onboarding: **interactive** when a human runs it in a terminal, * **non-interactive** (sensible defaults) for agents / CI / piped input — so * "set up vigiles" from a Claude Code or Codex prompt Just Works without hanging * on a prompt. See docs/agent-setup.md. */ import type { InitPolicy } from "./core/rule.js"; /** What `vigiles init` will set up. */ export interface SetupPlan { /** Lint pillar — verify instruction-file references (specs, types, compile, lint, hooks). */ lint: boolean; /** Test pillar — test the harness (scaffold a starter harness test + CI job). */ test: boolean; /** Wire CI (the `zernie/vigiles@v1` Action; creates a workflow if none). */ gha: boolean; /** * Install vigiles's skills + hooks for the agent: the skills LINKED into the * repo (every clone), the Claude Code hooks via the per-machine plugin. */ plugin: boolean; /** * Adopt/scaffold the instruction file(s) into typed `.spec.ts` (the compiled * `CLAUDE.md` + integrity hash). GATE-FIRST (see `gate-first-adoption`): the * INTEGRITY GATE (structural rules on your raw files + CI + devDep) is the * universal floor and needs NO spec — the `structural` rule group reads * skills/agents/hooks as-is, and a setup with this flag off writes * `require-instructions-spec: "off"` (`mergeProjectConfig`'s `specs`). So * scaffolding a spec (extra maintenance + a compiled artifact) is the INVITED * layer, gated on this flag (`setupPillar1` runs iff `lint && scaffoldSpecs`). * Default: TRACKS the lint pillar (`resolvePlan` sets it to `plan.lint`), so bare * `init` / `--lint` scaffold as before — non-interactive behaviour is unchanged. * The ONE thing that turns it off with lint on is the wizard's "gate" choice: a * pure lint gate on your raw files, nothing installed, no spec. No new CLI flag — * a non-interactive gate is a documented follow-up (research/adoption-design.md §1). */ scaffoldSpecs: boolean; /** Strict rule severities in `.vigilesrc.json`. */ strict: boolean; /** Rewrite an existing STALE CI workflow in place (instead of only warning). */ force: boolean; } /** The explicit choices a user pinned via flags (undefined = "not specified"). */ export interface ParsedSetupArgs { target?: string; strict: boolean; /** `--report-only` — write the gating rules at "warn" (nothing fails CI). The * orthogonal severity dial; composes with `--strict` (which rules) by setting * their severity. */ reportOnly: boolean; yes: boolean; /** `--force` — rewrite a stale CI workflow in place. */ force: boolean; /** Lint pillar — `--lint` → true, `--no-lint` → false, absent → undefined. */ lint?: boolean; /** Test pillar — `--test` → true, `--no-test` → false, absent → undefined. */ test?: boolean; /** `--harness=claude,codex` override (empty = auto-detect). */ harness?: string; gha?: boolean; plugin?: boolean; /** * `--ci-only` — the explicit CI-gate-only opt-in: the non-interactive equal of * the wizard's "gate" choice (the lint gate in CI + devDep, no plugin, no spec, * no test). Lets an agent/CI reach it without a TTY. Full stays the DEFAULT (bare * `init` is unchanged), so this is opt-IN — it never buries the richer layers * behind a default flip. (Named for what it sets up — the CI check — not the * internal "integrity gate" concept.) * * THE DISCOVERY FAILURE THIS FLAG IS THE WORKED EXAMPLE OF: it shipped in the * CLI and was invisible on the README, the site, the recommended agent prompt * and the internal doc, so an agent taking the full default never learned it * existed. Working code is not a delivered capability — a flag nobody can FIND * is not done. The fix was a pointer in the non-interactive summary, `--help`, * docs/agent-setup.md and the README; the standing rule it produced is the * DISCOVERY row of `cohesive-feature-delivery`. */ ciOnly: boolean; /** * `--full` — run the whole setup even on a repo that already uses vigiles. * Without it, a re-run there is the minimal one (see {@link rerunScope}). */ full: boolean; } /** Parse `init` args into the choices the user pinned. The two pillars are * selected with `--lint` / `--test`. */ export declare function parseSetupArgs(args: readonly string[]): ParsedSetupArgs; /** The non-interactive defaults: both pillars, CI, and the plugin. `scaffoldSpecs` * tracks the lint pillar (specs are created when the lint layer is set up) — the * ONE exception is the wizard's "gate" choice, which sets up the lint GATE on your * raw files WITHOUT scaffolding a spec (gate-first-adoption). Non-interactive * behaviour is unchanged: bare `init` (lint on) scaffolds as before. */ export declare function defaultPlan(strict?: boolean): SetupPlan; /** * Pure config-merge for what `vigiles init` writes to `.vigilesrc.json`: record * the harnesses if absent, gate the structural rules by EXTENDING the * `vigiles:recommended` preset (never by writing each rule), add the workflow * rules on `--strict`, and NEVER clobber an existing key or severity. Returns * the merged config, or `null` when nothing changed (so the IO layer skips the * write). The IO (read/parse/write + the malformed-file guard) stays in cli.ts. * * 🔴 THE PRESET, NOT NINE LINES (#338). Written lines are a snapshot of the * structural group on the day `init` ran, so a repo adopted earlier never got a * rule added to the group later; the preset is resolved by the loader on every * run (`src/core/presets.ts`). * * `--report-only` is the exception and stays EXPLICIT: it promises that nothing * fails CI, and extending a preset that later gains a rule at `error` would * break that promise on an upgrade. So it writes the same rules at `warn`. */ export declare function mergeProjectConfig(existing: Readonly>, opts: Readonly<{ /** What `init` writes, derived from the rule groups by the root. */ policy: InitPolicy; /** Canonical harness names this repo targets, in the order to declare them. */ harnesses: readonly string[]; strict: boolean; reportOnly?: boolean; /** Whether the LINT pillar is on (default true). The rule gate is a lint-layer * concern, so a test-only setup (`init --test` / `--no-lint`) records the * harness but writes NO lint rules. */ lint?: boolean; /** Whether this setup scaffolds specs (default true). `false` is the * gate-only choice (`--ci-only`, the wizard's "gate"): it declines specs, * so `require-instructions-spec` is written `off` rather than left to the * preset's `error` to fail the first `lint`. */ specs?: boolean; }>): Record | null; /** * Whether to drop into interactive prompts: a human at a TTY who passed neither * `--yes` nor an explicit `--target`, and who hasn't already pinned every choice * via flags. Agents / CI / piped input (no TTY) never prompt. */ export declare function shouldPrompt(parsed: ParsedSetupArgs, isTTY: boolean): boolean; /** Interactive answers (only the fields the prompts cover). */ export type SetupAnswers = Partial>; /** Ask one question with a default — injected so the interactive Q&A is pure + * unit-testable (a fake `ask` scripts answers; no TTY, no readline). */ export type AskFn = (question: string, def: string) => Promise; /** * The interactive setup Q&A as PURE logic over an injected `ask` — the prompts, * their defaults, and the answer→`SetupAnswers` mapping. The IO shell (readline) * lives in `cli.ts`'s `promptSetup`, which just supplies a real `ask`. Keeping * this here means the fragile interactive path is unit-tested deterministically * (the questions can't silently break) without a terminal. */ export declare function collectSetupAnswers(ask: AskFn): Promise; /** * The NON-EVIL invitation to graduate a gate-only setup to the full layer (skills * + typed specs) — `gate-first-adoption`'s "invite the rest". Pure: returns the * one-line invitation when the setup landed as a pure gate (no plugin, no specs), * else null. It's INFORMATIONAL — a printed line, never a second prompt (the TTY * wizard already asked; a headless run must never hang), and declining costs * nothing (the gate is fully functional). The IO (printing it, and the optional * later reminder on `audit`/`lint`) lives in cli.ts. */ export declare function gateOnlyInvitation(plan: SetupPlan): string | null; /** * How to install vigiles's skills/hooks for ONE harness — the deterministic * decision behind the IO in cli.ts, so a CI test asserts WHICH commands an * install runs without a network call or a real `claude`/`codex` binary. * * The method is genuinely harness-specific: Claude Code has a GLOBAL plugin * marketplace (installs to ~/.claude/plugins/, nothing in the repo); Codex has * no global store — its config is repo-committed (`.codex/`, AGENTS.md), so its * instructions are read directly and skills are an opt-in, repo-local concern. */ export interface InstallPlan { harness: string; /** Shell commands to auto-run (empty = nothing runnable here). */ commands: string[]; /** One-line success message printed after the commands run. */ successMessage: string; /** The equivalent commands a user runs by hand (printed on failure / no-CLI). */ manualSteps: string[]; /** Always-printed informational lines (where it installs, caveats). */ notes: string[]; /** Whether this method writes files into the consumer's repo (vendoring). */ vendors: boolean; /** * Link the package's shipped skills into the repo's skills home * (`src/skill-links.ts`). True only where the vendor documents following a * symlinked skill folder — a link the harness skips is a silent absence. */ linkSkills: boolean; /** * The global install carries HOOKS as well as skills, so linked skills do not * replace it (see {@link shouldRunGlobalInstall}). */ globalInstallCarriesHooks: boolean; } /** * The SHIPPED consumer skills (the `skills/` dir, published via `plugin.json`) — * the ONLY ones a user's install should get. The cross-agent `skills` CLI would * otherwise clone the whole repo and install EVERY skill dir it finds, including * this repo's CONTRIBUTOR-only `.claude/skills/` (generate-logo, landing-site, * pr-to-lint-rule, …) — internal tooling users never asked for. So the Codex * install is scoped with `-s`. Kept in lockstep with `skills/` on disk by the * dogfood test in `src/adapters/claude-code/skills-dogfood.test.ts`. */ export declare const SHIPPED_SKILLS: readonly ["adopt-spec", "debug-my-harness", "edit-spec", "linter-docs", "strengthen", "test-harness"]; /** * Per-harness install plan. `hasClaude` gates the auto-run `claude plugin` CLI * (else the same two steps are printed as `/plugin` slash commands). * * TWO CARRIERS, by what each one reaches. The SKILLS are linked into the repo * (`linkSkills`), so every clone and container has them after `npm install`. The * GLOBAL installs reach one machine: Claude Code's plugin (~/.claude/plugins/) * is still the only carrier of vigiles's Claude Code HOOKS, so it always runs; * Codex's `skills` CLI install (~/.agents/skills/) carries skills only, so it * runs only when the links could not be made (no `package.json` to install * into). Codex's nudge hooks are written into `.codex/config.toml` directly. * * THE CODEX GLOBAL COMMAND. The cross-agent `skills` CLI with `-g -y` installs * to the global store ~/.agents/skills/ (NOT the repo, and NOT ~/.codex/ — * verified against the real CLI). It is scoped to the SHIPPED skills with `-s` * — without it the `skills` CLI installs EVERY SKILL.md in the repo, leaking * the contributor-only .claude/skills/ into the user's global store * (#dogfood-I2; confirmed — the CLI's `-l` list shows 17 skills for this repo, * incl. .claude/skills). The `-s` parser is SPACE-separated (it consumes * consecutive non-dash args), NOT comma-separated — a comma list is read as one * literal skill name, matches nothing, and the install exits 1 (verified against * the real `skills@1.5.20` arg parser). */ export declare function planPluginInstall(harnesses: readonly string[], opts: { hasClaude: boolean; }): InstallPlan[]; /** * Run a plan's GLOBAL install, given whether the skills were linked into the * repo? Skip it only when linked skills make it redundant — when it carries * nothing but skills. Running it anyway would list every skill twice (Codex: * "If two skills share the same `name`, Codex doesn't merge them; both can * appear in skill selectors"). Claude Code's plugin carries the hooks, so it * always runs; its skills are namespaced `vigiles:` and load beside the * linked `` (a known, documented duplicate until the hooks move into the * repo too). */ export declare function shouldRunGlobalInstall(plan: Readonly>, skillsLinked: boolean): boolean; /** One vigiles-managed Codex hook: a `[[hooks.]]` entry. */ export interface CodexPluginHook { readonly event: string; /** Codex matcher (anchored regex, the dialect convention). */ readonly matcher: string; /** The shell command Codex runs (a direct `npx vigiles …`, no plugin root). */ readonly command: string; /** A unique command substring → idempotent re-merge (replaces in place). */ readonly key: string; } /** * vigiles's proactive nudges, wired into a Codex repo's `.codex/config.toml`. * * Codex has no global plugin store (unlike Claude Code's marketplace), so its * config is repo-committed — the idiomatic place for these. They run as DIRECT * `npx vigiles hook-runtime …` commands (NOT vendored bash scripts): the runtime * entrypoints read the event JSON on stdin and emit the `hookSpecificOutput. * additionalContext` shape Codex honors on `PostToolUse` (confirmed against the * official hooks docs + encoded in `HookProtocol.injectableEvents`). Safety: only * an INTENTIONAL `exit 2` blocks an edit (the refs nudge, when `unmarked-refs` is * `error`); an npx-resolution failure exits non-2, so a missing dep never blocks. * * Deliberately NOT here (a loud, documented deferral — no-silent-skips): the * SessionStart lint summary (CC delivers it as plain stdout, whose SessionStart * prepend is unconfirmed on Codex — vs the JSON `additionalContext` these use) and * the compile-on-edit / pre-edit-block guards (filename-gated bash with no * harness-neutral `hook-runtime` entrypoint yet). Those stay manual on Codex. */ export declare function codexPluginHooks(): CodexPluginHook[]; /** * Idempotently merge {@link codexPluginHooks} into a parsed `.codex/config.toml` * object. Pure — the IO (read/parse/serialize/write) stays in cli.ts's * `wireCodexHooks`. Each vigiles hook is keyed by a unique command substring, so * a re-run REPLACES its own entry in place and leaves the user's own Codex hooks * (and every other config key) untouched. Returns a new object. */ export declare function applyCodexPluginHooks(existing: Record): Record; /** * Whether this repo already uses vigiles, decided from what is ON DISK. * * 🔴 THE SIGNAL IS `.vigilesrc.json` OR A SPEC NEXT TO AN INSTRUCTION FILE, and * deliberately NOT a `vigiles` devDependency: the documented first step on a new * repo can be `npm i -D vigiles` followed by `npx vigiles init`, and that run * must still be the full setup. Every `init` writes `.vigilesrc.json` (it records * the harnesses even on a test-only run), and a hand-written one means the owner * has already configured vigiles. A spec beside `CLAUDE.md`/`AGENTS.md` covers a * repo adopted before the config file existed. */ export type AdoptionState = { readonly kind: "fresh"; } | { readonly kind: "adopted"; readonly evidence: string; }; /** {@link AdoptionState} from the two disk facts that decide it. */ export declare function adoptionState(facts: Readonly<{ hasConfig: boolean; specs: readonly string[]; }>): AdoptionState; /** * Which setup a run performs: the full one, or the minimal re-run (#338). * * MINIMAL only when the repo is adopted AND the run named nothing that asks for * more. A flag that selects or widens the setup (`--full`, `--strict`, * `--ci-only`, `--report-only`, `--force`, `--target=`, a positive `--lint` / * `--test`) is an explicit request and keeps today's behaviour; the narrowing * flags (`--no-*`, `--harness=`, `--yes`) do not, because they can only make a * run do less. */ export declare function rerunScope(parsed: Readonly, state: AdoptionState): "full" | "minimal"; /** * The `devDependencies` a minimal re-run writes, or null to leave package.json * alone. It ADDS vigiles when no dependency field declares it and never edits an * existing declaration: rewriting `^33.3.0` to `^33` loosens a pin the owner * chose, and moving it between fields is a decision, not a missing piece. */ export declare function missingVigilesDevDep(pkg: Readonly<{ dependencies?: Readonly>; devDependencies?: Readonly>; }>, spec: string): Readonly> | null; /** What a full setup would add on an adopted repo — the facts that decide it. */ export interface FullSetupGap { /** `.github/workflows/vigiles.yml` is absent (and CI wiring is not opted out). */ readonly workflow: boolean; /** `.vigilesrc.json`: absent, present without the preset, or already extending it. */ readonly config: "missing" | "no-preset" | "has-preset"; /** `vigiles.harness.mjs` is absent (and the test layer is not opted out). */ readonly harnessTest: boolean; /** Instruction files and surfaces a full setup would adopt into a spec. */ readonly unspecced: readonly string[]; /** `.vigiles/generated.d.ts` / `.vigiles/schema.json` would be created. */ readonly generated: boolean; /** The global plugin install / hook wiring a full setup would run, one line each. */ readonly installs: readonly string[]; /** The devDependency rewrite a full setup would make, if any. */ readonly devDep: Readonly<{ from: string; to: string; }> | null; } /** * The lines a minimal re-run prints about what it did NOT do: each thing a full * setup would add here, then the flag that does it. Empty when nothing is * missing, so a fully set-up repo is not told to run anything. */ export declare function fullSetupFollowUps(gap: FullSetupGap, /** The preset `init` writes (`InitPolicy.preset`). */ preset: string): readonly string[]; /** * Resolve the final plan: defaults, then flags, then interactive answers (each * layer overrides the previous only where it has an opinion). `--target` pins a * bare lint-pillar spec (no harness scaffold). */ export declare function resolvePlan(parsed: ParsedSetupArgs, answers?: SetupAnswers): SetupPlan; //# sourceMappingURL=setup-plan.d.ts.map