Files
zernie__vigiles/docs/agent-setup.md
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

251 lines
12 KiB
Markdown

# 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](../README.md)
## Contents
- [What `init` does](#what-init-does)
- [Per-agent](#per-agent) — [Claude Code](#claude-code) · [Codex / Copilot](#codex--github-copilot) · [Multi-agent](#multi-agent-claude--codex) · [Cursor / Windsurf](#cursor--windsurf--other-formats)
- [Non-interactive setup (agents & CI)](#non-interactive-setup-agents--ci)
- [CI pipeline](#ci-pipeline)
- [What the agent gets wrong](#what-the-agent-gets-wrong)
- [See also](#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@v1` workflow at `.github/workflows/vigiles.yml`
- **Dependency** — `vigiles` 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`:
```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](https://code.claude.com/docs/en/discover-plugins#configure-team-marketplaces),
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:
```bash
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:
```typescript
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](https://github.com/nichochar/rule-porter)
or [rulesync](https://github.com/dyoshikawa/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:
```bash
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`:
```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:
```yaml
- 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 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](markdown-mode.md) — the no-spec on-ramp (inline `<!-- vigiles:enforce -->` comments).
- [CLI reference](cli.md) — every verb and flag.
- [Harnesses](harnesses.md) — how vigiles targets Claude Code, Codex, and beyond.