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.
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
initdoes - Per-agent — Claude Code · Codex / Copilot · Multi-agent · Cursor / Windsurf
- Non-interactive setup (agents & CI)
- CI pipeline
- What the agent gets wrong
- See also
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@v1workflow at.github/workflows/vigiles.yml - Dependency —
vigilesadded todevDependencies - 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.jsonenables, 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 theclaude plugin installcommand 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 names —
enforce("no-console")instead ofenforce("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.