Files
zernie 900dc7e06a refactor(spec)!: name things for what they are; retire the vigiles/experimental subpath (#169)
Three exported symbols were named for something other than what they are, and
two mechanisms were doing one job.

RENAMES (old names kept one major, @deprecated, removed one major AFTER this one):
  claude()          → instructionFile()     compiles CLAUDE.md AND AGENTS.md, so a
                                            name from one harness was never right
  instructions``    → prose``               builds a prose FRAGMENT; the plural read
                                            as "the instruction file", which is what
                                            instructionFile() builds
  agent()           → experimental_agent()  the shape is not settled, and the tag was
                                            never there (git log -S empty)
  SkillSpec.result: → postcondition:        `result` already meant the subagent output
                                            contract one screen down; the doc comment
                                            had to spend a line disambiguating them

The alias window is not politeness. Measured: the previous rename shipped with
ZERO overlap (18.1.1 old names only, 19.0.0 new only). A container rollback put
the old node_modules under new sources, PreToolUse stopped loading, and a
PreToolUse that does not load denies EVERY Bash command — including the one that
fixes it. One window per major makes that pair unrepresentable.

SUBPATH RETIRED. vigiles/experimental is gone; its members moved to the door
their feature already uses. Of two mechanisms for one promise the subpath was the
weaker: it marks the import line, out of view by the time anyone reads the call.
Measured on the only user-facing example — with the prefix aliased away at
import, the marker survived at 0 of 5 call sites. So the prefix stays, the advice
to alias it away became an explicit prohibition, and the six define* entry points
are DECLARED prefixed rather than gaining it at re-export.

That de-aliasing is what unblocked moving the naming check into ESLint, and the
ORDER was load-bearing: measured before it, dropping the public/internal
exemption produced 14 findings, all from the re-export indirection. Measured
after, across all of src/ with no exemption: 22 tagged declarations, 0
unprefixed. The rule turns on silent.

EXPORT CLEANUP. vigiles/linting export * → curated, 103 → 79 symbols; its header
claimed to be curated with an `export *` one line below, and claimed the builders
were also at the package root (191 exports there, zero matches). ClaudeTool and
HookEvent deleted — zero references, and both had drifted off the dialect they
claimed to mirror; HookEvent listed 5 events, two of which do not exist, against
the 31 the vendor documents.

DOCS. experimental.md deleted and distributed per feature: the skill half into
skills.md §Status, the hook half into a new compiled-hooks.md §Status, and the
emit half — 111 of its 177 lines, a topic doc wearing a stability page's clothes
— into docs/emit-channel.md, which gains the "which channel when" comparison it
never had. The one cross-cutting sentence went to the README.

TWO GATES WERE GREEN AND WRONG, both found by this work:
  - api:check reported "verified for 11 entries — no drift" while reading
    dist/experimental.d.ts from a PREVIOUS build (tsc does not clean dist/) for a
    source file that no longer existed. Now asserted against package.json
    exports, with a mutation proving exit 1 on a phantom entry.
  - check-export-prefixes caught MY error: the R3 services first landed on
    vigiles/eval, where `paid_` means "this call can bill you" — and
    startServices takes a ContainerRuntime and calls no model. Naming it
    paid_experimental_startServices would have redefined paid_ as "expensive in
    some sense". They went to the package root.

SEVENTEEN review findings landed on this branch, all real. Two mechanical
closures came out of them: src/example-imports.test.ts resolves every relative
import under examples/ instead of matching a string, and the naming rule now
ENUMERATES the three export paths it visits rather than describing its coverage
in the abstract. One attempt is recorded as REFUTED: teaching the doc gate to
flag our own exports as free variables measured 66 findings across 93 blocks, so
that gate's original scoping was right.

BREAKING CHANGE: vigiles/experimental is removed. Its exports are unchanged in
name and behaviour and are now reached from `vigiles` (the emit channel, the R3
disposable-service tier). Every renamed symbol keeps a working @deprecated alias
for this major.
2026-08-21 07:54:51 +05:00

12 KiB

Agent setup & workflows

One command is all you need. npx vigiles init handles everything non-interactively — the agent runs it, the installed skills and hooks take over (auto-compiling specs, blocking stray edits, nudging when something needs attention), and there are no manual chores afterward. This guide covers what init does, the per-agent specifics, and the fallbacks.

→ Back to README

Contents

What init does

vigiles init scans your project, auto-detects which agents you already use, and sets both layers up. No --target flag needed unless you want to override.

Signal What it means
CLAUDE.md exists Claude Code in use — suggest migration if no spec
AGENTS.md exists Codex / GitHub Copilot in use
.claude/ directory Claude Code project config
.cursorrules Cursor in use — suggest rule-porter
.github/copilot-instructions.md GitHub Copilot custom instructions
.windsurfrules Windsurf in use
rule-porter / rulesync in package.json Sync tool already installed

What it sets up by default:

  • Lint layer — a typed .spec.ts + generated types
  • Test layer — a starter vigiles.harness.mjs
  • CI — a zernie/vigiles@v1 workflow at .github/workflows/vigiles.yml
  • Dependencyvigiles added to devDependencies
  • Plugin — the Claude Code plugin, installed globally via the marketplace (into ~/.claude/plugins/, never vendored into your repo)

Scope with flags: --lint / --test (one layer or both), --harness=claude,codex, --no-gha, --no-plugin, --strict.

Per-agent

Claude Code

Instruction file: CLAUDE.md. Once the plugin is installed, the agent no longer has to remember to compile:

Hook Trigger Action
PreToolUse Agent tries to Edit/Write a compiled .md file Blocks the edit, redirects to .spec.ts
PostToolUse Agent edits a .spec.ts file Auto-runs vigiles compile
PostToolUse Agent edits linter config or package.json Auto-runs vigiles generate types

init installs the plugin via the marketplace; by hand in a Claude Code session:

/plugin marketplace add zernie/vigiles
/plugin install vigiles@vigiles

⚠️ Without the plugin, run vigiles compile manually after editing specs. CI still catches stale files.

⚠️ npm install vigiles does NOT wire the skills. The npm tarball ships them (it doubles as the plugin payload), so they land in node_modules/vigiles/skills/ — a directory Claude Code never scans. Until the plugin install above has run, all six shipped skills, test-harness included, are present on disk and unselectable. vigiles audit says so out loud when it sees a repo in that state.

Checking by hand? Look in ~/.claude/plugins/installed_plugins.json for a vigiles@vigiles entry — that is what claude plugin install writes for a user-scope install. A repo's .claude/settings.json carries project-level enabledPlugins, and a correctly-installed user-scope plugin does not appear there; judging it from settings.json alone reports a working install as broken.

What init commits to your repo — and what it can't

init writes a declaration into your .claude/settings.json:

{
  "extraKnownMarketplaces": {
    "vigiles": { "source": { "source": "github", "repo": "zernie/vigiles" } }
  },
  "enabledPlugins": { "vigiles@vigiles": true }
}

It is merged, never overwritten — your hooks, permissions and other plugins are preserved, an existing vigiles entry is left alone (it may point at a fork), and an explicit "vigiles@vigiles": false is respected as a deliberate disable. Re-running init changes nothing.

What this does NOT do: install the plugin for anyone. Per the Claude Code team-marketplaces docs, as of CC v2.1.195:

A plugin that only the project's .claude/settings.json enables, and that comes from an external source such as a GitHub repository or npm package, doesn't load until the team member installs it. Until then, Claude Code reports the plugin as not installed and shows the claude plugin install command to run.

vigiles ships from a GitHub marketplace, so that is exactly our case, and the boundary is deliberate — plugins execute arbitrary code with your privileges, so a repo is not permitted to install one on your behalf.

So the declaration buys one thing, and it is worth having: a collaborator who clones and never runs init currently gets silence — the npm package is there, its six skills are unreachable, and nothing says so. With the declaration, Claude Code tells them the project wants this plugin and prints the install command. Silent absence becomes a prompt. A fresh clone and a CI job still have no plugin until someone installs it.

Nothing is vendored: two small JSON keys are a reference; the plugin content stays in the global cache, one copy shared across your repos.

That is also why the reachability warning above is advisory and never scored: it reports machine state that no repo-committed file can determine.

Removing it is a two-key edit — delete extraKnownMarketplaces.vigiles and enabledPlugins["vigiles@vigiles"]. eject does not do this: eject is the per-file inverse of compile ("hand this compiled file back to me"), and repo-scoped plugin wiring isn't a property of any one file. Uninstalling the global plugin is deliberately separate too — that install is shared by every repo on your machine, so one project can't speak for the others (claude plugin uninstall vigiles@vigiles if you do want it gone everywhere).

Codex / GitHub Copilot

Instruction file: AGENTS.md, read directly — there is no plugin or hook system. The enforcement path is:

npx vigiles init --harness=codex   # full setup: scaffolds AGENTS.md.spec.ts + types + CI + Codex skills
# 1. edit AGENTS.md.spec.ts (source of truth)
# 2. npx vigiles compile        → regenerates AGENTS.md
# 3. CI: npx vigiles lint && npx vigiles generate types --check

Use the full init --harness=codex (not init --target=AGENTS.md, which only scaffolds the spec) — it's what generates .vigiles/generated.d.ts and the CI config that step 3's generate types --check depends on. Authoring skills install globally via the cross-agent skills CLI (no repo vendoring): npx skills add zernie/vigiles -a codex -g -y~/.agents/skills/, which that same command handles. Codex hooks (.codex/config.toml [hooks]) aren't auto-wired yet.

Multi-agent (Claude + Codex)

Use a single spec with multiple targets — one source of truth, two outputs:

export default instructionFile({
  target: ["CLAUDE.md", "AGENTS.md"],
  rules: { ... },
});

Both compile from the same spec with the same linter verification.

Cursor / Windsurf / other formats

vigiles compiles to markdown only (CLAUDE.md, AGENTS.md). For non-markdown formats (.cursorrules, .github/copilot-instructions.md, Windsurf), use a sync tool to convert from the compiled markdown — rule-porter or rulesync. vigiles is the source-of-truth compiler; sync tools handle the last mile.

Non-interactive setup (agents & CI)

init auto-detects a non-TTY and runs without prompts — a prompt as simple as "set up vigiles in this repo" is enough:

npx vigiles init        # full: specs + skills/hooks + CI + devDep (the default)
npx vigiles init --ci-only # gate only: the CI integrity gate + devDep, nothing installed

Which one? Bare init sets up the full layers (the default). Use --ci-only when the repo already has its own harness (its own hooks/skills, or its own eval loop) or isn't JS/Python — you get the deterministic lint gate in CI with zero conflict: no plugin, no scaffolded spec, no test. It's the same choice the interactive wizard's first question offers; an agent picks it with the flag. A non-interactive full run prints a one-line pointer to --ci-only in its summary, so the alternative is discoverable even when nobody saw the wizard. Either way the setup stays non-destructive and invites the richer layers later — nothing is forced.

Fallback — install hooks directly. The plugin already brings the hooks. To commit project-level hooks instead of (or alongside) the plugin, write them to .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && case \"$FILE\" in *.md) [ -f \"$FILE\" ] && head -1 \"$FILE\" | grep -q 'vigiles:sha256:' && { echo \"BLOCKED: Edit the .spec.ts source instead.\" >&2; exit 2; } ;; esac; exit 0"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "FILE=$(cat | jq -r '.tool_input.file_path // empty') && case \"$(basename \"$FILE\")\" in eslint.config.*|.eslintrc*|package.json|pyproject.toml|Cargo.toml) npx vigiles generate types 2>&1 || true ;; esac && case \"$FILE\" in *.spec.ts) npx vigiles compile 2>&1 || true ;; esac"
      }
    ]
  }
}

Recommended agent prompt — if you want an agent to set up vigiles:

Set up vigiles for this project:
1. Run `npx vigiles init` (adds vigiles to devDependencies and installs the
   Claude Code plugin via the marketplace — nothing is vendored into the repo).
   If this repo already has its own harness or isn't JS/Python, use
   `npx vigiles init --ci-only` for the CI integrity gate alone (nothing installed).
2. Read the generated .spec.ts, fill in the project's actual conventions
3. Run `npm install`, then `npx vigiles compile` to verify
4. Commit the .spec.ts, compiled .md, .vigiles/generated.d.ts, and package.json

CI pipeline

All agents share the same CI step:

- name: Verify specs
  run: npx vigiles lint && npx vigiles generate types --check

It catches hash mismatches (someone edited the compiled .md), missing specs (require-instructions-spec), and stale generated types.

What the agent gets wrong

  • Editing CLAUDE.md directly — the PreToolUse hook prevents this if installed.
  • Wrong rule namesenforce("no-console") instead of enforce("eslint/no-console"). The compiler catches it.
  • Forgetting to compile — the PostToolUse hook handles it automatically.
  • Headers inside sections — the compiler catches #/## headers in section content.

See also

  • Markdown mode — the no-spec on-ramp (inline <!-- vigiles:enforce --> comments).
  • CLI reference — every verb and flag.
  • Harnesses — how vigiles targets Claude Code, Codex, and beyond.