* fix: read a space-separated tool list, so a working fence stops auditing as ineffective Claude Code documents three equivalent spellings of `allowed-tools:` / `disallowed-tools:` / a subagent's `tools:` — a comma-separated string, a SPACE-separated string, and a YAML list. `splitList` in core/frontmatter-read.ts split on `,` alone, so the space-separated form arrived as ONE token matching no built-in name. Reported with a reproduction by @vlad-ryzhkov in #217: on a 22-skill harness, changing only the separator moved Safety 81 -> 80 -> 81, reversibly. Reproduced here before changing anything — three of the seven documented shapes came back as a single token: `WebFetch WebSearch`, `Bash(git add *) Bash(git commit *)`, and `"Read Write Glob"` (a YAML-quoted scalar whose VALUE is a space-separated list, which is what most skills in the wild write). TWO SURFACES, AND THE SECOND IS WORSE THAN THE REPORTED ONE. #217 asks about a skill's fence, where `allowed-tools:` only pre-approves, so a mis-split MISLEADS: a fence Claude Code really enforces was reported as closing no lethal-trifecta leg, naming as still-supplied the very tools the author had just denied. But a SUBAGENT's `tools:` is the allowlist the PreToolUse rail denies against, and it reads the same `splitList`. Measured on `tools: Read Grep Glob`, pre-fix: Read DENY · Grep DENY · Bash DENY — the joined token matched nothing, so the agent was denied EVERY tool, including the three it was explicitly granted. #217 raised this path as an open question ("I have not worked out whether anything downstream depends on that"); it did. THE NAIVE FIX IS WORSE THAN THE BUG. `split(/[,\s]+/)` shreds `Bash(git push *)` into `Bash(git`, `push`, `*)`, and `bashGrantIsUnbounded()` answers "unbounded" when it cannot recognise a grant — so a BOUNDED grant would begin reading as an unbounded one. That trades a false-clean verdict for a false-exposed one, which is the worse side of the trade core/lethal-trifecta.ts already reasons about, for a reason the author cannot see anywhere in their file. So this is a tokenizer: whitespace separates only at paren depth 0. Quotes are stripped per token AFTER splitting, never treated as a delimiter — the outer quotes of `"Read Write Glob"` are YAML syntax, and treating them as token boundaries would keep exactly this bug for every skill that quotes its list. An unclosed `(` keeps the remainder as one token rather than fragmenting it into garbage tool names. The tokenizer is the one @vlad-ryzhkov proposed, with his six-row table plus irregular spacing and an unbalanced paren as tests. Every new test is mutation-proven with the patch verified landed: separator back to `,` only (pre-fix) -> the 4 new tests fail whitespace splits at ANY depth (the naive fix) -> the parens test fails One home, so the surfaces cannot drift: `frontmatterList` is the single reader behind scan, adopt, and the rail. Closes #217 Co-Authored-By: vlad-ryzhkov <211849729+vlad-ryzhkov@users.noreply.github.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: a parenthesis inside a quoted string is a character, not a bracket Follow-up within #217, found by the Codex review bot on the first cut of the tokenizer. The depth counter read every `(` as structure, so a grant carrying a literal unmatched paren in its own command left depth at 1 after the grant closed and swallowed everything after it: tools: Bash(printf '( %s' foo) Read -> ["Bash(printf '( %s' foo) Read"] one token On a subagent that is the same failure this PR exists to fix: the rail denies `Read`, a tool the author granted. Reproduced before changing anything. Quote state now gates the DEPTH COUNTER ONLY, never the splitting. That asymmetry is the fix, not an accident: if being inside quotes also suppressed whitespace splitting, a quoted list would collapse back to one token and reintroduce #217 itself for every skill that writes `allowed-tools: "Read Write Glob"`. Backslash escapes are honoured outside single quotes, per shell rules. MUTATION-PROVEN BOTH WAYS, each with the patch verified landed: depth counted inside quotes (the pre-fix state) -> the new test fails quotes suppress splitting too (the over-fix) -> the new test fails 🔴 AND THE SECOND MUTATION EXPOSED A DEFECT IN THE TEST FIRST. Its original counterweight asserted the quoted list on a VALID YAML block and stayed GREEN under the widening mutation — because js-yaml strips the quotes before `splitList` ever runs, so no quote character was present and the mutation could not bite. Measured, then moved onto a MALFORMED block, where the regex salvage path hands the quotes through: that is the only path on which the property is observable at all. A green mutation is a finding about the test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: vlad-ryzhkov <211849729+vlad-ryzhkov@users.noreply.github.com>
163 KiB
CLAUDE.md
Positioning
vigiles verifies the rule references in agent instruction files — that each linter rule exists AND is enabled, that file paths and scripts are real. ⚠️ ADOPTION DIRECTION (committed 2026-07-15): audit-FIRST, not spec-first. The markdown the user hand-edits is the SOURCE OF TRUTH; enforcement of code-quality rules lands in the repo's NATIVE linter config (ruff/eslint), not the spec; the typed .spec.ts is an OPTIONAL authoring layer for harness-STRUCTURE rules only (subagent contracts, purity, railway, composition — what no linter can express). init is the GRADUATION to a spec, not the front door (audit is — see below); adopt/strengthen are skills, not verbs. The first pass writes nothing and asserts nothing false (ref-verification is advisory, never written into the file). eject always reverses. The rule: the spec AUTHORS, the native linter RUNS (same architecture as @vigiles/rule-enforcer) — Rule of Least Power applied to enforcement homes. [LEGACY, being retired: the earlier spec-first three-level ladder — inline/frontmatter/typed — and init adopting a CLAUDE.md into a spec-as-source-of-truth with the markdown as a build artifact. Treat as current-shipped, NOT the target.] See docs/markdown-mode.md.
Positioned in the harness engineering frame coined early 2026: Agent = Model + Harness. The harness has two enforcement modes — probabilistic compliance (prompts, instructions) and deterministic constraints (linters, types, hooks). vigiles is the deterministic-constraints layer for instruction files.
The sharper, structural form of "deterministic constraints" is the CATEGORY line: markdown is inert prose, but a typed .spec.ts is a PROGRAM, so the whole PL/formal-methods toolbox applies to the harness — and none of it can apply to a markdown file. vigiles is a COMPILER/VERIFIER for agent harnesses. What that buys, concretely: UNSAFE HARNESSES DON'T COMPILE — a config that leaks, exceeds its declared effect floor, hands off mismatched data, or mutates out of order is a TYPE error at edit time, not a runtime surprise a linter notices after the fact (SHIPPED: typed purity — purity:'pure'+'Bash' won't tsc — and typed composition); the keystone is TYPE-SAFE PIPELINING — pipe(producer, pipeStep(agent, needs({…}))) cross-references at compile time that step N's ok SUPPLIES step N+1's needs, so a multi-agent pipeline DOESN'T COMPILE IF THE HANDOFFS DON'T LINE UP. The enforcement is GRADUATED/OPT-IN, never all-or-nothing — the Level 0/1/2 ladder, the plain agent() vs the opt-in typed vigiles/claude-code import, the purity:'dangerously-unrestricted' escape hatch — progressive like TypeScript's strict, not a wall.
THE ADOPTION FRONT DOOR is vigiles audit — "Lighthouse for your harness": one zero-config command → four DETERMINISTIC category RINGS (Truthfulness/Triggering/Structure/Tested, weighted A–F) + each finding's fix inline + a shareable HTML report. A plain audit is a DETERMINISTIC READ — safe to run anywhere (even a prod-wired repo), IDENTICAL on every OS, nothing executes. It is a LOCAL report (like Lighthouse), NOT a CI step — CI uses vigiles lint. The TWO executing checks — live MCP resolution (do referenced tools resolve on the real server?) + trigger-rate (do your skills FIRE?) — sit behind ONE CONSENT (the read-vs-run axis, src/scan-trigger-suggest.ts decideExecute): at a TTY audit ASKS ONCE (a bundled prompt that DISCLOSES confinement + cost) and REMEMBERS in .vigilesrc.json (audit.measure); headless (--json/CI/non-interactive/agent) it stays a read + a one-line nudge (never hangs, never silently executes). There is deliberately NO execution flag: audit is a LOCAL report (like Lighthouse), NOT a CI step — CI uses vigiles lint (the deterministic gate). It runs the executing checks only when a human can consent; AUTOMATION tests the harness via the vigiles testing API + skills, never the report verb. The founder-driven simplification (2026-06-27) collapsed the earlier --deep/--measure/--fast flag sprawl into this ONE read-vs-run consent, because a uniform interactive choice beats per-tier toggles. WHY these two are opt-in: live MCP STARTS your own servers (a real backend connection) and trigger-rate spends model quota — neither is unsafe, but both DO something beyond a read, so a plain audit never does them without a human's yes. On consent: LIVE MCP is own-repo only (never a stranger's server) because STARTING a server connects to a backend and deny-all-net would break the tools/list it performs; trigger-rate STUBS skill bodies so no skill PROCEDURE runs (hasModelAccess/isMeteredAccess only shape the disclosure wording, sub=$0 vs metered=credits). THE SAFETY BATTERY (do your hooks actually block?) is DELIBERATELY NOT an audit ring (narrowed 2026-06-27, founder "no half-made shit pre-release"): running ARBITRARY hooks safely needs cross-platform confinement and that's parked (bubblewrap is Linux-only; env-scrub ephemeral floor + macOS sandbox-exec are the unbuilt exit criterion) — so rather than ship a Linux-confined/Mac-unconfined ring, the battery lives in the vigiles testing API (guardrail-check/assertBlocksDisasters) where you opt in EXPLICITLY (a test you wrote, no zero-config-safety promise to break). audit re-promotes a Safety ring only once one confinement works the same on macOS+Linux. Everything renders FROM the versioned AuditReport JSON (src/audit-report.ts, schemaVersion), never from the HTML: the local React/shadcn single-file report and audit --json for CI. The report UI is a real Vite + React + shadcn app (report/) built to ONE self-contained file the CLI fills with the JSON (React runs in the reader's browser; the CLI ships only the built template + stays runtime-dep-light), and its components are presentational so any other renderer works off the same JSON contract.
The cross-referencing engine is what the tool is built on: enforce("@typescript-eslint/no-floating-promises") verifies the rule exists AND is enabled in your linter config. Same for ESLint, Ruff, Clippy, Pylint, RuboCop, Stylelint, Cedar policies (for AWS Bedrock AgentCore and other Cedar-using runtimes), and the JVM/Go ecosystem — detekt, ktlint, Checkstyle, golangci-lint. The rule name is resolved against each linter's own catalog rather than matched as a string, and the enabled state is read from the project's config. (Two exclusivity claims stood in this section until 2026-09-08 — one here, one in the adoption-direction paragraph above. Both rested on a documentation-checked competitor matrix rather than a run, and no-product-strategy-here forbids competitive positioning in this public repo. Recorded rather than silently dropped, so neither is reintroduced.)
Authoring-time feedback comes two ways: generate-types emits a .d.ts so the TS compiler PROVES .spec.ts references at edit time, and generate-schema emits a JSON Schema so a YAML LSP autocompletes and squiggles vigiles: frontmatter rule names — same guarantee, no TypeScript required. Both scan all 11 catalog APIs, package.json, and project files.
Second layer — TESTING the harness (docs/harness-testing.md is the guide; do not restate it here). Three tiers, cheapest first: runHook pipes a synthesized event to a hook process (no model, no key, reaches every event); runHarnessTest drives the real agent binary against a scripted mock model; runEval drives a real model across arms x trials. THE DISCIPLINE, which constrains what you build rather than describing it: push every question that can be answered deterministically DOWN a tier, and let only the two irreducibly-real-model questions — does a description FIRE, does behaviour MOVE — touch a real model. The real-model tier runs on the user's own Claude subscription (vigiles drives their own claude CLI), which is why a team can afford harness evals at all; CI runs only the free deterministic tiers. Never claim the mock-model tier is itself distinctive — every code-defined SDK ships a fake model. What no SDK does is test the harness loaded AS IT SHIPS, enforce a subagent's tool contract deterministically, and put a real-model tier on the sub.
Third layer — COMPILED HOOKS, the GATE instrument (vigiles/hook, src/core/hook-program.ts; docs/compiled-hooks.md is the guide and docs/experimental.md holds the exit criteria — do not restate either here). A hook is authored as a PURE typed (event) => Decision over a CLOSED vocabulary and compiled to the harness protocol, so whole classes of hook bug become unrepresentable: the author never writes the exit code or JSON field, the matcher is AST-backed rather than a glob over the command string, an import outside vigiles/hook does not compile, the artifact is SHA-stamped and a hand-edit is refused fail-closed, and a category mistake is a tsc error. THREE THINGS TO KEEP SAYING, because each is a claim someone will overstate: the entry points are exported experimental_* and renaming one is not a breaking change; compile and verify fix AUTHORING and LOGIC, never the harness's DELIVERY; and a model can route around a tool entirely (#45427/#32376), so a gate is a STRONG DEFAULT and never an unbypassable wall. The old subagent-bypass limit (#34692) is FIXED as of CC 2.1.241 and pinned by src/subagent-delivery.test.ts — prose cannot notice that someone else's product moved, a test can.
Multi-harness by design (a hard requirement). vigiles targets Claude Code today but the core is harness-agnostic: every Claude-Code-specific fact lives behind one of five injectable ports — HarnessDialect/PluginLayout/HarnessRuntime/HookProtocol/ModelMock — bundled per harness as a HarnessAdapter. Adding a harness (Codex likely next, then OpenCode/Crush/Gemini) is writing one adapter object and registering it; the boundary rule (core ⊄ adapter) keeps the core untouched. Third-party adapters are a first-class, supported extension point: vigiles/adapter exports the five ports plus a conformance kit, documented in docs/authoring-an-adapter.md. Backwards compatibility is non-negotiable — Claude Code stays the default everywhere (the CLI auto-detects; the library selects by import), so adding adapters never breaks existing consumers. See docs/harnesses.md.
Cross-platform by requirement (macOS + Linux). Most devs are on a Mac, so confinement cannot be Linux-only: it is a native backend per OS behind a vigiles/os-isolation port — bubblewrap (+nft per-host egress) on Linux today, sandbox-exec/Seatbelt for macOS (built-in, the committed next backend), refuse otherwise. Two ORTHOGONAL protections, not one switch: HOST protection (can foreign code read my secrets / escape?) is provenance-keyed — your own code runs direct, foreign plugin/pluginDir code is confined-or-refused; STATE protection (does RUNNING a model-driven skill mutate my world?) is UNCONDITIONAL — a fresh ephemeral run environment (throwaway CWD+HOME, scrubbed env), because the model, not the author, chooses the actions. Ephemerality needs no kernel features, so it is the cross-platform floor that lands on macOS immediately; the per-host egress wall stays a Linux capability and degrades honestly to deny-all-net elsewhere. See docs/safety.md.
vigiles does NOT do architectural linting. Use ast-grep, Dependency Cruiser, Steiger, or eslint-plugin-boundaries for that. vigiles can reference their rules via enforce().
Direction
vigiles is ONE LOOP, not a bag of features: DECLARE what the harness should do (the typed spec) → CHECK reality against that declaration. Four instruments are FACETS of the same loop, never a menu — VERIFY (lint/cross-ref/compile, author-time, free), GATE (compiled hooks, loop-time, free), MEASURE (evals on your subscription), OBSERVE (a local record of what the instruments saw). A feature belongs here only if it is a facet of declare→check.
DECIDABILITY DECIDES WHICH INSTRUMENT FITS WHICH SURFACE. On deterministic surfaces (a hook decision, a subagent's tool contract, a file/rule reference, a declared effect) the "supposed to" is DECLARED by the spec, so "did reality match?" is a deterministic check — precise, free, no model. On behavioural surfaces (does a skill FIRE here?) that question is undecidable statically, so it needs an authored expectation measured on a real model (measureTriggerRate) — never claim a passive record catches a MISS. Prose like "write clean code" is neither and stays prose. The rule underneath: you can check a DECISION, you cannot check "does this fire" without running it.
Architecture
Three rule types in specs:
enforce()— delegated to external tool (linter, ast-grep, dependency-cruiser). vigiles verifies the rule exists and is enabled.guard()— a path→command guard (e.g.*.spec.ts→npx vigiles compile), compiles to**Guard:**and wires spec-driven automation into hook engines.guidance()— prose only, compiles to**Guidance only**in markdown.
Architectural linting (file pairing, import boundaries, AST patterns) belongs in external tools — reference them via enforce().
Template literal types ensure linter names (eslint/, ruff/, etc.) are type-safe. Branded types (VerifiedPath, VerifiedCmd, VerifiedRef) distinguish verified references from raw strings.
Compilation: spec.ts → compiler reads spec, validates references (file paths via existsSync, npm scripts via package.json, linter rules via linter APIs), generates markdown with SHA-256 integrity hash.
Core modules: src/core/spec.ts (types + builders), src/core/compile.ts (compiler), src/core/linters.ts (11-catalog cross-referencing engine), src/core/effects.ts (static effect-surface / purity ladder), src/core/bash-effects.ts (deterministic Bash-effect classifier), src/core/generate-types.ts (type generator), src/core/proofs.ts (proof algorithms for self-evolving specs), src/core/evolve.ts (evolution engine).
Harness-adapter layout (hexagonal — see docs/harnesses.md). The tree is split so a second harness (Codex likely next) sits beside Claude Code without touching the core:
src/core/— the harness-agnostic reference-verification DOMAIN (spec, compile, linters, proofs, …). Knows nothing about how an agent runs.src/adapters/claude-code/— the Claude Code ADAPTER: the swappable ports + bundle + harness glue (theHarnessDialect/PluginLayout/HarnessRuntime/HookProtocol/ModelMockport impls, theHarnessAdapterbundle, agent/skill runtime, run-scripts, and a thin plugin-loader wrapper that defaults the CC layout). A futuresrc/adapters/<other-harness>/mirrors it.src/root — the application/composition layer AND the harness-testing LIBRARY: cli, scan, the harness-agnosticplugin-loader(layout-injected; the CC wrapper + every other adapter delegate to it, so no adapter imports a sibling), the deterministic runners (harness-test/run-hook/eval+mock-model/sandbox/egress/judge) that default to Claude Code via an injected{ adapter }, and thetesting/unit/integration/e2ebarrels that route through the runners and NEVER import an adapter directly.
Two boundary rules are enforced by eslint-plugin-boundaries (rule boundaries/dependencies, error-mode, classified by directory) and dogfooded via enforce("boundaries/dependencies"): core ⊄ adapter (the domain stays harness-agnostic), and the agnostic-surface (src/{testing,unit,integration,e2e}.ts) ⊄ any adapter (the public agnostic entries route through the composition-root runners, so "agnostic" is enforced, not just named) — the architecture invariant is a verified reference, not a convention. Consumers select a harness by import (vigiles/claude-code beside the agnostic vigiles/vigiles/eval), not a runtime config key; the CLI selects per repo — auto-detect, a --harness= override, or a harness key in .vigilesrc.json written by init (the project-level declaration, distinct from the library's import-time selection; an array for a repo targeting several harnesses also drives a byte-identical CLAUDE.md⇄AGENTS.md mirror when no sync tool fans it out).
Layout
What each ROOT directory is (the per-file map is Key Files below; this is the per-dir index). This repo is FIVE things at once — a TS library, a shipped Claude Code plugin, two auxiliary packages, a benchmark suite, and its own dogfood harness — so the root has a dir per concern, grouped here by concern.
THE LIBRARY (the one compiled thing):
src/— all TypeScript source, compiled todist/. The ONLY thingeslint src/lints, vitest sweeps (src/**/*.test.ts), and the 100% coverage gate covers. Hexagonal:src/core/(domain) ·src/adapters/<harness>/(ports) ·src/*.ts(composition root + library). Alsosrc/schemas/= DEPRECATED mdschema presets, parked (seesrc/schemas/DEPRECATED.md).dist/— compiled JS output (gitignored; the published artifact + the audit-report template).
THE SHIPPED PLUGIN (root-level by Claude Code plugin convention — these MUST sit at the repo root beside the manifest; they are markdown/shell consumed verbatim by the harness, never compiled):
plugin.json(repo ROOT) — the AGENT PLUGINS 1.0.0 manifest (the vendor-neutral packaging standard, agent-plugins.org). Pins$schemato the 1.0.0 identifier; carries metadata only, since skills are discovered at the spec's fixedskills/<name>/SKILL.mdpath and the 1.0.0 schema has NO home for hooks/subagents. So the CC manifest below is NOT redundant — the two coexist by design (neutral = portability, CC = what only CC understands), andsrc/agent-plugins-manifest.test.tsfails CI if their name/version/description/license drift..claude-plugin/— the CLAUDE CODE plugin manifest (plugin.json, thehooksblock) +marketplace.json. The repo root IS the plugin root.skills/— the SHIPPED consumer skills (published; both manifests point here — the CC one explicitly, the neutral one by the standard's fixed path).hooks/— the SHIPPED plugin's hook scripts (pre-edit/post-edit/refs-nudge/eval-lock-nudge/session-start; published, referenced byplugin.json).
THIS REPO'S OWN HARNESS (how Claude Code behaves when a CONTRIBUTOR works in-repo — NOT shipped; .claude/ is not published):
.claude/—settings.json+hooks/+skills/..claude/skills/holds the CONTRIBUTOR-only dev skills (add-a-linter, audience-check, audit-feedback-loop, code-quality, dogfood-cli, enforce-rules-format, generate-logo, landing-site, pr-to-lint-rule, screenshot, ship-a-feature, verify-docs-findable) + vendored deep-research + review-docs. (The olddev/second plugin was folded here 2026-07-14.)
AUXILIARY PACKAGES (separate npm packages with their OWN package.json, NOT part of src/):
rule-enforcer/—@vigiles/rule-enforcer, the opt-in rule-SYNTHESIS engine + its two-stage blind-gold TRUST GATE (gate.js). Folded in from a former standalone repo; CI runs its gate (dogfood). Thepr-to-lint-ruleskill's engine.report/—@vigiles/report, the Vite + Tailwind audit-report app that CONSUMES@vigiles/report-viewand builds it to one self-contained HTML template the CLI fills. Kept out of the CLI's runtime deps.packages/report-view/—@vigiles/report-view, the SHARED source-only report view (presentational components + AuditReport schema + band tokens + theme.css) rendered byreport/(and latersite/+ the in-browser demo) from the same AuditReport JSON. Wired as npm WORKSPACES (packages/*,report,site) — deps hoist to root, the published package's CI stays green.
DOCS (see the doc-tiers rule):
docs/— PUBLIC user docs (what a user needs to act).
TESTS + FIXTURES + BENCHMARKS:
test/— test DATA + tier config, NOT the unit tests (those aresrc/**/*.test.ts). Holdstest/dogfood/(SHA-pinned vendored real plugins — see the dogfood-vendoring-policy rule),test/fixtures/(the E2E example-project),test/{e2e,runners,types}/.examples/— user-facing copy-paste*.harness.mjs/*.eval.mjsdemos + compiled example specs; the CLI-fallback on-ramp.bench/— the real-model A/B benchmark corpus, deliberately OUTSIDE the vitest sweep (run by node, on your sub). Its freecorpus/verify*.mjsself-checks are CI-gated.
BUILD + TOOLING + GENERATED:
scripts/— the BUILD pipeline only (api-extractor.mjs,build-report.mjs), invoked bynpm run build/CI.tools/— HUMAN-run maintenance scripts, never in CI. Each documented intools/README.md(what + WHEN-run + live/dead status): live corpus-maintenance (refresh-vendor.sh,dogfood-sweep.sh), pre-launchfp-sweep.sh, and the demo-asset tooling (demo.sh,make-demo-gif.py) for the demo assets.api-surface/— the committed public-API surface snapshots (*.api.md), one per exported entry point. A generated CONTRACT (lockfile-class), reviewed as a diff, never hand-edited: CI diffs the live.d.tssurface against these (the API surface gate) and fails a PR that changes a public export untilnpm run api:reportregenerates + commits it. (Renamed 2026-07-14 frometc/, which was Microsoft API Extractor's default output dir; the report folder is set inscripts/api-extractor.mjs.) Distinct from the human API REFERENCE (TypeDoc → GitHub Pages)..github/— CI workflows (ci.ymlis the gate)..vigiles/— vigiles's own runtime output (gitignored: hook observations, eval locks, the runs ledger).
Key Files
src/core/spec.ts— Type system and builder functions (enforce, guidance, claude, skill, agent, file, cmd, ref, symbol, dir, glob — dir() verifies a path exists AND is a directory, glob() verifies a pattern matches ≥1…src/core/compile.ts— Compiler: spec → markdown with SHA-256 hash, linter verification, reference validation; compileClaude/compileSkill/compileAgent (subagents: frontmatter + verified tool contract + body marks +…src/core/hook-program.ts— COMPILED HOOKS core (the GATE instrument; harness-neutral, pure).src/core/hook-program.test.ts— Compiled-hooks core suite (vitest): the gate decision is pure + AST-matched (compound bypass denied, grep false-positive allowed), compiles to a CC block, an out-of-vocabulary import does NOT…src/hook.ts—vigiles/hook— the public closed vocabulary for authoring a compiled hook (re-exports core/hook-program.ts): defineHook/defineFileGate/definePromptGate/defineStopGate/defineInject/defineReact…src/hook.test.ts— Compiled-hooks E2E (vitest): drives the REAL built CLI runtimenode dist/cli.js hook-runtime run-program <fixture.mjs>over runHook — a gate denies force-push (exit 2) + the compound bypass and…src/hook-install.ts— Hook installation — the bridge folding hook compilation intovigiles compile(no straycompile-hookverb; cohesive-cli-surface).src/hook-install.test.ts— Hook-install suite (vitest): mergeHooksJson adds to an empty/existing settings object, preserves the user's own + a sibling vigiles hook for a different file, is idempotent (recompile replaces…src/hook-dogfood.test.ts— OSS dogfood (vitest, model-free): proves a COMPILED hook closes the gaps a hand-written guard leaves, using the DISASTER_CATALOG as the oracle — a faithful substring guard (the widely-copied disler…src/hook-oss-comparison.test.ts— Aggressive OSS comparison dogfood (vitest, model-free): compiled hooks vs the hand-written shapes the ecosystem ships, DISASTER_CATALOG as oracle.src/guardrail-check.ts— VERIFY feature — 'prove your safety hook ACTUALLY blocks' (onvigilesroot).src/verify-plugin-guards.ts— The DIRECTORY-level disaster battery —experimental_verifyPluginGuards(dir, opts?)on thevigilesroot: reads the hooks a repo actually declares and measures each with its OWN condition, and its report is a union so a hook that never ran cannot be read as a score (#212).src/core/bash-equivalents.ts— Shell-EQUIVALENT rewrites of a dangerous command — the generator behindexperimental_alternateSpellings(events)in guardrail-check.ts (renamed 2026-09-02 fromequivalentDisasters, which its….vigiles/hooks/test-tier-nudge.hook.mjs— THIS repo's OWN compiled hook, and its first (2026-09-07) — the artifact that retired thedocs/compiled-hooks.mdline "neither [consumer] is this repository's own harness, which still wires its…src/test-tier-nudge.hook.test.ts— The test for this repo's own compiled hook (vitest, unit tier). Named<surface>.hook.test.tsbecause a hook's{surface}name carries.hook— that is what binds it to the surface under theincludeglobs in .vigilesrc.json..vigiles/hooks/docs-drift-nudge.hook.mjs— This repo's OWN compiled hook #2 — a react that nudges when product code under src/ is edited and no doc (docs/**, README, a CLAUDE.md.spec.ts, CONTRIBUTING) has been touched this session.doc-consistencyas a mechanism instead of prose:lintreads a checkout, but 'the code changed and the docs did not' is a property of a DIFF, which only a hook sees. Two named facts — docs.followed (quiet because the work IS being done) and docs.nudged (quiet because it just spoke) — so the two silences never merge.src/docs-drift-nudge.hook.test.ts— The test for the docs-drift hook (vitest, unit tier): both silences asserted apart, and mutation-proven — deleting the docs.followed branch fails exactly one test.examples/harness/safe-bash-guard.mjs— The compiled-hook dogfood artifact — a Bash safety gate authored againstvigiles/hookexpressing a real guard's full intent (force-push/reset --hard/--no-verify/forced-rm/secret-read/curl|sh) as a pure typed function. In-repo it imports the built dist (runs via hook-runtime run-program in src/hook-dogfood.test.ts); external users authorfrom "vigiles/hook"+vigiles compilesrc/verify-plugin-guards.test.ts— The plugin-guard sweep's suite (vitest): the union report proved in both directions — a measured hook carries a score, and a hook that never ran carries a REASON and no number. Mutation-proven three ways (#212).src/core/command-files.ts— Which FILES a shell command reads or writes — the deterministic extractor behind the path-aware half of a Bash gate (#212).src/core/command-files.test.ts— Command-file-reference suite (vitest, unit tier, nothing spawned) — both directions, because either alone is worthless: it must NAME the script an interpreter runs (relative, absolute, by extension behind an unknown runner, extensionless behind a known one, a path-shaped head, through a variable whose value was supplied, nested in a pipeline) and must stay SILENT on the shapes measured in the wild that merely look path-ish — the five davila7 commands the wide rule got wrong (rm/mv/cat/tailon a temp file,echo N/A), plus URLs, flags, assignments, globs,-c/-e/-moperands, a command substitution, a bare head, and the interpreter's own trailing argumentssrc/core/guards.ts— EXPERIMENTAL prototype (the GATE axis of the reliability runtime) — typed safe-by-construction harness GUARDS: declare guard.block / requireBefore (the ORDER axis — destroy-after-plan, enforced live…src/hook-matcher-delivery.test.ts— Which matcher strings Claude Code honours as match-all — MEASURED, after**sat in MATCH_ALL (core/hook-matcher.ts) on no evidence. A claim about somebody else's product that prose cannot keep true.src/core/guards.test.ts— Guard prototype suite (vitest) — pure block/requireBefore/confine decisions + the session-ledger round-trip + the runnable hook-runtime guard gate. EXPERIMENTAL (see src/core/guards.ts)src/core/tool-contract.ts— Tool-contract verification — the cross-referencing engine applied to a subagent's tools: rail. verifyToolContract(tools, dialect) flags never-available (denylist) + unknown (with did-you-mean ≤2)…src/core/tool-contract.test.ts— Tool-contract detector suite (vitest): clean contract, never-available flagged, close typo → did-you-mean, far unknown → no suggestion + suppressed by confidentToolIssues, Tool(restriction) strip…src/core/hook-events.ts— Hook-event verification — the cross-referencing engine applied to the EVENT a hook registers under (a typo → the hook never fires). verifyHookEvents(events, dialect) flags unknown events…src/core/hook-events.test.ts— Hook-event detector suite (vitest): real events pass, close typo → did-you-mean, framework/custom event (TeammateIdle) suppressed by confidentHookEventIssuessrc/core/mcp-config.ts— MCP-config verification — verifyMcpServers flags a declared MCP server with neither a command (stdio) nor a url (http/sse): it can't start.src/core/mcp-config.test.ts— MCP-config detector suite (vitest): stdio (command) + http (url) pass, neither → flagged, non-object entry flagged, empty command string doesn't countsrc/core/agent-plugins.ts— Agent Plugins (agent-plugins.org) 1.0.0 — the VENDOR-NEUTRAL packaging standard, recognized as an ADDITIONAL manifest source, NOT a HarnessAdapter.src/core/mcp-tool.ts— MCP-tool resolution — the cross-referencing engine applied to an MCP tool reference's SERVER (the MCP half of the tool-reference check; subagent-tool-contract checks the built-in half but passes any…src/core/mcp-tool.test.ts— MCP-tool detector suite (vitest): a declared server resolves, an undeclared one is flagged, GUARD 1 (no declared set → nothing), GUARD 2 (built-inideallowlisted), GUARD 3 (plugin-namespaced form…src/core/description-overlap.ts— Description-overlap — a DETERMINISTIC proxy for a behavioral risk (the showpiece): two model-invocable skills whose descriptions are near-identical can't be told apart by the selector → the wrong…src/core/description-overlap.test.ts— Description-overlap detector suite (vitest): fires on a copy-paste near-dup, stays quiet on a parallel-but-distinct pair (the create-issue/create-pr shape at NCD ~0.25), the calibrated cutoff sits…src/core/mcp-hook.ts— MCP-hook target verification — the cross-referencing engine applied to atype: mcp_toolHOOK action (CC hooks support command/http/mcp_tool/prompt/agent; mcp_tool calls a tool on a connected MCP…src/core/mcp-hook.test.ts— MCP-hook detector suite (vitest): complete action on a declared server passes, a command hook is ignored, missing tool → incomplete, undeclared server flagged (with a declared set), GATE (no…src/core/effects.ts— Static effect-surface analysis + the RUNTIME purity gate — the deterministic, model-free purity ladder over a declared tools: contract (an agent's side effects ARE its tool calls; the…src/core/effects.test.ts— Effect-surface detector suite (vitest): tool classification (read-only/side-effecting/unknown), Tool(restriction) strip, effectSurface bucketing + de-dupe + the three purity rungs (incl.…src/core/bash-effects.ts— Deterministic Bash-effect classifier (no LLM) — the no-model answer to 'is this Bash command read-only or side-effecting?' (Claude Code uses an LLM permission classifier; this is the deterministic…src/core/bash-effects.test.ts— Bash-classifier suite (vitest, 95 tests): read-only corpus (cat/ls/grep/git status/pipes), side-effecting (rm/git push/redirects/find -delete/sed -i/tee), undecidable (eval/$CMD/sh…src/core/linters.ts— Cross-referencing engine (ESLint, Stylelint, Ruff, Clippy, Pylint, RuboCop, Cedar, detekt, ktlint, Checkstyle, golangci-lint)src/core/dialect.ts— HarnessDialect — the format/dialect PORT (hexagonal format axis): the harness-specific vocabulary the compiler needs (built-in subagent tool catalog, never-available tools, side-effecting tools via…src/core/dialect.test.ts— Dialect-port test suite (vitest): the Claude Code dialect has the expected shape, compileAgent verifies the tool contract against the injected CC dialect (built-in ok, typo → did-you-mean), and an…src/adapters/claude-code/dialect.ts— The Claude Code adapter's HarnessDialect surface: DEFINES claudeCodeDialect (the concrete dialect lives in the adapter, symmetric with claudeCodeLayout/Runtime/HookProtocol/ModelMock; core holds…src/adapters/claude-code/typed-spec.ts— Typed Claude Code authoring surface (exported on vigiles/claude-code): the CC-bound agent/skill that pin the generic ToolVocabulary to ClaudeCodeToolVocabulary (readOnly/bounded unions derived from…src/core/layout.ts— PluginLayout — the plugin/repo LAYOUT port (filesystem half of the format axis): where a harness's instruction file / skills / agents / commands / hooks / settings live on disk + the plugin-root…src/adapters/claude-code/layout.ts— claudeCodeLayout — the PluginLayout port's Claude Code reference impl (.claude-plugin/plugin.json, .claude/settings.json, skills/agents/commands surfaces, ${CLAUDE_PLUGIN_ROOT}); loadPlugin defaults to itsrc/adapters/claude-code/layout.test.ts— Layout-port test suite (vitest): claudeCodeLayout is the loadPlugin default; an alternate Codex-shaped PluginLayout (AGENTS.md, prompts/ surface, .codex/ settings, ${CODEX_PLUGIN_ROOT}) loads…src/core/runtime.ts— HarnessRuntime — the runtime/transport PORT (transport axis): the facts the test tiers need to drive a harness (the agent binary to spawn + the env a no-key mock is reached through: base-URL var…src/core/harness-driver.ts— HarnessTestDriver — the layer-2 deterministic-runner PORT: how runHarnessTest builds a harness's argv (buildArgs), starts its scripted wire-format mock (startMock → HarnessMockHandle), parses its…src/adapters/claude-code/runtime.ts— claudeCodeRuntime — the HarnessRuntime port's Claude Code impl (spawnclaude, reach the mock via ANTHROPIC_BASE_URL + dummy ANTHROPIC_API_KEY; versionKey → major.minor) + mockModelEnv, the pure…src/adapters/claude-code/runtime.test.ts— Runtime-port test suite (vitest): claudeCodeRuntime values, mockModelEnv layers mock URL + dummy key over the base env, and an alternate runtime maps the URL onto its own env var (OPENAI_BASE_URL) — the Codex transport seamsrc/core/hook-protocol.ts— HookProtocol — the hook-wire PORT (transport axis): how a harness signals a hook block/deny (block exit code + deny decision values + event env vars) PLUS injectableEvents — the events that honor…src/adapters/claude-code/hook-protocol.ts— claudeCodeHookProtocol — the HookProtocol impl: blocks via exit 2 or permissionDecision:deny / decision:block; injectableEvents = [SessionStart, UserPromptSubmit, PostToolUse]src/core/model-mock.ts— ModelMock — the model-mock PORT (transport axis): the mock model's wire format (anthropic-messages vs openai-responses) + the turn-consuming endpoint + optional count-tokens endpoint. startMock…src/adapters/claude-code/model-mock.ts— claudeCodeModelMock — the ModelMock impl: anthropic-messages SSE at /v1/messages (+ count_tokens). A Codex codexModelMock sets openai-responses + /v1/responsessrc/core/adapter.ts— HarnessAdapter — the bundle that makes a harness a single addable unit: groups the ports (dialect/layout always; runtime/hookProtocol/modelMock OPTIONAL, present iff the matching capability is…src/adapters/claude-code/adapter.ts— claudeCodeAdapter — the Claude Code HarnessAdapter: the five CC ports bundled + a detect that recognizes a .claude-plugin/ manifest, .claude/settings.json, or CLAUDE.md.src/adapters/claude-code/adapter.test.ts— Adapter-bundle test suite (vitest): claudeCodeAdapter bundles all five ports, passes the conformance kit, the kit catches a broken adapter, detect recognizes a CLAUDE.md / .claude-plugin repo (and…src/adapters/codex/adapter.ts— codexAdapter — the OpenAI Codex HarnessAdapter (the five Codex ports in src/adapters/codex/{dialect,layout,runtime,hook-protocol,model-mock}.ts).src/adapters/codex/codex.test.ts— Codex adapter validation (vitest): codexAdapter passes assertAdapterConformance + assertAdapterLoadsHooks (TOML config.toml [hooks] round-trip), the compiler verifies a subagent contract under…src/adapters/codex/mock-model.ts— Codex transport, BUILT + PROVEN against the realcodexbinary (closes the deferred ModelMock-renderer gap): renderResponsesSSE emits the OpenAI Responses 9-event SSE sequence (response.created → ……src/adapters/codex/mock-model.test.ts— Codex mock test suite (vitest): pure unit tests for renderResponsesSSE (event order + completed-carries-text) and parseResponsesRequest (prompt/model/tool extraction + malformed-JSON tolerance) +…src/adapters/codex/driver.ts— codexDriver — the Codex HarnessTestDriver: buildArgs (codex exec flags + the keyless -c model_provider mock flags via runtime.wireMock + prompt last), startMock (startCodexMock — the Responses SSE…src/adapters/codex/harness-test.test.ts— Codex harness-test suite (vitest): pure unit tests for buildCodexArgs (exec flags, mock flags after exec, prompt last) and parseCodexRun (trimmed stdout, empty tools/hooks) + a GATED test that…src/adapters/codex/eval.ts— Codex EVAL-tier transport (exported on vigiles/codex), the seam measureTriggerRate/runEval dispatch to via the ModelOutputParser.src/adapters/codex/eval.test.ts— Codex eval-parser suite (vitest): fixtures are REAL codex exec --json captures (plain / tool-calling / skill-activating turns) — parseCodexEvalRun reads agent_message + usage, dedups…src/adapters/opencode/adapter.ts— opencodeAdapter — an EXPERIMENTAL, internal-only prototype HarnessAdapter for OpenCode (sst): dialect/layout/runtime/model-mock in src/adapters/opencode/, but NO hook-protocol — it declares…src/adapters/opencode/opencode.test.ts— OpenCode prototype validation (vitest): opencodeAdapter passes assertAdapterConformance WITHOUT a hookProtocol (shellHooks:false relaxes it), assertHarnessTestable returns its runtime+modelMock (it…src/adapter.ts—vigiles/adapter— the harness-adapter authoring kit (the documented small lib third parties use to build their own adapter): re-exports the five port interfaces + HarnessAdapter + the conformance…src/codex.ts—vigiles/codex— the OpenAI Codex adapter surface (sibling of vigiles/claude-code): re-exports the Codex ports + codexAdapter + codexDriver + the proven Responses mock…src/adapter-registry.ts— Adapter registry (composition root): ADAPTERS = [claudeCodeAdapter, codexAdapter] + detectAdapterResult/detectAdapter (highest detect() specificity wins, reports ambiguousWith for a repo that…src/adapter-conformance.ts— Adapter conformance kit: checkAdapterConformance/assertAdapterConformance (each port populated + cross-port invariants — port names agree, instructionFile is a declared target, plugin-root tokens…src/adapter-contract.test.ts— Adapter CONTRACT suite — the STRUCTURAL enforcement of test-both-harnesses: runs the conformance kit (src/adapter-conformance.ts) over the WHOLE registry in a loop (for adapter of ADAPTERS) instead…src/core/cedar.test.ts— Cedar policy resolution tests — filesystem-based @id() lookup with filename fallbacksrc/core/generate-types.ts— Type generator: scans linters/package.json/filesystem → emits .d.tssrc/core/generate-schema.ts— JSON Schema generator: emits .vigiles/schema.json from real linter config so YAML LSP autocompletes frontmatter rule namessrc/core/generate-harness.ts— Whole-harness codegen (harness-agnostic core, the THIRD generated artifact beside generate-types/generate-schema):vigiles generate harness [dir] [out]emits ONE harness.gen.ts registry over every…src/cli.ts— Thebin— a DISPATCHER SHIM with NO top-level imports (#216). CommonJS resolves top-level imports before argv is parsed, so a compiled hook's allow/deny used to loadcompile/lint/audit/evalfirst; measured at 610-661 ms per gated tool call against a 42 ms bare Node start. It now lazily requireshook-runtime.jsfor a decision andcli-main.jsfor everything else. The emitted commandnpx vigiles hook-runtime run-program <file>is unchanged and must stay so — it is baked into every already-emitted settings block and its SHA stamp.src/hook-runtime.ts— The compiled-hook RUNTIME (hook-runtime run-program) — stdin event → stamp check → dispatch by role. Kept OUT of the verb barrel so a decision loads only what it needs; three lazy edges carry their measurement at the call site (@iarna/toml, mvdan-sh, the adapter registry, the last reached only by a react).src/hook-runtime-graph.test.tsasserts the graph;tools/measure-hook-startup.mjsre-measures the numbers.src/hook-runtime-graph.test.ts— The module-graph invariant behind the runtime's startup cost: a decision must not pull the CLI barrel,@iarna/toml, or (unless it is a Bash gate)mvdan-sh. Asserts the GRAPH, not a duration — a timing threshold on a shared CI runner would be quarantined first. Both directions: a bash gate DOES load the parser,--helpDOES load the barrel.tools/measure-hook-startup.mjs— Human-run: prices a compiled hook's startup layer by layer (arequireper module) and end-to-end per hook role. The NUMBERS live here and are re-measured, never quoted from prose.src/cli-main.ts— CLI: init, compile, lint, test, eval, scan (primary commands + generate-types plumbing) +--version(prints the version, not the help banner).src/setup-plan.ts— Purevigiles initdecision logic (parseSetupArgs/shouldPrompt/resolvePlan/planPluginInstall): turns CLI flags + whether a human's at a TTY into a SetupPlan {lint,test,gha,plugin,strict}.src/core/adopt.ts— Faithful markdown → typed-spec ADOPTION — the deterministic half ofinitauto-adopt. adoptMarkdown(md, target) converts an existing CLAUDE.md/AGENTS.md into aclaude()spec SOURCE that compiles…src/core/adopt.test.ts— Adoption suite (vitest): structural unit checks (1:1 heading→section mapping, title-h1 consumed, ### kept inside a body, reserved-key capitalized, fenced ## not split, duplicate headings deduped…src/exclude.ts— The ONE exclusion policy for every walk that polices the user's repository (#192) — the parsed.vigilesrc.json#excludeas anExcludeSet, built ONCE whereloadConfig()runs and taken as a…src/exclude.test.ts— ExcludeSet unit suite (vitest, 100%-gated): the spellings (bare name, trailing slash,/**,./, globs), the floor, root/outside-root never excluded,explain(), the string face's name + name/**…src/exclude-cli.test.ts—excludee2e over the REAL built CLI on one fixture repo in BOTH directions (#192): withvendored(bare name) excluded, compile/lint/audit/test are clean and exit 0 — a frozen un-loadable spec, a…src/scan.ts—vigiles audit <dir>— deterministic, no-model report of what a plugin/repo ships and what's broken: the top-level instruction file (CLAUDE.md/AGENTS.md) presence + spec-managed-vs-hand-written (so…src/scan.test.ts— Scan test suite (vitest): instruction-file reporting (spec-managed vs hand-written vs none), skill description/user-invoked flags, agent tool-contract incl. inherits-all, hook resolution…src/scan-cli.test.ts— Scan CLI e2e suite (vitest, free unit tier): drives the REAL builtnode dist/cli.js auditover the repo-shape matrix — DOGFOOD on pinned OSS plugins (test/dogfood/*) for the cc surface +…src/scan-vendor.test.ts— Golden scan-VERDICT conformance over REAL SHA-pinned vendored plugins (test/dogfood/*) — the rules complement to vendor.test.ts (loader invariants), offline + model-free.src/scan-behavioral.ts—vigiles auditmodel trigger tier — the BEHAVIORAL column (the model-gated half of audit, the deliberate exception to one-detector-no-drift): probePluginTriggers measures whether a plugin's…src/scan-behavioral.test.ts— Behavioral-column test suite (vitest): builds a tiny real plugin dir and drives probePluginTriggersWith with an injected fakeProbe (HarnessProbe, no model) — only model-invocable+described skills…src/leaderboard.ts— Plugin health leaderboard: scoreReport turns a ScanReport into a 0–100 structural-health score + A–F grade from concrete facts (missing hook -15, no-description skill -10…src/leaderboard.test.ts— Leaderboard test suite (vitest): pure scoreReport penalty weights + clamp + empty-machine=0 + command-only/hooks-only/MCP-only-is-a-real-surface (incl. an end-to-end hooks-only plugin dir, and that…src/score-explainer.ts— Score-explainer (C4 of the measurement-authority pivot, the strongest pairing): the deterministic WHY behind a low measured score.src/score-explainer.test.ts— Score-explainer test suite (vitest): pure over a hand-built ScanReport (no fs/model) — each cross-ref finding maps to the right symptom + an ACTIONABLE fix (the fix is the product, tested as much as…src/optimize.ts— The per-repo harness optimizer's DETERMINISTIC spine, folded INLINE into the defaultvigiles auditreport (each finding carries its fix via formatRecommendations — NOT its ownoptimizeverb, and…src/audit-score.ts— Category scoring forvigiles audit— the Lighthouse RINGS.src/audit-prompts.ts— Auto-generated trigger probes for theauditmodel trigger tier (zero-setup): derive a diverse probe set from each skill's own description so the model-gated trigger-rate needs no hand-authored…src/segment.ts— Tier-A deterministic (no-model) SEGMENTER — the rule-vs-not stage of the audit rule map. segmentInstructions(markdown) splits a CLAUDE.md/AGENTS.md into ATOMIC candidate rules with provenance…src/rule-inventory.ts— The NARROW rule inventory + the static INTENT_MAP — 'which prose lines NAME an off-the-shelf lint rule, and is it enabled?'.src/rule-routing.ts— The deterministic (no-model) ROUTING stage of the audit rule map — routeRules(text,{availableRules,minConfidence}) segments (via src/segment.ts) then routes each atomic rule into a lane by…src/rule-signals.ts— The shared LEXICAL rule-signal vocabulary — ONE home for the deontic/imperative regexes both detection stages key on, so they can't drift.src/core/rule-catalog.ts— The DYNAMIC available-rule catalog for the audit rule map — enumerateEslintCatalog/enumeratePylintCatalog EXECUTE the repo's real linter (own-repo + consent) to list every installed rule (plugins…src/audit-report.ts— TheAuditReport— the VERSIONED JSON contract every renderer reads.src/audit-html.ts— The shareable HTML audit report — ONE renderer (pure shadcn/Tailwind, no custom-CSS fallback). renderAuditHtml(report) injects the AuditReport JSON into the prebuilt React/shadcn template…packages/report-view/— @vigiles/report-view — the SHARED audit report view (source-only, private, never published): the presentational React components (Report, Ring, RuleInventory, Adopt, Adoptability, Observations…report/— The audit report UI (@vigiles/report): a Vite + Tailwind v4 app whose src is now just main.tsx + index.css — it CONSUMES @vigiles/report-view (the shared components + schema + theme) and builds it…src/scaffold-test.ts— The deterministic test-gen ENGINE (B1: free-form in, a RUNNABLE starter test out) — SKILL-INTERNAL, no CLI verb (the test-harness skill drives it; the standalone verb was demoted to trim the launch…src/scaffold-test.test.ts— Scaffold-test engine suite (vitest): each kind (hook/skill/agent) yields the right colocated path + tier + public import + core call + the surface's metadata (namespaced id, declared tools, the…src/optimize.test.ts— Optimize test suite (vitest): pure over a hand-built ScanReport (no fs/model) — a clean loaded repo scores 100/A with no fixes and hands off to the measured layer (interactive/subscription), an…bench/corpus/coding-tasks.mjs— The reusable real-task corpus — the substrate the MEASUREMENT layer runs on. 5 NEUTRAL coding tasks (slugify/debounce/bugfix-offbyone/bigO/regex-email), each self-contained (seeds its own input)…bench/corpus/verify.mjs— Runnable, no-model self-check for the corpus correctness oracles — proves each task's check DISCRIMINATES a known-good vs known-bad artifact (a check that always returns 1 would silently void the…src/adapters/claude-code/run-scripts.ts— Script runner forvigiles test/vigiles eval: discover*.harness.*/*.eval.*(JS+TS), run each as a child node process, classify each result pass/skip/fail (exit 0 / SKIP_EXIT_CODE 77 /…src/adapters/claude-code/run-scripts.test.ts— Script-runner test suite (node:test): discovery, exit-code aggregation, env forwarding, summary formattingsrc/core/inline.ts— Inline-mode parser:<!-- vigiles:enforce ... -->comments in markdown for gradual adoptionsrc/core/frontmatter.ts— Frontmatter-mode parser:vigiles: enforce:YAML frontmatter rules in markdown (Level 1 adoption)src/core/frontmatter.test.ts— Frontmatter parser test suite (node:test)src/core/frontmatter-read.ts— Lenient frontmatter reader — ONE reader for the SKILL.md/subagent --- block, shared by scan + the PreToolUse rail (agent-runtime), replacing three divergent hand-parsers (scan's readField +…src/core/frontmatter-read.test.ts— Lenient-reader suite (vitest): valid YAML scalars + flow array, the comma / space / YAML-list split (whitespace separates only outside parens, #217), absent→null vs present-empty→[], block-scalar + next-line-quoted, malformed YAML → malformed:true AND salvages a…src/core/hook-normalize.ts— Hook settings normalization — the typed boundary (parse-don't-validate) the audit hook detectors read. normalizeHooks(raw) parses the raw settings.hooksunknownONCE into a typed…src/core/hook-normalize.test.ts— Hook-normalize suite (vitest): flattens the CC nested shape, reads the Codex flat shape, carries matcher null when absent, drops empty/non-string commands + non-object entries, returns [] for…action.yml— GitHub Action — a composite action over the publishednpx vigilesCLI (NOT a node20 entry pointing at an uncommitted dist/): maps every input to a real CLI flag, sets thevalidoutput via $GITHUB_OUTPUT, and supportsversion: localso the repo dogfoods it viauses: ./. See docs/cli.md and theprod-grade-gha-clirule..github/workflows/pages.yml— ONE GitHub Pages deploy for the WHOLE site (a repo gets one Pages site), on push to main — REPLACED the old separate api-docs.yml + build-only site.yml (combined in #76).src/cli-flags.ts— Shared CLI flag → config bridge (applyConfigFlags): --max-rules / --catalog-only override the loaded config so every GitHub Action input maps to a real CLI flag.src/cli-commands.ts— The canonical vigiles command surface — the SINGLE SOURCE OF TRUTH (VERBS + HOOK_RUNTIME_KINDS) the self-command-refs dogfood cross-references vigiles's OWN docs/comments against, so a renamed…src/dialect-drift.ts— Dialect freshness/drift detection — the read-local backstop for the hand-maintained claudeCodeDialect (CC is a black box).src/dialect-drift.test.ts— Dialect-drift suite (vitest): pure parser units + a GATED read-local freshness check against the INSTALLED claude-code — fails loud when CC's sdk-tools.d.ts tool-input set drifts from…src/self-command-refs.ts— Self-command-reference verification — the cross-referencing engine applied to vigiles's OWN docs (the cohesive-cli-surface enforcement): everyvigiles <cmd>reference must resolve to a real VERB…src/self-command-refs.test.ts— Self-command-refs unit tests + the repo DOGFOOD: scan vigiles's own docs/comments (docs/, README, CLAUDE.md, src/, examples/, hooks/) and assert everyvigiles <cmd>resolves.src/doc-command-coverage.ts— Doc-command coverage — the INVERSE of self-command-refs and the deterministic FLOOR under the document-the-why rule. self-command-refs checks docs→code (everyvigiles <cmd>ref resolves); this…src/doc-command-coverage.test.ts— Doc-command-coverage unit tests + the repo DOGFOOD: every public VERB is mentioned under docs/ (the floor under document-the-why). verbMentioned counts a command context not a bare word…src/doc-test-script-coverage.ts— Doc-test-script coverage — the SIBLING of doc-command-coverage.ts aimed at the other reader.src/doc-test-script-coverage.test.ts— Doc-test-script-coverage unit tests + the repo DOGFOOD: everytest:*script in package.json is named on the CONTRIBUTING.md tier map.src/core/spec.test.ts— Spec + compiler test suite (node:test)src/core/agent.test.ts— Subagent compilation test suite (node:test): agent() builder + compileAgent — frontmatter, tool-contract verification (built-in/MCP/never-available/did-you-mean), body-ref validation, Rules section…src/adapters/claude-code/agent-runtime.ts— Agent PreToolUse tool-contract rail — the differentiator that closes the declared-vs-enforced gap (#4740/#21460, SDK #172): tools: is documentation, so a PreToolUse hook (vigiles hook-runtime agent)…src/adapters/claude-code/agent-runtime.test.ts— Agent-runtime test suite: pure parse/decide logic, active-agent round-trip, hook ⇄ allowlist agree (the declared contract IS the enforced rail), parseAgentPurity round-trip + the runtime purity gate…src/adapters/claude-code/effect-region.ts— Effect-region state — the position-aware half of the purity gate.src/adapters/claude-code/effect-region.test.ts— Effect-region test suite (vitest): setEffectActive/readEffectActive/clearEffectActive round-trip + idempotent clear + malformed-JSON tolerance, hasEffectBoundary true/false on a markersrc/adapters/claude-code/agent-result.ts— Railway result parser: a subagent with a result() contract ends its turn with a vigiles:ok/err block; parseAgentResult turns that text into a discriminated outcome (ok | err | malformed) and…src/adapters/claude-code/agent-result.test.ts— Railway result-parser test suite: ok/err/malformed tracks, last-block-wins, JSON + shape validation across every field type (string/number/boolean/string[]), both success and error trackssrc/core/railway.test.ts— Railway surface test suite: result() Output-contract rendering in compileAgent, delegate()/railway() builders, compileRailway orchestrator output, validateRailway static checks (unknown delegate…src/core/validate.test.ts— Validation test suite (node:test)src/cli.test.ts— CLI integration + E2E test suite (node:test)src/core/integrity.ts— Integrity check: SHA-256 hash verification for compiled markdown (detects hand-edits)src/core/sidecar.ts— Per-spec sidecar manifests at .vigiles/.inputs.json, used by session auditsrc/core/sidecar.test.ts— Tests for sidecar manifests, per-file hashes, and integrity checksrc/core/coverage.ts— Spec coverage analysis: linter rule coverage + npm script coverage with configurable thresholdssrc/core/coverage.test.ts— Coverage test suite (node:test)src/core/session.ts— Post-session audit: git diff analysis against spec surface areasrc/core/session.test.ts— Session lint test suite (node:test)src/core/hash.ts— Shared SHA256Hash branded type and assertNever exhaustive check helpersrc/core/orphans.ts— Orphan-docs detector (OPT-IN via the .vigilesrc.jsonorphansblock, off unless declared): finds .md files in a configured dir (defaultdocs/) that no other .md references.src/core/orphans.test.ts— Orphan-docs detector test suite (node:test)src/core/compose.ts— Sync-tool compatibility detector: detectSyncTools/composeCollisions — pure filesystem check that vigiles stays composable with Ruler (.ruler/, ruler.toml) and rulesync (.rulesync/) instead of…src/core/compose.test.ts— Sync-tool detector test suite (node:test): ruler/rulesync detection (dir + ruler.toml keys), source-slot paths, CLAUDE.md/AGENTS.md collision incl. path-qualified target match by filename, no-tool and non-overlapping no-collision, both-tools-presentsrc/test-coverage.ts— Untested-surface detector (the per-kind vigiles/untested-skill + untested-subagent + untested-hook rules — replacing the old umbrella untested-surface): finds skills/agents/hooks that ship with no…src/test-coverage.test.ts— Untested-surface detector test suite (vitest): colocation + content-reference coverage, command-only skills held to the requirement (no invocation-mode exemption), vigiles:ignore-test opt-out…docs/rules/untested-skill.md— Rule doc: untested-skill — a SKILL.md must ship with a test/eval; config, severity, scope (skills/*), the two coverage detectors, the explicit vigiles:ignore-test opt-out, why.docs/rules/untested-subagent.md— Rule doc: untested-subagent — a subagent (agents/*.md) must ship with a test/eval; the agent's contract (tool-contract / outcome) is what's tested, distinct from a skill's triggerdocs/rules/untested-hook.md— Rule doc: untested-hook — a file-backed hook script must ship with a test/eval (a runHook unit test is the natural one); scope = hooks referenced from plugin.json/settings.jsondocs/rules/unmarked-refs.md— Rule doc: unmarked-refs — the PostToolUse refs-hook that nudges the agent to MARK code-shaped references in instruction files (warn → non-blocking nudge, error → block, false → off); what it checks…docs/rules/subagent-tool-contract.md— Rule doc: subagent-tool-contract — cross-reference a subagent's tools: rail against the harness tool catalog (the cross-referencing engine).docs/rules/hook-events.md— Rule doc: hook-events — cross-reference a hook's event name against the harness event catalog (a typo never fires).docs/rules/subagent-frontmatter.md— Rule doc: subagent-frontmatter — two subagent-frontmatter defects, one rule: (1) a SUBAGENT missing name/description (won't register, no fallback); (2) an invalid model:/color: VALUE (a close typo…docs/rules/mcp-config.md— Rule doc: mcp-config — flag a declared MCP server that can't start (no command/url); FP-safe unambiguous check; reads .mcp.json + manifest mcpServers (Codex TOML not yet parsed); default warn; same…docs/rules/mcp-tool-resolves.md— Rule doc: mcp-tool-resolves — the MCP half of the tool-reference check: flag a subagent's mcp__server__tool whose server isn't in the plugin's declared mcpServers.docs/rules/mcp-hook-target-resolves.md— Rule doc: mcp-hook-target-resolves — cross-reference a type:mcp_tool hook action against declared mcpServers: flag an incomplete action (no server/tool, always) + a server the plugin doesn't declare…docs/rules/hook-script-exists.md— Rule doc: hook-script-exists — flag a hook command referencing a script file missing on disk (silently never runs). 🔴 The 'matches Anthropic's ownclaude plugin validate' claim that stood here was…docs/rules/disallowed-tools-contract.md— Rule doc: disallowed-tools-contract — the DENY-side mirror of subagent-tool-contract: a disallowedTools: block-list entry that's a close typo of a real tool blocks NOTHING (the tool stays available…docs/rules/description-overlap.md— Rule doc: description-overlap — flag two model-invocable skills with near-identical descriptions (the selector can't tell them apart → wrong one fires).docs/rules/frontmatter-valid.md— Rule doc: frontmatter-valid — flag a skill/agent --- block that EXISTS but isn't valid YAML (fields may not parse as intended).docs/rules/skill-frontmatter.md— Rule doc: skill-frontmatter — RECOMMEND (not require) explicit skill name+description for a reliable trigger surface; skills load without it (dir/first-paragraph fallback) so it's a best-practice…docs/rules/doc-refs.md— Rule doc: doc-refs — validate enforce()/file()/cmd()/ref() calls quoted inside markdown ```ts fences against the real linter catalog / filesystem / package scripts.src/core/doc-refs.ts— Markdown code-block ref validator (the opt-indoc-refsrule, default off): enforce()/file()/cmd()/ref() calls inside ```ts blocks, with vigiles:ignore opt-outsrc/core/doc-refs.test.ts— Doc-refs validator test suite (node:test)src/doc-refs-rule.test.ts— doc-refs WIRING test — drives the built CLI over a fixture with one broken ref across all three tiers: unconfigured (section absent, walk skipped), "warn" (prints ℹ, exit untouched), "error" (prints ✗, exit 2)src/core/symbols.ts— Cross-language symbol extractor (ast-grep): defines symbols a file declares (functions/classes/methods/constants) across JS/TS/Python/Ruby/Rust/CSS; fileDefinesSymbol with .d.ts/.rbi fallbacksrc/core/symbols.test.ts— Symbol extractor test suite (node:test)src/core/refs.ts— Symbol reference verification: thevigiles:symbol path#namemark (verify the named file defines the symbol) + unmarkedCodeRefs detection. collectRefIssues (shared by thevigiles refsCLI and the…src/core/refs.test.ts— Symbol reference verification test suite (node:test)src/mock-model.ts— Scriptable, dependency-free Anthropic Messages SSE mock (startMock/scriptModel) — point real claude at it via ANTHROPIC_BASE_URL for deterministic harness tests; extractRequest + onRequest capture…src/harness-test.ts— Deterministic harness testing: runHarnessTest(spec, { adapter = claudeCodeAdapter }) runs the adapter's real binary + real hooks/settings against a scripted mock model, dispatching the…src/harness-test.test.ts— Harness-test suite (node:test, skips without claude)src/sandbox.ts— Safe-by-default confinement: decideSandbox is the pure policy (untrusted plugin code never runs unconfined unless sandbox:false), runSandboxed co-launches the mock + claude inside one bubblewrap…src/sandbox.test.ts— Sandbox test suite: pure policy/trust/args/log-parse coverage + a gated end-to-end test proving a sandboxed run blocks network egress while the in-sandbox mock stays reachable (skips without…src/mock-entry.ts— In-sandbox mock entry: run as a subprocess inside the bwrap netns so the scripted mock lives on the isolated loopback; streams captured requests to a file the parent reads back for trace.modelRequestssrc/run-hook.ts— Hook unit tier: runHook pipes a synthesized event JSON to a hook process (no claude, no model) and reports exit code + normalized block/allow decision — the cheap base of the pyramid, and the only…src/hook-state-store.ts— The named-state STORE — where a compiled hook'srecord()ed facts live on disk (.vigiles/state/<hook's dir>/<key>.json), and the ONE seam a test seeds them through.src/hook-state-store.test.ts— Named-state store suite (vitest): the path derivation (mirrored inside the root, hashed-and-isolated outside), a corrupt/wrong-shaped entry reading as null rather than fresh, the write round-trip…src/run-hook.test.ts— Hook unit-tier test suite (node:test): pure decision logic + real shell hooks across exit codes, stdin event passthrough, env injection, JSON permission decisions, runHookWith sandbox + egress…src/egress.ts— Allowlisted real egress (egress: { allow }): the in-between between deny-all and recordEgress — let a hook reach ONLY the listed hosts, boundary at the packet layer (an nft policy-drop chain on…src/egress-entry.ts— Allowlisted-egress orchestrator subprocess: runHook is sync (spawnSync) but egress needs bwrap + slirp4netns alive at once, so the parent spawnSyncs this entry — it spawns bwrap (--info-fd → child…src/egress.test.ts— Egress allowlist test suite: pure helpers — parseGetent family split, resolveAllow (injected resolver), parseResolvers, buildEgressNft (policy drop, DNS allow, per-host v4/ip6 rules, comment…src/eval.ts— Harness eval API: runEval drives the real claude CLI across arms x trials and aggregates mean ± se (variance) + cost/latency/token usage; bounded concurrency (runPool) + rate-limit backoff +…src/eval.test.ts— Eval aggregation/formatting + variance + usage/cache test suite (node:test)src/eval-cache.ts— Eval record/replay cache — the LOCAL-SPEED half of eval staleness (gitignored, throwaway; the COMMITTED CI half is src/eval-lock.ts — keep them distinct). cacheKey hashes the model-affecting inputs…src/eval-cache.test.ts— Eval-cache test suite (node:test): key stability/sensitivity, record round-trip + malformed-record tolerance, filesystem snapshot/restoresrc/eval-lock.ts— Eval LOCK — the CI STALENESS GATE for evals run on a subscription (distinct from the local eval CACHE: a COMMITTED integrity stamp, not a gitignored speed cache — the founder's 'it's more of an…src/eval-lock.test.ts— Eval-lock unit suite (vitest): evalInputsHash stable/order-independent + sensitive to every model-affecting field, the harness version is NOT in the hash, lockSlug fs-safe…src/eval-lock-wiring.test.ts— Eval-lock WIRING suite (vitest): the lock end-to-end through runEvalWith + measureTriggerRateWith with an INJECTED runner (no model) — off drives the runner, update writes a committed lock, check…src/stats.ts— Significance testing for eval A/B arms: Welch's t-test over the per-arm summary stats (mean/se/n, no raw rows) → two-sided p-value + verdict (Numerical-Recipes incomplete beta); compareArms computes…src/stats.test.ts— Stats test suite (node:test): incomplete-beta vs known closed forms, p-values vs t-table critical values, Welch significant/noise/deterministic casessrc/plugin-loader.ts— Plugin/repo harness loader (harness-agnostic, composition root): loadPlugin(path, layout) + resolveHarness(opts, layout) — layout REQUIRED, injected via the PluginLayout port (src/core/layout.ts)…src/adapters/claude-code/plugin-loader.ts— Thin Claude Code wrapper over the harness-agnostic src/plugin-loader.ts: loadPlugin(path, layout = claudeCodeLayout) + resolveHarness(opts, layout = claudeCodeLayout) supply the CC layout as the…src/adapters/claude-code/plugin-loader.test.ts— Plugin-loader test suite (node:test): CLAUDE_PLUGIN_ROOT resolution, CLAUDE.md/skills/agents/commands materialization, surface + empty-machine + MCP warnings, settings merge, in-repo dogfoodsrc/adapters/claude-code/vendor.test.ts— Conformance suite over REAL vendored plugins under test/dogfood/: model-free, in-gate, table-driven loadPlugin invariants (loads a surface, ${CLAUDE_PLUGIN_ROOT} resolves, skills materialize…src/adapters/claude-code/skills-dogfood.test.ts— Dogfood conformance over vigiles's OWN plugin (.claude-plugin/): loadPlugin materializes every skills//SKILL.md and each has a name + non-empty description (the trigger surface); plus a…src/agent-plugins-manifest.test.ts— Agent Plugins conformance dogfood (vitest, offline): vigiles's OWN root plugin.json is held to the agent-plugins.org 1.0.0 schema — $schema pinned to the canonical identifier, the name pattern, the…src/harness-assert.ts— Runner-agnostic harness helpers: withHarness (auto-cleanup), throwingassert*helpers incl. assertHookBlocked/assertHookAllowed (over a runHook RESULT) + assertHookDenies/assertHookAllows (over a…src/harness-assert.test.ts— Harness-assert test suite (node:test): eval delta helpers + matcher pass/fail logicsrc/check.ts— The declarative check vocabulary (exposed on thevigilesroot surface — exceptjudged, which calls a model and lives onvigiles/evalaspaid_judged; the standalonevigiles/checksubpath…src/check.test.ts— Check-vocabulary test suite (vitest): each check's pass/fail + 0–1 score + ACTIONABLE failure message (the message is the product, tested as much as the verdict) + toJSON round-trip; evalChecks…src/arg-match.ts— Shared ArgMatcher over a tool call's input (dot-path keys; RegExp = pattern, primitive = exact; AND across keys) + matchesArgs/getPath/stringifyValue/describeArgs/serializeArgs.src/tool-intercept.ts— Tool interception — the eval-tier half of the tool-call spy.src/tool-intercept.test.ts— Tool-intercept test suite (vitest): decideIntercept (unconditional/when-scoped/first-match-wins/default reason), interceptHookDecision (PreToolUse event parse + malformed tolerance)…src/judge.ts— Thin LLM-as-judge for the eval tier: judge() grades an output against a rubric with a model (synchronous, for use inside measure); parseJudgeOutput is the pure, testable verdict parsersrc/judge.test.ts— Judge verdict-parsing test suite (node:test): result-field unwrap, prose-wrapped JSON, threshold, clamping, unparseable fallbacksrc/vitest.mts— Opt-in vitest integration entry (ESM, since vitest is ESM-only): registers vigilesMatchers + augments @vitest/expect Matchers so toHaveCreated/toBeatBaseline type-check; vitest is an optional peer depsrc/jest.ts— Opt-in jest integration entry (CJS): registers vigilesMatchers + augments @jest/expect Matchers; jest is an optional peer deptest/types/smoke.vitest.ts— Type-level constraint:vigiles/vitestmakes the matchers type-check on vitest's expect (tsc --noEmit via npm run test:types)test/types/smoke.jest.ts— Type-level constraint:vigiles/jestmakes the matchers type-check on jest's expecttest/runners/matchers.vitest.mjs— Cross-runner constraint: vigilesMatchers + helpers register and pass under vitest (proves runner-agnostic;src/*.test.tsexcluded via vitest.config.mjs)test/runners/matchers.jest.cjs— Cross-runner constraint: the same vigilesMatchers register and pass under jest (CommonJS dist required natively; scoped via jest.config.cjs)src/core/test-utils.ts— Shared test utilities: makeTmpDir, makeSpec, cleanupTmpDir, initGitReposrc/core/types.ts— Shared types: RulesConfig, VigilesConfig, FreshnessMode, CoverageThresholdssrc/core/rule-meta.ts— The RULE METADATA registry (the ESLint-metapattern adapted to vigiles's shared-detector architecture): ONE Record<RuleName, RuleMeta> keyed by rule name — NOT scatteredexport const meta…src/core/rule-meta.test.ts— Rule-meta coverage suite (vitest): the registry's keys EXACTLY match docs/rules/*.md (every rule has a meta AND a doc — the set is one thing), each defaultSeverity matches the real exported…src/core/proofs.ts— Deterministic proof algorithms (monotonicity lattice, NCD, Bloom filter, Merkle DAG, fixed-point, property testing)src/core/evolve.ts— Evolution engine: mutation operators, fitness function, proof-gated selectionsrc/core/proofs.test.ts— Proof system + evolution engine tests (node:test)CLAUDE.md.spec.ts— This file — the source of truth for CLAUDE.mdexamples/SKILL.md.spec.ts— Example SKILL.md specexamples/railway/ship-pr.md.spec.ts— Dogfood: a railway() over five flat agent() workers (planner→implementer→reviewer, bounded fixer recovery, reporter error track), each with a result() contract.examples/harness/hook-unit.harness.mjs— Canonical hook unit-tier example (runHook): test a hook's logic in isolation with no claude CLI — the cheap base of the pyramid; runs in CI for freeexamples/harness/policy-gate.harness.mjs— Canonical deterministic harness test (runHarnessTest): a PreToolUse Bash policy gate (block-no-verify shape) + a SessionStart setup hook (obra/superpowers shape)examples/harness/refs-nudge.harness.mjs— Real-claude dogfood of the refs-hook (deterministic tier, no API key): the mock model Writes a CLAUDE.md naming an unmarked linter rule, the PostToolUse refs-hook fires, and requestContains asserts…examples/harness/skill-outcome.eval.mjs— Canonical skill-outcome eval (runEval): does a skill change the agent's output? — the question you ask of any SKILL.mdexamples/harness/skill-trigger-rate.eval.mjs— Canonical trigger-rate eval (measureTriggerRate): does a skill's description actually FIRE across varied prompts? — installs a real pinned plugin via pluginDir and reuses the skillResolved predicateexamples/harness/skill-selection-collision.eval.mjs— Canonical selection-collision eval (measureSelectionMatrix + assertNoCollision, on vigiles/claude-code): installs a multi-skill plugin, runs each skill's own prompts against the WHOLE set, and gates…examples/harness/dogfood/test-harness.trigger.eval.mjs— Dogfood trigger eval — vigiles's OWN test-harness skill: fires on harness-testing requests (recall) AND stays quiet on unrelated coding (precision via irrelevantPrompts), gated by…examples/harness/dogfood/strengthen.trigger.eval.mjs— Dogfood trigger eval — vigiles's OWN strengthen skill (made model-invocable so the agent reaches for guidance()→enforce() upgrades on its own): fires on strengthen/harden-my-rules requests (recall)…examples/harness/dogfood/edit-spec.trigger.eval.mjs— Dogfood trigger eval — vigiles's OWN edit-spec skill (made model-invocable; the skill the pre-edit hook points at when a direct CLAUDE.md edit is blocked): fires on 'change my CLAUDE.md / add a…examples/harness/dogfood/debug-my-harness.trigger.eval.mjs— Dogfood trigger eval — vigiles's OWN debug-my-harness skill: fires on 'why did my skill stop firing / why didn't my hook block / debug my harness' requests (recall) AND stays quiet on ordinary…examples/harness/checks-dogfood.harness.mjs— Dogfood of the new check vocabulary on the DETERMINISTIC tiers (no key, RUNS in CI): assertChecks over a real runHook (blocked/allowed) + a real runHarness mock run (turns/output).examples/harness/dogfood/skill-quality.eval.mjs— Dogfood of the SCORED check evaluator on vigiles's OWN strengthen skill: measure({ checks: [skill, judged, cost] }) + assertRates — does the skill FIRE, is its suggestion GOOD (model-graded judged)…examples/harness/dogfood/skill-firing-cheap.eval.mjs— Dogfood of the CHEAP firing-only path: measure({ checks: [skill, latency], stubSkillBodies: true }) on vigiles's OWN strengthen skill — the sibling of skill-quality.eval.mjs that stubs each skill…examples/harness/subagent.eval.mjs— Dogfood of the subagent nested-trace check on a REAL subagent — the vendored oh-my-claudecode code-reviewer agent (vigiles ships no subagents). measure drives a task that dispatches it via Task…examples/harness/dogfood/generate-logo.trigger.eval.mjs— Dogfood trigger eval — vigiles's OWN generate-logo skill, an INTERNAL contributor-only skill (lives under .claude/skills/, this repo's own harness, NOT shipped to consumers) loaded via skillsDir: a…examples/harness/skill-compression.eval.mjs— Worked eval verifying a token-compression claim (e.g.examples/harness/plugin-cohesion.harness.mjs— Canonical cohesion test (runHarnessTest with plugin:): load a whole plugin (.claude-plugin/plugin.json + CLAUDE.md) and assert multiple hooks fire togetherexamples/harness/railway-result.harness.mjs— Worked example of the railway/Result payoff — asserting a subagent's TYPED OUTCOME deterministically (the deterministic-assert-replaces-an-LLM-judge thesis made runnable).examples/harness/effect-boundary.harness.mjs— Worked example of the SIDE-EFFECT BOUNDARY as a deterministic test (rung 2 of the typed-contracts ladder): a unit that declares it writes only out.txt and never pushes is ASSERTED to stay inside…bench/evals/refs-hook.eval.mjs— Worked eval reproducing benchmark #4 (forcing symbol marks → verifiable references?) as a runEval library callsrc/core/hook-providers.ts— Hook CONTEXT PROVIDERS — lets a compiled gate decide on EXTERNAL STATE vianeeds/e.ctxwithout breaking capability=API-surface: the pure decide() never fetches; the TRUSTED runtime gathers a…src/core/hook-providers.test.ts— Hook context-provider suite (vitest): gatherContext runs ONLY declared providers + parses each fact (branch trim, porcelain→dirty bool, cwd ambient), a clean tree isn't dirty, gathering NEVER throws…docs/compiled-hooks.md— Public guide to COMPILED HOOKS: author a hook as a pure typed(event)=>Decisionagainst the closedvigiles/hookvocabulary and compile it.src/eval-baseline.ts— Eval regression gating (Phase C): record a run's EvalReports to a committed .vigiles/eval-baseline.json, then flag any arm×metric that moved SIGNIFICANTLY in the bad direction vs that baseline…src/eval-baseline.test.ts— Eval-baseline test suite (node:test): baseline round-trip + version/shape validation, regression vs improvement vs unchanged classification, lowerIsBetter direction flip, skip of absent…docs/harness-testing.md— Harness-testing guide (harness-AGNOSTIC core): the levels (refs/unit/integration/eval), select-by-import + { adapter } default-Claude-Code, the Trace model + predicates/assertions…docs/harness-testing-claude-code.md— Harness-testing — Claude Code specifics: the oh-my-claudecode Tier 0-3 walkthrough, ${CLAUDE_PLUGIN_ROOT}/hooks.json/plugin/pluginDir/Skill-tool, scriptModel + the Anthropic-Messages mock…docs/harness-testing-codex.md— Harness-testing — Codex specifics: runHarnessTest(spec, { adapter: codexAdapter }) against real codex exec (keyless, Responses mock via startCodexMock); the what-maps (AGENTS.md instructions…docs/harnesses.md— User-facing harness-adapter guide: which harness vigiles targets (Claude Code now, Codex likely next) and how a consumer picks one — by importing vigiles/claude-code beside the harness-agnostic…docs/adapter-api.md— vigiles/adapter API reference: every export of the authoring kit — the five port interfaces field-by-field (with Claude Code AND Codex example values), HarnessAdapter + the detect specificity…docs/authoring-an-adapter.md— Third-party adapter authoring guide: the documented small lib (vigiles/adapter) for teaching vigiles a new harness — the five ports to implement, a worked myHarnessAdapter skeleton, validating with…docs/testing-matrix.md— Testing matrix: every harness-testing use case mapped to its test tier (unit / cross-runner / type / integration-CI) and file, plus why the CLI examples are .mjs and the API is TypeScriptdocs/agent-setup.md— Agent setup & workflows — one guide: whatinitdoes + auto-detection, per-agent recipes (Claude Code / Codex / multi-agent / Cursor), non-interactive setup + fallback hooks, the recommended agent…docs/spec-format.md— Spec format reference (target, sections, rules)docs/railway-subagents.md— Public guide to railway-oriented subagents: the typed Result<ok,err> outcome contract (result() on an agent()), what it compiles to (vigiles:ok/err blocks), composing flat workers…docs/linter-support.md— Linter support details (11 catalogs + generate-types/generate-schema)docs/comparison.md— Before/after tables (Claude Code, Codex), determinism breakdown, flow diagramdocs/rules/require-instructions-spec.md— Rule doc: require-instructions-spec — require a .spec.ts behind each CLAUDE.md/AGENTS.md (NARROW: only a .spec.ts satisfies it, not inline/frontmatter). workflow-group opt-in (--strict); satisfied…docs/rules/require-skill-spec.md— Rule doc: require-skill-spec — the consistent require--spec parallel (default OFF).docs/rules/integrity.md— Rule doc: integrity check (SHA-256 hash verification for compiled markdown)docs/rules/coverage.md— Rule doc: spec coverage thresholds (scripts, linter rules)docs/markdown-mode.md— Markdown mode: the single no-spec on-ramp doc — inline<!-- vigiles:enforce -->comments (the live zero-TS floor; the former inline-mode.md reference is folded in here).skills/linter-docs/eslint.md— ESLint reference: plugin table, AST selectors, type-aware rules, auto-fix, edge casesskills/linter-docs/rubocop.md— RuboCop reference: gem table, node pattern DSL, auto-correct, custom copsskills/linter-docs/pylint.md— Pylint reference: plugin table, astroid AST, type inference, custom checkersskills/linter-docs/ruff.md— Ruff reference: 800+ reimplemented rules, rule selection, auto-fix, pyproject.toml configskills/linter-docs/stylelint.md— Stylelint reference: plugin table, PostCSS AST, custom rules, CSS-in-JS, SCSSskills/strengthen/SKILL.md— Strengthen skill (MODEL-INVOCABLE): upgrade guidance() → enforce() by finding existing linter rules.skills/edit-spec/SKILL.md— Edit-spec skill (MODEL-INVOCABLE): change a compiled CLAUDE.md/AGENTS.md by editing its .spec.ts (add/modify/remove a rule, section, command, key file) — the skill the pre-edit hook points at when a…skills/debug-my-harness/SKILL.md— Debug-my-harness skill (MODEL-INVOCABLE): diagnose why the harness misbehaved by reading the local flight-recorder ledger (.vigiles/runs.jsonl) — skill fires/collisions, hook decisions (blocked vs…skills/linter-docs/SKILL.md— Linter-docs reference skill (user-invoked): a SKILL.md indexing the per-linter reference files (eslint/ruff/pylint/rubocop/stylelint/clippy .md) the strengthen + edit-spec skills read when matching…
Commands
npm run build— Compile TypeScript to dist/npm test— Build and run all testsnpm run fmt— Format with prettiernpm run fmt:check— Check formattingnpm run lint— Run ESLint on src/npm run test:harness— Build + run the deterministic harness tests (*.harness.mjs) on the in-repo CLI — needs the claude CLI, no API keynpm run test:eval— Build + run the real-model harness evals (*.eval.mjs) on the in-repo CLI — needs claude + model authnpm run test:vitest— Build + run the cross-runner matcher constraints under vitest (test/runners/*.vitest.mjs)npm run test:jest— Build + run the cross-runner matcher constraints under jest (test/runners/*.jest.cjs)npm run test:types— Build + type-check the vitest/jest matcher augmentation (tsc --noEmit on test/types/)
Rules
No Non Null Assertion
Enforced by: @typescript-eslint/no-non-null-assertion
Why: Use proper narrowing instead of ! assertions.
No Floating Promises
Enforced by: @typescript-eslint/no-floating-promises
Why: Always await or return promises. Unhandled rejections crash the process.
Cognitive Complexity
Enforced by: sonarjs/cognitive-complexity
Why: Keep functions under 15 cognitive complexity. Split complex logic into helpers.
Core Not Adapter
Enforced by: boundaries/dependencies
Why: Hexagonal boundary: the reference-verification domain (verify-core) must not import the Claude Code harness/transport adapter (cc-harness). The core stays harness-agnostic so a future vigiles/ (e.g. Codex) can sit beside the Claude Code one. Dogfoods enforce() over eslint-plugin-boundaries.
Experimental Names Carry The Warning
Guidance only — If we EXPORT it and it is not stable, its NAME starts with experimental_. Declare it once with the @experimental TSDoc tag; the ESLint rule local/experimental-name (eslint-rules/experimental-name.mjs) fails if a tagged, exported declaration lacks the prefix, so the tag and the name cannot drift. It checks EVERY exported declaration, public or not — the earlier version exempted internals, which contradicted its own reason for existing (an internal reader is still a reader who trusts the name). That exemption was also the only thing making the check cross-file; without it the check is jsdoc + name, both in one file, which is why it is a lint rule now and not a script. @internal is a DIFFERENT question and stays a script (scripts/check-internal-tag.mjs, npm run internal:check): it does NOT mean unstable, it answers whether something is part of the API at all, and an @internal symbol appearing in api-surface/*.api.md is a contradiction — deciding that needs the export graph, which no single file knows. Types are out of scope (the convention is about call sites; a type annotation is not one). Opt out on the declaration with vigiles:experimental-name-ok <reason> — the reason is required and must be on the same line. WHY the name and not just a tag: a tag is invisible where it matters, while a name is in every call site, diff, review and autocomplete. Learned the hard way — skill() shipped under a stable name while docs/skills.md opened with "skill() is experimental", and nothing caught it because the convention was a habit rather than a check. There is no vigiles/experimental subpath any more either (deleted 2026-08-21): it marked the import line, which is out of view by the time anyone reads the call. Renaming an experimental_ symbol is NOT a breaking change and gets no major bump; that is what the prefix promises. COROLLARY — ONE EXPERIMENTAL ROOT PER FEATURE (2026-08-21): the prefix says whether THIS name is stable and cannot say whether the name a STABLE symbol depends on is stable. That gap shipped — railway(), delegate() and result() carried stable names while being meaningless without experimental_agent(), and local/experimental-name cannot see it (tag-vs-name on ONE declaration, not the dependency between two). So the vocabulary of an experimental feature hangs off ONE marked root: experimental_agent.result() / .railway() / .delegate() / .pipe() / .pipeStep() / .needs() / .start() / .andThen(), the same chokepoint as experimental_skill.input(). It buys three things, and only the first is unreachable by the prefix alone: no stable name is left to rest on an unstable one; collision-prone words (result, needs, start, pipe) stop owning package-level names; and the warning SURVIVES DESTRUCTURING (const { result } = experimental_agent still names the root at the binding site) — unlike a namespace OBJECT or an import subpath, which is why vigiles/experimental was retired rather than reused. Mechanically Object.assign(fn, {…}), NOT a TS namespace: @typescript-eslint/no-namespace is an error here, and a namespace merges only with function declarations while experimental_skill is a const. Types stay top-level for the same reason the prefix rule excludes them. See STABILITY.md.
Never Skip Tests
Guidance only — All tests must pass. If a test requires a CLI tool (pylint, rubocop, ruff, clippy), install the tool, don't skip the test.
No Silent Skips
Guidance only — A skip must be LOUD, never a silent green. When a test/script genuinely can't run (e.g. the deterministic tier with no claude), it must report ⊘ SKIPPED (its own status, tallied separately as 'N skipped'), not exit 0 and masquerade as a ✓ passed. vigiles test/vigiles eval classify each script pass/skip/fail by exit code (0 / SKIP_EXIT_CODE 77 / else); scripts call skip(reason) (vigiles) to emit it. By default a skip does NOT fail the run — tiers need different capabilities (claude, bubblewrap, model auth) that aren't all present everywhere, so failing-by-default would make the command red in any partial environment (this repo's own non-privileged harness job legitimately skips the egress tier and runs it under e2e). But where you ASSERT the capability is present (a CI job), pass vigiles test --no-skip: a skipped tier is untested surface and FAILS — a green-with-skips is itself a hidden gap. Equally important — DON'T over-skip: there is no blanket claude-gate, so unit-tier runHook tests (which need no model) always run; only the tiers that truly need a capability skip, and they say so. The corollary of never-skip-tests: if you can't make it pass, surface WHY, don't hide it.
Zero Config By Default
Guidance only — vigiles compile should work with just a .spec.ts file. Config exists only for overrides (maxRules, maxTokens).
Dont Reimplement Linters
Guidance only — Architectural linting belongs in ast-grep/Dependency Cruiser/Steiger. Per-file code rules belong in ESLint/Ruff/Clippy. vigiles owns: spec compilation, linter cross-referencing, type generation, stale reference detection, and proof-based spec evolution.
Prefer Existing Solutions
Guidance only — Before building any non-trivial capability — a tool, a check, a runtime, a parser — LOOK FOR EXISTING SOLUTIONS FIRST and PREFER adopting/composing/delegating over reinventing. This is a process step, not a vibe: actually search the landscape (the web, the ecosystem, the stdlib) and write down what you found. The decision order is ADOPT > COMPOSE > BUILD. BUILD is justified ONLY when one of these holds, and you SAY WHICH: (1) no existing solution fits the exact need (after a real look); (2) building DOGFOODS or reinforces the core cross-referencing engine (e.g. self-command-refs reuses the reference-verification engine vigiles already ships, so it's ~120 lines, not a new dependency); (3) every existing option's cost — a heavy dependency, a runtime/env requirement, lock-in, lost narrative control — outweighs a small purpose-built piece. When you DO build, the prior-art comparison IS part of the justification: name the existing solutions and why each was rejected (as the self-command-refs decision did — doctest runs everything/needs a build, doc-generators kill hand-written prose, link-checkers don't cover command names, so a small deterministic cross-ref check that dogfoods the engine won). Never present a home-grown thing as novel without having checked; 'is there an existing solution?' is a question to answer with research, before writing code, not after. This generalizes the project's existing don't-reinvent stance — dont-reimplement-linters (delegate architectural/per-file linting to ast-grep/ESLint/Ruff) and compose-with-sync-tools (compose with Ruler/rulesync, don't absorb their job) — into the default reflex for ALL new work.
Parse Structured Input With A Real Parser
Guidance only — Parse a STRUCTURED language with its REAL parser, never a hand-rolled regex/line-scanner. This repo already lives this everywhere: Bash → mvdan-sh AST (bash-effects.ts), code → @ast-grep/napi (symbols.ts), YAML → js-yaml, TOML → @iarna/toml. Markdown was the LONE holdout — five detectors (adopt/compile/refs/skill-resources/segment) each hand-rolled the SAME inFence = !inFence fence toggle, which is WRONG on nested/unbalanced fences (a 4-backtick block containing a bare ``` line flips the toggle to 'outside' and leaks a ## inside the block out as a real heading — demonstrated to mis-split + drift bytes on round-trip). Fixed by routing ALL FIVE through ONE markdown-it-backed oracle (src/core/markdown.ts fencedLineFlags) — the make-invalid-states-irrepresentable choke-point, so no detector can hand-roll fence state again. THE RULE: block-structure / stateful markdown parsing (fence-awareness, heading boundaries, list/blockquote nesting — anything tracking parser STATE across lines) MUST go through the shared markdown-it helper; a within-line token match (is THIS one line a fence delimiter, does a line contain a link) MAY stay a local regex (it tracks no state, so it can't have the toggle bug). This is a direct instance of prefer-existing-solutions (markdown-it is a small, zero-dep, browser-safe CommonMark parser — ADOPT beat BUILD once the failure was proven to be exactly one construct, fence boundaries) and the same 'delegate parsing to the language's real parser' reflex as dont-reimplement-linters. When you need a NEW piece of markdown structure, extend src/core/markdown.ts (token.map line ranges preserve verbatim slicing, so a faithful round-trip is kept — never AST-reserialize, which reflows whitespace), never a sixth private fence loop.
Dogfood Vendoring Policy
Guidance only — The dogfood corpus — real/vendored artifacts vigiles tests ITSELF against (real OSS plugins, real instruction files, gold sets, task corpora) — follows ONE policy. The rules: (1) SHA-PIN — a vendored upstream lives in test/dogfood/<name>@<sha>/, reproducible + offline. (2) MIT-ONLY — only MIT upstreams are vendored verbatim; a no-license/non-permissive repo is NOT committed. (3) PROVENANCE PER SLICE — every vendored slice ships BOTH a LICENSE (upstream MIT text + copyright) AND a SOURCE (upstream URL/path/commit + why-it's-here + the minimal slice); a slice without both is incomplete. (4) MINIMAL SLICE — vendor the smallest slice that reproduces the behaviour under test, never the whole repo; trim manifests so the loader sees a coherent plugin. (5) CI-ENFORCED OR LABELLED-MANUAL — a dogfood artifact CI never reads is decoration, not a guard: it must be read by a src/**/*.test.ts (→ npm run coverage) or run by a dedicated CI step (the rule-enforcer/ trust gate and the bench/corpus/verify*.mjs self-checks each have one in the check job), OR be explicitly labelled MANUAL with the reason (real-model evals/benchmarks that cost money — examples/harness/dogfood/*.eval.mjs, bench/ecosystem/**). (6) REFRESHABLE — re-pin via tools/refresh-vendor.sh, breadth-sweep via tools/dogfood-sweep.sh (both human-run, never CI — they clone the network). When you add a new vendored corpus, it follows this same shape and gets a row in the index. DON'T CONFUSE THREE THINGS called 'dogfood' — only test/dogfood/ is THIS corpus; examples/harness/dogfood/ are skill-EXAMPLES, and rule-enforcer/gold/ is that package's own gold sets, none governed by this rule. PER-ADAPTER: the ADAPTERS are symmetrically enforced (src/adapter-contract.test.ts loops the whole registry), but the VENDORED corpus is CC-only (real Claude Code plugins) with Codex on artificial tmp fixtures (src/scan-cli.test.ts) — a documented asymmetry (real Codex plugins are rare), tracked in the roadmap, not an oversight.
One Detector No Drift
Guidance only — A DETERMINISTIC structural check has ONE home: a single pure detector reused by BOTH lint (the per-commit, severity-configurable gate) and audit (the read-only report) — never reimplemented in each, so the two surfaces can't drift. The detectors already shared this way: untested-surface (src/test-coverage.ts), dangling-ref (danglingRefs in src/plugin-loader.ts), and description-overlap (findDescriptionOverlaps in src/core/description-overlap.ts, the NCD near-duplicate proxy). When a deterministic signal earns a lint rule, the rule and audit must call the SAME function (plus a docs/rules/<name>.md per the doc-per-rule rule). The model-gated BEHAVIORAL column (the audit model trigger tier / runEval trigger-rate, observed egress) is the deliberate EXCEPTION: it's LLM-based — needs model auth, costs tokens/quota, is non-deterministic — so it lives ONLY in the audit/eval tier and is NEVER promoted to a lint rule (lint stays deterministic, free, runnable on every commit with no key). The dividing line is the tool's core axis: cheap deterministic DETECTION can be a lint rule; expensive model-gated CONFIRMATION cannot. So two near-identical skill descriptions are flagged deterministically (lint/audit), but whether the wrong one actually FIRES is measured only by the model trigger tier (the audit trigger tier, interactive — or measureTriggerRate for automation).
Lint Rule Calibration
Guidance only — Every deterministic check sits at the STRONGEST enforcement its DECIDABILITY allows — and no further, because going further is itself the bug (false confidence or crying wolf). Prevention is a GRADIENT, not a switch: unrepresentable → won't-typecheck → won't-build → error → warning → measured, weakening left-to-right as the property gets less decidable / less self-contained. Three best-practice results FIX where a given defect can sit: 'make illegal states unrepresentable' (Minsky) works ONLY for structural+decidable+closed-vocab properties; 'parse, don't validate' (King) relocates a check for anything touching the external world to one boundary but never deletes it; Rice's theorem makes any behavioral property (does this skill FIRE?) undecidable statically — measurable only. So every rule lands in exactly ONE of three BUCKETS, and the bucket sets the CEILING: (A) structural-closed — decidable from the artifact's own content over a closed vocabulary; a TYPE could make it impossible for spec-authors → ceiling is won't-typecheck, error-capable (e.g. subagent-tool-contract, skill-missing-fence, hook-block-ineffective). (B) external-decidable — decidable but needs the EXTERNAL world (filesystem, linter catalog, another file/server); NO type can read those, so the ceiling is a hard ERROR at compile-cross-ref or lint, NEVER a type (e.g. hook-script-exists, mcp-tool-resolves, integrity). (C) heuristic-behavioral — undecidable or a fuzzy proxy; the ceiling is a WARNING or a model-MEASUREMENT, and an error here CRIES WOLF (e.g. description-overlap's NCD proxy, frontmatter-valid's stricter-than-the-loader YAML, unmarked-refs). The reframe that kills 'why isn't it all types?': bucket A wants a type, but bucket B is type-IMPOSSIBLE (yet still a hard error — equally gating, just later) and bucket C is warn-CORRECT (not a compromise). SEVERITY TRACKS CONFIDENCE, NOT IMPORTANCE — a clean fact (A/B) can be error; a proxy (C) must be warn. The bucket is the CEILING; defaultSeverity is where the rule sits TODAY — a gap is meaningful: an A/B rule at warn is a PROMOTION CANDIDATE (deterministic, rolling out, e.g. lethal-trifecta), a C rule at warn is PERMANENT. This is the basis of the structural(error)/nudge(warn) split in install-enforcement-model. MECHANICALLY ENFORCED: every rule MUST be declared in src/core/rule-meta.ts (the ESLint-meta pattern adapted to vigiles's shared-detector architecture — ONE registry keyed by rule name, NOT scattered export const meta, because one detector feeds many rules) with its bucket / surface / defaultSeverity / detector / optional upstreamPrevention; Record<RuleName, RuleMeta> makes completeness a tsc error, and src/core/rule-meta.test.ts binds the registry to docs/rules/*.md (exact set match) + cross-checks each defaultSeverity against the real DEFAULT_RULES. The honest reframe of the apparent 'spec rules vs markdown rules' fragmentation: NO lint rule reads a .spec.ts's content — the whole rule set is ARTIFACT-targeted (it reads what SHIPS), plus two meta-rules (require-instructions-spec checks adoption EXISTS, integrity checks the artifact still matches its hash). Spec correctness is a SEPARATE pipeline (Stage 1 TYPE = the user's tsc; Stage 2 COMPILE = vigiles compile), which fires earlier and harder; a lint rule that re-checked the spec would just duplicate tsc. So we NEVER move a rule onto specs — the same defect can be PREVENTED at Stage 1 for spec-authors (recorded as RuleMeta.upstreamPrevention) AND DETECTED at Stage 3 for everyone else (defense-in-depth across two populations, not duplication). Adding/calibrating a rule is not done until rule-meta.ts + enforcement-model.md are updated alongside the docs (sibling of rules-docs-in-sync).
Harness Parity And Extensibility
Guidance only — When you design ANY capability — not just a lint rule or a test, but compile EMIT, a runtime, a new port, a new subsystem — two constraints are NON-NEGOTIABLE and apply at DESIGN time, before you write code. (1) FULLY SUPPORT BOTH SHIPPING HARNESSES — Claude Code AND Codex — as equals. Never build CC-first and bolt Codex on after: if a capability maps to both, it must WORK on both and be asserted on both (the test-both-harnesses arm); the emitted artifact must be each harness's native format (CC JSON settings / Codex TOML); the runtime must be the shared neutral path where one exists (e.g. the gate deny→exit 2 is byte-identical). Where a capability genuinely cannot map to a harness yet, that is a LOUD, documented deferral (a compile warning + a gated/skipped test stating WHY — e.g. hook REACT output shape is CC-confirmed-only; inject is now confirmed on BOTH and encoded in HookProtocol.injectableEvents), NEVER a silent CC-only path. 'Both harnesses fully' is the bar, not 'CC plus best-effort Codex'. (2) LEAVE ROOM FOR FUTURE ADAPTERS (OpenCode, Gemini, …) by construction. Every harness-specific fact lives behind one of the five ports (HarnessDialect / PluginLayout / HarnessRuntime / HookProtocol / ModelMock); the core stays harness-agnostic (core ⊄ adapter, eslint-enforced) and reads facts from the injected adapter, never a hard-coded literal. Prefer a PORT + INJECTION over an inline conditional, so adapter #3 is a NEW OBJECT registered in ADAPTERS, not an edit threaded through the core. (3) DESIGN THE NEUTRAL SHAPE FIRST, defer the abstraction until it's earned. Model the capability in harness-neutral terms and let the dialect/port supply the per-harness specifics; do NOT invent a neutral indirection layer for a difference that doesn't exist yet (rule-of-three / YAGNI / leaky-abstraction risk) — e.g. native event names are validated per-dialect now, and a neutral event-alias map is deferred to the dialect seam until a harness with DIVERGENT names lands. The seam must EXIST (the port); the mapping is added when a second divergent implementation needs it. This is the DESIGN-TIME principle; its enforcement arms are adapter-aware-lint-rules (rules), test-both-harnesses (tests), the adapter CONTRACT suite (src/adapter-contract.test.ts, the whole-registry conformance loop), and the core⊄adapter / no-CC-literal eslint boundaries. See docs/harnesses.md. Record-only is a deliberate decision, not a parity gap.
Adapter Aware Lint Rules
Guidance only — A lint/scan rule is scoped to the SURFACE it checks (instruction file, skills, MCP, docs, shell hooks, subagents), NEVER to a harness — vigiles is multi-harness, so a rule must work for WHATEVER harness a repo targets (Claude Code, Codex, a future one), including a repo targeting SEVERAL at once: a byte-identical CLAUDE.md⇄AGENTS.md mirror (one logical artifact, linted once on the source slot) OR INDEPENDENT .claude//.codex/ trees with different instruction files (linted per target). Three hard rules, all enforced/dogfooded: (1) NAME BY THE HARNESS-NEUTRAL CONCEPT, never a harness's vocabulary. The Claude Code 'agent' is a SUBAGENT, so the rules are subagent-tool-contract / subagent-frontmatter / untested-subagent — NOT agent-* (Codex's [agents] is a TOML concurrency table, a different concept that merely shares the word). A harness/ PREFIX (e.g. a future codex/parallel-agents) is RESERVED for a concept only one harness has with no cross-harness analogue — none exist today, so no rule is prefixed. (2) THREAD THE RESOLVED ADAPTER; never default to Claude Code. Each check resolves the harness ONCE (resolveHarnessSelection — honouring --harness= / the .vigilesrc.json harness key / auto-detect) and passes adapter.layout + adapter.dialect into the shared detector (scanPlugin, findUntestedSurfaces). A harness-agnostic detector MUST NOT hard-code a Claude Code literal — no ${CLAUDE_PLUGIN_ROOT}, no .claude/, no agents//skills//commands/ surface dir — but read them from the layout (pluginRootToken, skillDir/agentDir/commandDir, materializeRoot, manifestPath) and the dialect (tool + hook-event catalogs). This is MECHANICALLY ENFORCED: an eslint no-restricted-syntax rule bans CC string literals (plain + template quasi) in src/core/** + the detectors (src/scan.ts, src/test-coverage.ts, src/plugin-loader.ts), and eslint-plugin-boundaries bans the core importing an adapter. (3) GATE BY CAPABILITY and report n/a (the no-silent-skips corollary). A surface rule runs only where the active adapter HAS the surface (AdapterCapabilities.subagents / shellHooks); where it doesn't, it reports n/a — <harness> has no <surface> via reportNotApplicable — LOUD, never a false pass, never a crash (CC + OpenCode have subagents; Codex does not). DOGFOOD MUST BE NON-CLAUDE-CODE-SHAPED: a Claude-Code-shaped fixture STRUCTURALLY CANNOT catch a CC-hardcoding regression, so the regression tests for the agnostic path use a non-CC layout/harness — the Codex lint e2e (src/scan-cli.test.ts), the custom-agentDir classification test (src/scan.test.ts), and the custom-token surface-discovery test (src/test-coverage.test.ts). The ONLY places a CC literal legitimately lives: src/adapters/claude-code/, the CC eval transport (src/eval.ts), and init onboarding (it installs the CC plugin). The per-rule applicability is documented as the surface→harness 'Applies to' matrix in docs/verifying-instruction-files.md. See docs/harnesses.md.
Test Both Harnesses
Guidance only — vigiles is MULTI-HARNESS, so a test of a HARNESS-FACING capability must cover BOTH adapters (Claude Code AND Codex), never just Claude Code by default. This is the test-layer companion to adapter-aware-lint-rules (which governs lint/scan RULES): it applies to compile EMIT (a spec/hook → the harness's native format — assert the CC JSON AND the Codex TOML output), the hook/agent/skill RUNTIME, the layer-2 harness-testing runners, and the scan/lint detectors. Three sub-rules, mirroring no-silent-skips. (1) TEST WHAT MAPS ON BOTH — if a capability exists for both harnesses, assert it on both: compile --harness=codex beside the CC default, the agnostic detector over a NON-CC-shaped layout (a CC-shaped fixture structurally can't catch a CC-hardcoding regression — the dogfood-must-be-non-cc point from adapter-aware-lint-rules). A genuinely harness-NEUTRAL path that does not branch on harness (e.g. the compiled-hook GATE runtime — deny→exit 2 is byte-identical on CC and Codex, and react run() just spawns) is covered ONCE, but a comment MUST say WHY one run suffices, so 'only CC is tested' is never left ambiguous (was it neutral-on-purpose, or a CC-only oversight?). (2) GATE/SKIP LOUDLY where a harness genuinely can't run — its binary isn't on PATH (the real-codex tests skip when absent), or the capability is DEFERRED for it (Codex inject/ask/react OUTPUT shape is CC-confirmed-only) — via skip(reason) or a gated test that states WHY, NEVER a silent CC-only pass. (3) DON'T DOUBLE-TEST AGNOSTIC CORE — a pure detector / Trace check that reads only harness-neutral fields needs ONE run, not a redundant per-harness loop; the both-harnesses bar is for the harness-FACING surface, not every unit test. The src/adapters/<harness>/* suites are per-harness by construction; the agnostic suites (src/*.test.ts) must not silently assume Claude Code. STRUCTURALLY ENFORCED (not just guidance) by the adapter CONTRACT suite (src/adapter-contract.test.ts): it runs the conformance kit over the WHOLE registry in a loop (for (const a of ADAPTERS)), so registering an adapter auto-subjects it to every contract (can't forget a new harness), a capability an adapter lacks is a VISIBLE it.skip(… n/a …) (gated on shellHooks/harnessTesting), and a meta-test FAILS THE BUILD when a src/adapters/<dir>/ exists but isn't registered (or a declared prototype like opencode). KNOWN GAPS, documented not hidden: (a) the contract catches ADAPTER-level gaps (an unregistered / broken-port / failed-capability adapter), but it CANNOT auto-catch a harness-FACING capability whose assertion was written CC-only OUTSIDE the contract — putting an assertion INTO the iterated contract is the judgment THIS rule governs (the contract is the FLOOR: every registered adapter is held to everything in it; this rule is the CEILING: decide what belongs in it); (b) it can't force OSS-grounded or real-binary coverage — those gate/skip LOUDLY when the binary's absent, never a false green. Complements adapter-aware-lint-rules (the rule half), no-silent-skips (loud gating), dual-language-tests, and prefer-existing-solutions.
Compose With Sync Tools
Guidance only — vigiles is the author+verify layer, not the fan-out layer. Stay composable with the top rule-sync/interop tools instead of reimplementing them: Ruler (intellectronica/ruler, the leading single-source→many-agent distributor), rulesync, and the AGENTS.md cross-tool standard. The division of labour: vigiles owns truth (a typed spec compiled to a verified CLAUDE.md/AGENTS.md whose references are real and enabled); those tools own reach (distributing that canonical file to Cursor/Cline/Windsurf/Copilot/etc.). Concretely: (1) keep emitting the canonical formats they consume — CLAUDE.md and standard AGENTS.md — and never break that contract; (2) be able to verify references in the files they generate (lint-after-the-fact for their outputs). Do NOT add native multi-format emitters (.mdc, .clinerules, …) — compose, don't absorb the per-agent format-maintenance burden. (3) Treat a symlinked-or-synced CLAUDE.md⇄AGENTS.md as ONE artifact, not two: Claude Code reads CLAUDE.md only (it does NOT natively load AGENTS.md — anthropics/claude-code#34235), so users bridge to the AGENTS.md tools via a symlink (ln -s CLAUDE.md AGENTS.md) or a sync tool keeping them byte-identical. vigiles must follow the symlink on read (hash + require-instructions-spec run once on the real file; the mirror is never flagged as a second spec-less instruction file) and stamp the integrity hash only on the compile source slot, not the distributed mirror.
Smooth Adoption
Guidance only — npx vigiles init must work on first run with zero config and set up BOTH layers by default: a typed spec + types (the Lint layer), a vigiles.harness.mjs starter (the Test layer), a zernie/vigiles@v1 CI workflow (created when none exists; a stale old-API one is flagged, not skipped), vigiles added to devDependencies, and the Claude Code plugin installed via the MARKETPLACE (/plugin marketplace add zernie/vigiles + /plugin install vigiles@vigiles, into ~/.claude/plugins/ — never vendored into the repo). Onboarding is interactive at a TTY (asks which layers / CI / plugin) and NON-INTERACTIVE for agents, CI, or piped input (or with --yes) — so 'set up vigiles' from a Claude Code / Codex prompt Just Works without hanging on a prompt; --lint/--test scope the layers (--harness= overrides detection). After install the agent edits specs automatically — no workflow change required. The DEFAULT already GATES broken surfaces (the FP-safe structural rule group at error — a typo'd tool / dead hook / broken MCP / skill collision fails CI, while a clean repo stays green); the WORKFLOW tier (a spec per file + a test per surface) is OFFERED interactively (opt-out, default-yes) or via --strict, and --report-only downgrades everything to warnings for cautious migration. 'Permissive' means vigiles doesn't FORCE specs/TS on you and doesn't CRY WOLF — NOT that it ignores genuine breakage (see install-enforcement-model). Hesitant adopters can use inline mode (<!-- vigiles:enforce ... --> comments) without a .spec.ts — see docs/markdown-mode.md. See docs/agent-setup.md.
Great Agent Flow
Guidance only — The end-to-end experience when an AGENT is handed a short prompt like 'install vigiles and test my skills' must be GREAT — discoverable from the README front door and frictionless, because for many users the agent IS the installer. Concretely: (1) install is one non-interactive command an agent runs without hanging — npx vigiles init auto-detects a non-TTY and applies defaults (both layers); (2) init installs the model-invocable test-harness skill, so a follow-up 'test my skills' FIRES that skill (it picks the tier and writes the test) rather than leaving the agent to flail; (3) the README must make the skill-testing path explicit and high-signal — name measureTriggerRate (does a skill's description actually fire? recall + precision), not just hook examples, and surface the test-harness skill where the task lives, not buried in a list. The README is the agent's front door: an agent reading it for 'install + test skills' must find a clear, actionable path. Dogfood it — the shipped model-invocable consumer skills are test-harness, strengthen, edit-spec, and debug-my-harness (each reaches for itself on the matching request rather than waiting for a slash command — debug-my-harness reads the .vigiles/runs.jsonl flight recorder to diagnose why the harness misbehaved; generate-logo is an internal contributor-only skill under .claude/skills/, not shipped); all are covered by trigger-rate evals (examples/harness/dogfood/) gating recall + precision so making them auto-fire doesn't make them over-fire. The user-invoked skills (adopt-spec, linter-docs) stay disable-model-invocation. See docs/agent-setup.md and docs/harness-testing.md.
Install Enforcement Model
Guidance only — vigiles init's enforcement model is RULE GROUPS keyed by confidence, NOT a strictness-preset MENU — the linter best practice (Clippy/Ruff/Biome; Biome's own design note: 'presets are a poor emulation of rule groups', and ESLint's strict/stylistic preset-piling is 'a sign of a lack of consistent rule grouping'). THREE groups: (1) structural — the FP-safe correctness rules (subagent-tool-contract, subagent-frontmatter, hook-events, hook-script-exists, mcp-config, mcp-tool-resolves, mcp-hook-target-resolves, disallowed-tools-contract, description-overlap, integrity) — DEFAULT error: catches a typo'd tool / dead hook / broken MCP / skill collision, and a clean plugin stays green so it never cries wolf; (2) workflow — the opinionated rules a CLEAN repo can still fail because the work isn't done yet (require-instructions-spec, untested-skill/subagent/hook) — OFF by default, error under --strict (the ONE opinionated opt-in, the Clippy-pedantic / TS-strict analog); (3) nudge — recommendations / acknowledged-noisy (frontmatter-valid, skill-frontmatter, unmarked-refs at warn; prefer-compiled-hooks defaults OFF) — NEVER gate (frontmatter-valid stays warn even under --strict). require-instructions-spec is NARROW (only a .spec.ts satisfies it, not inline/frontmatter) and is satisfied BY CONSTRUCTION because init AUTO-ADOPTS every instruction file into a faithful spec — so opting into workflow never lands a wall of missing-spec failures. --report-only is an ORTHOGONAL severity dial that downgrades the gating groups to warn (the migration / observe on-ramp, like Biome's only-warn) — it is NOT a rule set and composes with the group selection. Presets/flags EXPAND to EXPLICIT per-rule severities in .vigilesrc.json (greppable, editable, downgrade any single rule) — NEVER a runtime preset key (the explicit > magic ethos that put the rules in the config rather than code defaults). DO NOT build a relaxed/standard/strict preset menu. INSTALLATION IS EXPLICIT ABOUT TRADEOFFS: every choice states what you GET and what it COSTS at the point of choosing, and the SAME tradeoff text is reused across the CLI prompt (one terse line per option), the agent's AskUserQuestion (the tradeoff is the option description; the tool's automatic 'Other' takes a custom answer), and the post-install summary (what landed + the implication + how to change it) — one source, three surfaces, no drift. The agentic install presents the GROUP opt-in WITH its tradeoff ('also enforce the workflow group? — fails until you have written specs + tests'), never an abstract strictness band. Mechanism today: STRUCTURAL_RULES (= structural) + WORKFLOW_RULES (= workflow) + NUDGE_RULES (= nudge) in src/setup-plan.ts, written by mergeProjectConfig (which takes reportOnly for the --report-only dial).
Gate First Adoption
Guidance only — The adoption NORTH STAR. Every change touching init, the report, CLI output, onboarding, or SHARING serves these GOALS: (G1) the INTEGRITY GATE is the universal floor — audit (local) + lint (CI) must be valuable AND safe for EVERY repo with zero setup and zero conflict, any stack, existing-harness-or-not, and stand entirely on their own (this is what everyone adopts first); (G2) everything richer (specs, installed skills, rules → enforced, eval) is VALUE ON TOP, INVITED never forced; (G3) FIT THE TEAM, don't fight it — the path branches by stack (supported linter?) + existing-harness, and the tool shows ONLY what's TRUE for THIS repo (no silent rules → enforced on a non-JS stack, no Tested alarm for a team with its own tests); (G4) a result is worth sharing AND sharing is clean — a fair grade spreads on merit (a real report link, an opt-in badge, the PR comment), and a LOCAL CLI result is as shareable as a web-demo one; (G5) every path is judged by whether someone can run npx vigiles audit and get something useful on the first try. The NON-NEGOTIABLE 'not evil' contract binds every adoption/onboarding/sharing surface (breaking one is a DEFECT, not a tradeoff): (1) a READ never writes — audit/lint never mutate/install/phone-home; setup is a separate consented step; (2) DECLINING is free, one keystroke, and REMEMBERED — no nag loops, no default-yes on an invasive choice, no penalized grade, and agent/CI/piped runs never hang on a prompt (they take the safe default + print a one-line invitation); (3) never OVER-PROMISE or measure the wrong thing — honesty over a flattering-but-false signal; (4) grade OTHERS fairly (the Lighthouse contract) — objective, reproducible, one-line-fixable findings, tone is 'worth fixing' never 'gotcha', and only feature a repo vigiles doesn't own if it's MIT-vendored (reuse dogfood-vendoring-policy) or opted-in, methodology published, sorted-not-editorialized (no 'hall of shame'), private-first for real defects; (5) NO dark-pattern sharing — no forced/coerced shares, contact-list spam, 'share to unlock', auto-posting, fake scarcity, or streak/guilt manipulation, and never gate real value behind a viral action (grounded in the LinkedIn-$13M / FB-frictionless / Path / Duolingo / GitHub-name-and-shame backlash corpus in adoption-design.md). THE THROUGHLINE: make the GATE useful to everyone, INVITE the rest, FIT each team, and let a fair result spread on its own merits — adoption is earned by being useful and honest at every step, never extracted by pressure. This is the umbrella over smooth-adoption / great-agent-flow / install-enforcement-model / progressive-adoption / lead-with-easy-adoption (they are HOW; this is the WHY they answer to).
Format Before Commit
Guidance only — Run npm run fmt:check before committing. Inline code spans in markdown need surrounding spaces to render correctly.
Run The Gates
Guidance only — The gate set is ONE COMMAND — npm run check — never a list you retype. It builds, runs the read-only checks in parallel, then the file-WRITING ones serially, and names whatever failed; node scripts/check.mjs --list prints the commands without running them. THEN READ ITS LAST LINE. check covers ONE of CI's jobs and PRINTS the ones it does not (NOT covered here — N more CI job(s), rendered from CI_JOBS_NOT_COVERED in scripts/check.mjs, which scripts/check-covers-ci.test.ts binds to ci.yml so a NEW job is a failing test rather than a silent omission). Decide FROM THAT OUTPUT which of the jobs it names your change touches, and run those too. Do NOT restate either list — not here, not in a doc, not in a prompt: the second copy is the one that goes stale, and this rule exists because copies did. MEASURED 2026-09-07 — three sessions in one day reported the gates green off a hand-written five-command list, and not one of them ran the 14 deterministic harness tests, a separate CI job that list never named. A remembered list cannot notice a job it does not know about; a printed one can. WHAT SURVIVES from that list and still applies: (a) run the gates LAST, after the final edit, on a tree nobody is touching — a gate that linted a half-written file failed on a state that never existed; (b) take the exit code WITHOUT a pipe (cmd > /tmp/log 2>&1; RC=$?), because cmd | tail reports tail's status, which is how a red suite once shipped green; (c) REPORT WHICH GATES YOU RAN, BY NAME, with their exit codes — the gates are green is indistinguishable from I ran a subset, and a tier that could not run is named as SKIPPED, loudly (no-silent-skips), never folded into a pass. A MUTATION run obeys the same rule: run the mutant against the WHOLE relevant suite AND prove the patch landed (grep the mutated source) before reading a green as evidence — a green mutation whose patch never applied carries zero information. The executable form is .claude/skills/ship-a-feature/ (--gate runs npm run check on a hash-frozen tree).
Pick The Test Tier
Guidance only — A new check goes in the CHEAPEST tier that can decide it — cost rises steeply, and each tier is only worth what the one below cannot reach.
| what the check needs | tier | run it with |
|---|---|---|
| nothing executes — a pure detector over given input | unit | npm run test:unit (CI reaches it via npm run coverage) |
a real claude/codex binary, but no model |
harness | npm run test:harness |
| a real model — real money | eval | not in CI; run by hand |
| real network or Docker | integration / e2e | npm run test:integration / npm run test:e2e |
The FULL tier map — where each lives, what it needs, and which CI job (if any) runs it — is the ### Test section of CONTRIBUTING.md, kept honest by src/doc-test-script-coverage.test.ts (every test:* script must appear there). What a new test still OWES is already written down and is deliberately not restated here: both harnesses (test-both-harnesses), a tier that cannot run saying so out loud (no-silent-skips), and a vendored fixture (dogfood-vendoring-policy).
Conventional Commits
Guidance only — Commit subjects AND PR titles are Conventional Commits — they drive the release version via @semantic-release/commit-analyzer off main (feat → minor, fix → patch, everything else no bump). Allowed types (enforced by the validate CI job, amannn/action-semantic-pull-request): feat, fix, docs, chore, refactor, test, perf, ci, build. OPERATIONAL GOTCHA — a PR opened from the Claude Code UI (or any non-CLI flow) gets a plain prose TITLE by default, which FAILS the validate job (it checks the PR TITLE, not the commits). Fix the PR title to a Conventional Commit (type + optional ! + subject) as soon as the PR exists — edit it via the GitHub API/UI; validate re-runs on the title edit, no new push needed. The title must carry the SAME breaking-change signal as the commits: if the branch removes/renames a public API, the PR title needs the ! too (the release version comes from the merged title/commits). CRITICAL — signal breaking changes explicitly: a ! after the type (feat!:/refactor!:) or a BREAKING CHANGE: footer triggers the MAJOR bump. The CI lint only checks the type prefix; it CANNOT tell whether a change is semantically breaking, so the ! is on you. A change is breaking when it removes/renames/moves a public API: a package.json exports subpath (e.g. vigiles/claude-code), an exported symbol, a CLI command/flag, a config-file key, or a compiled-output contract. When in doubt whether a change is breaking, mark it ! — under-signalling ships a wrong (too-low) version, which is worse than an extra major. (Pre-1.0 the major stays 0, but the signal must still be correct so the changelog is right and the first 1.0 bump is clean.)
Lead With Easy Adoption
Guidance only — Public docs — and the README's FIRST SCREEN especially — must make CLEAR and UP-FRONT that adopting vigiles is LOW-FRICTION because SKILLS + HOOKS do the mechanical work for you. The reader's first impression must be 'I run one command and the agent takes it from there,' NEVER 'this looks like a pile of setup.' The recurring failure is presenting features as manual chores (hand-wire this hook, write this spec, remember to run --update) and burying the agent-does-it-for-you story — which reads as friction and loses adopters before they try it. Lead with the reassurance, then prove it. WHAT IS ACTUALLY SMOOTH (state it plainly, early): (1) ONE COMMAND — npx vigiles init — installs the plugin (skills + hooks), adopts your existing CLAUDE.md into a spec, and wires CI; non-interactive so an agent runs it without hanging. (2) THE AGENT DOES THE EDITS — model-invocable skills handle the work on request: edit-spec changes your CLAUDE.md by editing its spec, strengthen upgrades guidance into verified rules, and test-harness writes + runs the test/eval AND maintains its artifacts (e.g. it runs vigiles eval --update and commits the lock for you — you don't type it). (3) HOOKS NUDGE AT THE RIGHT MOMENT — the refs nudge, the eval-lock staleness nudge — so the agent is reminded in-loop instead of you remembering. So FRAME EVERY FEATURE'S UPKEEP as agent/skill/hook-driven, not homework: the eval lock is 'the test-harness skill keeps it fresh + a hook reminds you', not 'remember to run --update'. HONEST (don't overclaim): the agent + skills + hooks handle the MECHANICS; you still make the decisions and review the diffs — low-friction, not zero-thought, and the deterministic gates still catch real breakage (install-enforcement-model). The TEST: a skeptical reader skimming the first screen should come away reassured adoption is smooth, with the agent/skill/hook path obvious — not daunted by setup. This is the MESSAGING companion to smooth-adoption (the init mechanics) + great-agent-flow (the agent on-ramp) + cohesive-feature-delivery (every feature ships the agent-awareness surface that makes this claim TRUE); keep it honest with install-enforcement-model (smooth never means it ignores real breakage) and tight with readme-brevity.
Progressive Adoption
Guidance only — vigiles must be adoptable incrementally, like TypeScript — but progressive about ENFORCEMENT DEPTH, NOT about whether obvious breakage fails. The on-ramps: (1) --report-only / inline <!-- vigiles:enforce ... --> comments on an existing CLAUDE.md — see problems as warnings, fix gradually, nothing fails CI, no new files; (2) DEFAULT npx vigiles init — the FP-safe structural rule group gates REAL breakage (a clean repo stays green), zero linter setup; (3) --strict — also require a spec per file + a test per surface (the workflow group). Each level adds value without requiring the next; never FORCE specs/TS to use the basics. The reframe (see install-enforcement-model): 'permissive' = no forced specs/TS + no crying wolf (the default rules are FP-safe), NOT ignoring genuine breakage. README examples show the simplest path first.
No Session Links
Guidance only — This is a public repo. Claude Code session URLs are private and must not appear in commits or PRs.
No Product Strategy Here
Guidance only — This repository is PUBLIC. Anything committed here is readable by anyone, and deleting a file does not remove it from history or from other branches. NEVER commit here, in any directory: roadmap / prioritisation / what-we-build-next; competitive analysis, positioning, poach lists; adoption, distribution, go-to-market, pricing; or design documents for capabilities that are NOT YET SHIPPED. THE LINE IS BUILT VS UNBUILT, not engineering vs business — documentation of shipped behaviour belongs here and is useful, while a design document for something unbuilt is a roadmap however technical it reads. A competitive FACT may appear only as a neutral, public-source technical note; the narrative around it does not belong in this repository at all.
Subagent Model Note
Guidance only — When you launch a subagent (the Agent/Task tool), tell the user in one short line WHICH model you chose for it and WHY — e.g. "spawning the Explore agent on Haiku — cheap fan-out read, no synthesis needed" or "using Opus for this one — it has to reconcile conflicting findings across docs". The user wants visibility into the model-selection tradeoff (cost vs capability) you're making on their behalf. Keep it to a clause, not a paragraph; do it at launch, not after.
Surface Architecture Decisions
Guidance only — The user is often flying BLIND on structure — they can't see WHERE you put a file, whether a thing is CORE vs an ADAPTER vs a PORT, or whether you extended something vs created a sibling. So when you make a non-trivial ARCHITECTURAL decision, STATE IT IN CHAT in a short, scannable block BEFORE or AS you implement it — never bury it in a big diff the user then has to reverse-engineer. What counts as architectural (surface it): WHERE a new file / module / type / detector goes (and why THERE, in the tree); whether it's harness-agnostic CORE (src/core/**) vs an ADAPTER (src/adapters/<harness>/**) vs a PORT (the five injected interfaces) vs the composition/library root (src/*.ts); EXTENDING an existing verb/module/report-type vs creating a new one; a CROSS-CUTTING change touching many files or a shared type; and any placement the CONSTRAINTS make load-bearing. For each decision give three things, tight: (1) WHAT — the file/module + where it sits; (2) WHY THERE — the reasoning against OUR constraints, naming the relevant one (hexagonal core ⊄ adapter / multi-harness-behind-a-PORT + no-CC-literal-in-core / one-detector-no-drift / cohesive-cli-surface / the rule-meta triad / additive-and-lock-safe); (3) the ALTERNATIVE you rejected, in a clause, when there was a real fork (e.g. 'a new driver method vs reuse the parser's existing usage — chose reuse, it's harness-neutral already'). Keep it to a few bullets — a QUICK SCAN, not an essay; the point is the user catches a wrong call (a leaked adapter import, a type in the wrong layer) in seconds, before the diff. This is the STRUCTURE analog of subagent-model-note (which surfaces the model choice): surface the placement + boundary reasoning the same way. Especially load-bearing because vigiles is hexagonal + multi-adapter, where a wrong placement (core importing an adapter, a CC literal in core, a per-harness fact not behind a port) is exactly the mistake the boundaries exist to prevent — so say the boundary out loud as you honor it. TWO type-design principles are THEMSELVES architectural decisions — APPLY them AND surface them when you make a structural call (the mechanical HOW lives in ts-essentials; HERE the point is to NAME the decision so the user can scan it): (a) PARSE, DON'T VALIDATE — turn loose/untrusted input (a string | string[], an unknown config blob, a CLI flag) into a typed, normalized shape ONCE at a boundary, then pass the typed value inward; WHERE that boundary sits is a placement decision, so state it ('parse settings.hooks unknown once in hook-normalize → typed everywhere after', not re-validated at each use site). (b) MAKE ILLEGAL STATES IRREPRESENTABLE — reach for a type that CANNOT express the bad state (a discriminated/tagged union, a branded type) before a runtime check that merely hopes to catch it; when you choose a type shape, NAME the invalid state it now forbids ('a Decision that carries notice only on its kind:"notice" variant, so a caller can't read a null notice'). Both are load-bearing precisely because a wrong call — re-validating the same primitive at ten sites, or a boolean-flag soup that lets an impossible combo compile — is exactly the structural debt this rule exists to catch EARLY, at the decision, not after the diff.
Doc Per Rule
Guidance only — Every validation rule in .vigilesrc.json must have a corresponding doc in docs/rules/.md. The doc covers configuration, severity levels, options, what the rule checks, and why. README links to each rule doc from the rules table.
Rules Docs In Sync
Guidance only — The set of validation rules is a SINGLE SOURCE OF TRUTH that the docs must track, never drift from: the RulesConfig keys in src/core/types.ts (plus any built-in vigiles/* rule like orphan-docs) ARE the rule set, and the canonical rules matrix in docs/verifying-instruction-files.md (the ## The validation rules section — it lives in the LINTING GUIDE, where a reader looking for the rules expects it, NOT buried in the CLI reference) must list EVERY one — each with its default severity and a one-line what it checks, linking its docs/rules/<name>.md. The matrix is grouped by family (spec & integrity, test coverage, reference marking, subagent contracts, hooks & MCP, skill triggers, docs hygiene). So adding, removing, or renaming a rule is not done until the SAME change updates FIVE places together: (1) the RulesConfig type in src/core/types.ts; (2) the per-rule matrix row in docs/verifying-instruction-files.md; (3) the docs/rules/<name>.md doc (the doc-per-rule requirement); (4) the PUBLIC coverage matrix in docs/what-vigiles-catches.md (the benefit-framed, biggest-problem-first list of what vigiles prevents/catches/measures — see public-vs-internal-docs); and (5) the INTERNAL handling-mode matrix (## The handling-mode matrix: prevent vs detect vs measure). The TWO matrices differ in shape and obligation: the per-rule matrix (2) is a 1:1 list that must carry EVERY rule, so a new rule ALWAYS adds a row and a removed rule ALWAYS drops one. The coverage matrices (4) + (5) are organized by USER-FACING PROBLEM, not per-rule — a new rule updates them only when it changes what real-world problem is prevented/caught/measured (a new detector for a problem already covered may sharpen an existing row rather than add one; a genuinely new class of bug adds a row, placed by how badly the problem bites, NOT alphabetically). No rule ships absent from the per-rule matrix, and no row points at a rule that no longer exists. docs/cli.md and the README do NOT re-list the rules (readme-brevity); they LINK to the one matrix in the linting guide, so that matrix is the place that must stay complete. When you touch the rule set, re-derive the per-rule matrix from the RulesConfig keys to catch a missed row, then re-read the two coverage matrices and update any row whose prevent/catch/measure story changed. Complements doc-per-rule (which governs the per-rule doc) — this governs the COMPLETENESS of the shared lists and WHERE they live.
Readme Brevity
Guidance only — README.md is the FRONT DOOR and a marketing asset, not a reference manual — it must land a WOW in the first screen for someone who already lives in agentic-coding tools (Claude Code, Codex, Cursor). Optimize for FAST SCANNING by a human skimming on a phone: SHORT (target ~140 lines, hard cap ~200), NO walls of text (a paragraph is ≤ ~3 lines; break dense prose into bullets, tables, or a one-line + [Details →] link), and GREAT FORMATTING (a punchy one-line tagline, a tight 2-row Lint/Test table, runnable code blocks, bold lead-ins on bullets, em-dashes not semicolons). Lead with the sell, not jargon (no "layer"; footnote the Latin). Push every detail into docs/ and LINK it ([Details →](docs/X.md)) the moment a section runs long — the README states WHAT and WHY, the docs hold HOW. The evals section MUST state the cost model: most questions are answered deterministically (no model, no key), and real-model evals run on YOUR Claude subscription rather than a metered API key — keep it to a sentence or a small table row, not a wall, with [Why it's affordable →] for the rest. Every claim that needs proof links out; nothing load-bearing is buried in prose. Put the INSTALL PATH ABOVE THE FOLD: the slim Quick start (the copy-paste agent prompt + npx vigiles init) sits right after the what-it-does table, BEFORE the deep-dive sections — order is hook → what → get-it-now → why (proof/depth) → reference — so a skimmer can try it in seconds; keep the verbose setup detail in a <details> so the top stays lean.
Docs Quality
Guidance only — The docs/ reference docs are the HOW the README defers to — they are NOT held to README brevity (depth is their job; a long reference doc is correct, a wall of undifferentiated prose is not). Hold each to a DOC-appropriate polish bar instead: (1) open with a one-line "what this doc is" + an UP-LINK to the README in the same breath ("the README has the pitch; this is the full guide") so a reader who landed deep can climb back to the sell; (2) be SCANNABLE — descriptive section headings, a Contents list once a doc runs long, tables and runnable code blocks over prose walls, bold lead-ins; (3) CROSS-LINK siblings (the lint guide and the testing guide point at each other; every doc ends in a "See also") and link DOWN to the deeper doc rather than inlining its detail; (4) stay CONSISTENT with the README and with each other — terminology, claims, and framing must track the front door, never contradict it (the lint/test naming, the promptfoo cost contrast, the deterministic-vs-real-model split), and a doc the README promises must actually deliver that depth (no thin stubs, no stale/renamed commands or dead example paths). The README sells; the docs prove and instruct — both are first-class, neither is a dumping ground.
Prose Clarity
Guidance only — Public prose (README + docs/) must be CLEAR and SCANNABLE for a human skimming on a screen — not dense, jargon-packed, or wall-of-text. The recurring failure is sentences a reader can't parse on one pass: stacked em-dash asides, nested parentheticals, three ideas welded together, internal shorthand used without a plain gloss, and BOLD on every other clause (which emphasizes nothing). Write the opposite. SEVEN HABITS, applied to every public doc you write or touch: (1) LEAD WITH THE POINT (BLUF) — the first sentence of a section says what it is or what to DO, in plain words, before any nuance. (2) ONE IDEA PER SENTENCE, short. If a sentence has two em-dashes or two nested parentheticals, split it. Prefer a period over a dash. (3) DEFINE OR DROP JARGON — gloss a term the first time ('the lock — a committed file CI checks') or replace it; never stack project shorthand ('the seam', 'model-affecting inputs', 'the gate') without a plain-English handle. (4) SCANNABLE STRUCTURE over prose walls — a paragraph is <= ~3-4 lines; break dense explanation into a TABLE (for comparisons / option lists), BULLETS with bold lead-ins, a fenced CODE block (for commands), or an ASCII / Mermaid DIAGRAM (for a flow or before/after). A flow is almost always clearer as a diagram than a paragraph. (5) EMOJI AS SIGNPOSTS, sparingly + consistently — ✅/❌/⚠️/ℹ️ to mark pass/fail/caveat/note, not decoration; one system per doc, never mid-sentence confetti. (6) BOLD ONE PHRASE PER POINT — the single key term, not every clause. (7) CONCRETE BEFORE ABSTRACT — show the command / diagram / example first, then generalize. The TEST: read each section once at skim speed; if you can't tell what it means or what to do, rewrite it. Graphics: ASCII + Mermaid + tables render natively on GitHub with no dependency and stay diffable — PREFER them; do NOT commit generated raster images into docs (binary, non-diffable, a runtime key dependency) — reserve a generated image for a README hero/logo where a raster truly earns it. This governs the WORDS + FORMATTING for readability; complements readme-brevity (front-door LENGTH), docs-quality (structure + polish + cross-links), and public-vs-internal-docs / doc-tiers (which TIER a fact belongs in). Internal docs may run denser, but the same habits still help. When in doubt, cut words and add a diagram.
Public Vs Internal Docs
Guidance only — Match the detail to the AUDIENCE — don't overload public readers. INTERNAL docs (CLAUDE.md) hold the full record: confirmed wire schemas, parsing mechanics (dedup rules, field maps), version regressions, spike narratives, env-validation checklists, the why-behind-the-why. PUBLIC docs (docs/*.md, README.md) state only what a USER needs to ACT — the status, the one or two genuinely useful insights, and how to use the feature — and keep the rest in the internal doc (the confirmed schema + full findings live in the internal doc, referenced from CLAUDE.md, NOT linked from the public page — see no-internal-links-in-public-docs). Do NOT paste a JSONL/event schema, an internal dedup/parse detail, a dependency's internal version quirk, or a spike story into a user-facing guide unless a user genuinely needs it to USE the feature. The test: would a user reading this to ACCOMPLISH a task be helped by this line, or just made to scroll? When in doubt, put it in the internal doc (and don't link it publicly). A reader does not care how a feature positions us; they care what it DOES, so every tier names the user-facing BEHAVIOUR (say 'the cross-referencing engine' / 'runs on your subscription'), never a competitive label. (Complements doc-per-rule + docs-quality + readme-brevity: those govern polish and where rule docs live; this governs which TIER a given fact belongs in.)
No Internal Links In Public Docs
Guidance only — PUBLIC docs (README.md + docs/*.md) must NEVER link to or name-drop an INTERNAL doc (any internal-only file). Internal research is the project's PRIVATE record — referenced ONLY from CLAUDE.md keyFiles, never from a user-facing page. A reader in the README or a docs/ guide should never be sent into internal design notes, half-finished spikes, or design notes for something unbuilt. When a public doc is tempted to link an internal doc, link the matching docs/ guide instead — and if none exists, that's the signal to write the public one (or just drop the pointer; the prose must stand on its own). GOTCHA: stripping a public→internal link can ORPHAN the internal doc (orphan-docs requires every opted-in .md be referenced somewhere), so when you remove the last inbound link, ensure the doc is still cited from CLAUDE.md keyFiles. NOTE: currently GUIDANCE only — the deterministic enforcement (a lint rule failing on an internal-doc link in README.md/docs/) is a P1 roadmap item; until it lands, hold the line by hand. Supersedes the old 'link to the internal doc for the rest' advice in public-vs-internal-docs (keep the FACT internal, just don't LINK it publicly). Complements readme-brevity (front door) + public-vs-internal-docs (tiering).
Doc Consistency
Guidance only — Docs MUST stay consistent with the code. When you ship, rename, or remove a CAPABILITY (a command, flag, exported symbol, key file, config key, or behaviour), update its docs in the SAME change, across BOTH tiers: the PUBLIC docs (docs/, README.md) AND the internal record (this file's keyFiles + positioning). Never let one tier describe a reality the other contradicts, and never leave a renamed command/path/example rotting in either. SPLIT THE WORK BY WHAT'S MECHANIZABLE — the lesson from the hook-runtime rename: a manual sweep leaked 8 stale vigiles <cmd> references EVEN WITH the cohesive-cli-surface rule freshly written, so guidance alone is not enough. The CHECKABLE half is ENFORCED by deterministic CI gates, never trusted to discipline: self-command-refs (every command reference in docs/comments resolves to a real verb or hook-runtime kind — src/self-command-refs.test.ts), orphan-docs (no docs/ file goes unreferenced), the markdown doc-refs validator (enforce/file/cmd/ref marks in fenced TS blocks resolve — OPT-IN since 2026-08-19, {"rules":{"doc-refs":"error"}}, because measured at 0 true positives across 2 582 markdown files: a fence in prose is a drawing of config, not config), and integrity (a compiled CLAUDE.md/AGENTS.md matches its spec). Where a doc-consistency fact CAN become a deterministic check, MAKE it one (analogical-transfer + don't-cry-wolf) rather than adding another guidance rule — the check is the mechanism, dogfooded on vigiles itself. The JUDGMENT half this rule names is what no check can verify: prose accuracy, which TIER a fact belongs in, cross-links, and keeping framing aligned with the front door — governed in detail by public-vs-internal-docs (tier), docs-quality (polish + 'no stale/renamed commands or dead example paths'), readme-brevity (front door), and rules-docs-in-sync (the rule set ↔ matrix). MEASURED LIMIT of the checkable half (don't widen a check into crying wolf): self-command-refs catches the vigiles <cmd> INVOCATION form only — a BARE command name (agent-hook without the prefix) or a BARE file path (src/foo.ts) is NOT auto-flagged, because both collide with legitimate non-references (concept names like 'the refs-hook nudge' in ~10 files, test labels, makeTmpDir names; illustrative paths like the README's src/auth/login.ts; test fixtures). A denylist there would fire on dozens of valid usages, so bare-name/path terminology accuracy is JUDGMENT, not a gate. The deterministic check is the FLOOR; updating both tiers together, every time, is the ceiling.
Document The Why
Guidance only — Docs must give every user-facing DECISION and CONCEPT a discoverable home that answers WHY / WHAT, not only a command-or-API reference for HOW. The repeatable failure this rule names (proven twice in one session: the opt-in 'why' needed two passes; the audit→behavioral→consent story was undocumented): each guide POLISHED the mechanics while the reasoning a user needs to make a decision — why a default is what it is, what audit runs by default (a deterministic READ) vs only under the measure-consent, why a tier is opt-in — lived ONLY in code + a research note, so users (and the next maintainer) re-derive it in conversation. For any user-facing DECISION (a default, a consent gate, the opt-in/strict ladder, the read-vs-run axis) or CONCEPT a user must reason about (a measurement axis like recall/precision, a tier, a safety boundary, selection-collision), there must be a plain-language PUBLIC home — a guide section or an FAQ entry — that states the WHY/WHAT in the user's terms. A named feature dropped WITHOUT explanation (a bare 'selection-collision matrix' mention with no definition) FAILS this: name-dropping is not documenting. THE TEST: could a user answer 'why is it like this / what does it actually do?' from the docs ALONE, without asking a maintainer? If the only answer lives in an internal note or in the code, the rule fails. This is the COVERAGE-OF-REASONING gap none of the sibling rules own: polish rules (readme-brevity / docs-quality / prose-clarity) govern how WELL a fact is written; placement rules (doc-tiers / public-vs-internal-docs / no-internal-links-in-public-docs) govern WHERE it lives; sync rules (rules-docs-in-sync / doc-consistency) govern that docs TRACK the code and don't contradict it — but none require that the REASONING behind a user-facing decision be documented AT ALL. It sharpens cohesive-feature-delivery item (6) from one checklist line into a standing, testable bar. DETERMINISTIC FLOOR (analogical-transfer — prefer a check to guidance where one fits): the inverse of self-command-refs — every public CLI verb is MENTIONED somewhere under docs/ — is mechanizable and catches a verb shipped undocumented (src/doc-command-coverage.ts, dogfooded in its test); the CONCEPT-coverage half (is this a concept that needs a why?) is undecidable and stays judgment. The check is the FLOOR; documenting the why for every user-facing decision is the ceiling.
Ts Essentials
Guidance only — Prefer branded types over plain strings for semantic values (hashes, file paths, rule IDs). Use tagged/discriminated unions freely whenever they help — over boolean flags that gate optional fields, and to MAKE INVALID STATES IRREPRESENTABLE (e.g. a result that carries a notice only on its kind:"notice" variant, never a nullable string the caller can forget to check). Add exhaustive default: assertNever(x) to every switch on a union type. Follow PARSE, DON'T VALIDATE: parse raw/untrusted input (a config value, a string | string[], a CLI flag) into a typed, normalized representation ONCE at the boundary, then pass that typed value around — don't re-validate or re-normalize the primitive at each use site. These patterns convert runtime bugs into compile-time errors.
Prod Grade Gha Cli
Guidance only — The CLI and the GitHub Action are production-grade, first-class surfaces — for most users CI is how vigiles actually runs — and must be kept that way, not treated as afterthoughts. The CLI is the single source of truth: every Action input maps to a real CLI flag (no config-file-only knobs the Action can't reach), commands emit GitHub annotations under GITHUB_ACTIONS, and exit codes are stable and documented (0 clean / 1 warn / 2 error). The Action wraps that CLI and must actually work as a published uses: zernie/vigiles@v1 reference — a composite action over the published npx vigiles CLI, so it reuses the same tested artifact rather than a node20 entry pointing at an uncommitted dist/. It must declare its outputs: and set them via $GITHUB_OUTPUT (never the deprecated ::set-output), pin to a stable floating major-version tag (v1) maintained by the release pipeline, and ship a full copy-paste workflow in the docs (runs-on, checkout, setup-node, permissions:, every input + the valid output, and @v1-vs-@main-vs-pinned-SHA versioning guidance). The Action surfaces results three complementary ways: inline GitHub annotations, a job summary ($GITHUB_STEP_SUMMARY), and an automatic STICKY pull-request comment (found by marker and updated in place — never a new comment per run; gated to pull_request events with pull-requests: write, best-effort so a fork PR without write access degrades to a warning, not a failure). Both surfaces are dogfooded in this repo's own CI (the Action via uses: ./, including the PR comment) and covered by tests, so a regression in the shipped entry points is caught here. See docs/cli.md.
Cohesive Cli Surface
Guidance only — vigiles is ONE cohesive organism, not a pile of stray commands — the CLI, the GitHub Action, and the agentic-coding flow are three FACES of the same capability set and must stay coherent. Four rules. (1) FEW HUMAN VERBS, each a clear mental action; a new capability EXTENDS the verb that already owns that action instead of spawning a sibling. 'Compile a typed authoring artifact into the harness's native format' is ONE verb — compile — whatever the artifact: a .spec.ts → markdown, a hook program → a settings block + stamp. NEVER ship a compile-hook-style stray sibling of a verb that already exists; fold it in. (2) TWO TIERS — VERBS vs RUNTIME ENTRYPOINTS. A verb is typed by a human/agent/CI (init/compile/lint/scan/test/eval). A runtime entrypoint is EMITTED into settings/hooks and invoked BY the harness, never typed by a human (the compiled-hook runtime, the agent/skill PreToolUse rails, effect markers, the refs nudge). Runtime entrypoints live under ONE hidden umbrella subcommand (vigiles hook-runtime <kind>) and stay OUT of the help/verb surface, so the organism reads as ~10 verbs not ~36 commands — a runtime entrypoint is an implementation detail of an emitted block, not a public command. (3) A VERB THAT PRODUCES WIRING WRITES IT. Emitting a paste-this blob is a stray half-step; the cohesive behaviour is to MERGE the result into the harness's native config (.claude/settings.json / config.toml) idempotently — found-by-marker, updated-in-place, never clobbering hand edits (the way init installs) — so the user's harness is actually wired, not handed homework. (4) COHERENT ACROSS THE THREE FACES. The same capability is reachable identically from the CLI verb, a GHA input that maps to that verb's flag (the prod-grade-gha-cli contract — no config-only knobs), and an agentic on-ramp (init wires it; a model-invocable skill reaches for it on the matching prompt — the great-agent-flow contract). A capability is NOT DONE until it is coherent on all three. When you add/rename/remove a command, change the CLI + the GHA input + docs/cli.md + the agent on-ramp TOGETHER (sibling of rules-docs-in-sync); renaming a runtime entrypoint BREAKS the emitted-command contract baked into every settings block + stamp, so it is a BREAKING change (! + regenerate every emitted block). ENFORCED, not just guidance: the self-command-refs dogfood (src/self-command-refs.test.ts) cross-references every vigiles <cmd> reference in the docs + comments against the real command set (src/cli-commands.ts) and FAILS CI on a stale ref — so a renamed command can't leave a rotting reference (it caught 8 a manual rename sweep missed). Complements prod-grade-gha-cli (the GHA/exit-code contract), great-agent-flow (the agent on-ramp), and smooth-adoption (init).
Cohesive Feature Delivery
Guidance only — A capability is ONE organism with MANY surfaces — and 'done' means coherent across ALL of them, not just the one you started with. This is the DEFINITION-OF-DONE CHECKLIST for any non-trivial feature; run it EVERY time, because the recurring failure is shipping the mechanism and missing the flow (the CLI lands but the agent never learns it exists; CC works but Codex is forgotten; the code ships but the docs re-research what was already decided). Before calling a feature done, walk this checklist OUT LOUD and name N/A explicitly (never silently skip a row): (1) CLI — the verb/flags exist, exit codes are stable, docs/cli.md updated (cohesive-cli-surface). (2) GHA — a real input maps to the verb/flag, no config-only knob, dogfooded in this repo's CI (prod-grade-gha-cli). (3) INIT — vigiles init scaffolds/wires it where it belongs (smooth-adoption), and does NOT do the invasive thing (e.g. NEVER edit the user's CLAUDE.md to convey a workflow — that's a hook/skill's job). (4) AGENT-AWARENESS — the agent can DISCOVER + maintain the feature: a model-invocable SKILL documents it (description always-in-context on CC) AND/OR a HOOK nudges at the right moment (great-agent-flow). Awareness is delivered by skill+hook, NOT by editing the user's instruction file. (5) BOTH HARNESSES — CC AND Codex are equals (harness-parity-and-extensibility + test-both-harnesses): the capability works on both, the emitted artifact is each one's native format, and where a harness genuinely can't do a piece YET it is a LOUD, documented deferral with the reason (e.g. hook BLOCK is shared via exit-2, hook INJECT is now confirmed on BOTH and encoded in HookProtocol.injectableEvents, and hook REACT output stays CC-confirmed-only — state the caveat at every site that relies on it), never a silent CC-only path. (6) DOCS — per-adapter specifics live in the per-adapter guide (docs/harness-testing-claude-code.md / -codex.md), the agnostic guide links DOWN to them (no duplication), and the DECISION/MECHANICS are captured ONCE in a research doc cited from keyFiles so the next session doesn't re-derive them (the 'researching the same stuff over and over' failure — capture-once, link-everywhere; obey public-vs-internal-docs + no-internal-links-in-public-docs + doc-consistency). (7) TESTS — both-harness coverage where the surface is harness-facing, 100% on the gated modules, loud skips. (8) DISCOVERY / FRONT DOORS — a user-facing capability is only done when it is FINDABLE where users and agents actually look, not just wired in the code. Sweep EVERY entry point as ONE organism: the CLI --help/usage, the README front door (a one-liner + link per readme-brevity — never buried), the WEBSITE (vigiles.sh / site/ — if it's marketing-relevant, reflect it, held to the landing-site skill), the recommended AGENT PROMPT (docs/agent-setup.md), the model-invocable SKILLS, and the INTERNAL contributor docs (src/CLAUDE.md etc.). The --ci-only flag is the worked failure: it shipped in the CLI but was invisible on the README, the site, the recommended prompt, and the internal doc — so an agent taking the full default never learned it existed (a flag nobody can DISCOVER is not done; the fix added a pointer in the non-interactive summary + help + agent-setup + README). (9) ONE SOURCE / OUTPUT PARITY — where the same information renders in more than one place, both render from ONE source (SSOT / shared renderers — the DRY principle applied to user-facing surfaces), never hand-copied literals that silently drift. The audit HTML report and the site demo already share @vigiles/report-view + the AuditReport JSON (one component library, two variants — summary vs full, the landing-site invariant), so a report change lands in both for free. The audit CLI TERMINAL output is a SEPARATE plain-text renderer that CANNOT share React components — so it is the DRIFT SEAM to watch: a string changed in the terminal (a nudge, a leaderboard header, a ring label) must be checked against the HTML, and shared DATA (the AuditReport / score) is the source both should read rather than duplicating a literal in one. The THROUGH-LINE: when you add a capability, the question is never 'does the code work?' but 'is the whole LOOP closed — a user/agent on CC or Codex can FIND it (README, site, --help, prompt, internal doc), run it (CLI/GHA/init), be reminded of it, and see it rendered CONSISTENTLY across every surface (terminal + HTML + site + docs)?'. If any row is unchecked or unaddressed, the feature is NOT done. This rule is the umbrella over cohesive-cli-surface (CLI/GHA/init), great-agent-flow (the agent path), harness-parity-and-extensibility + test-both-harnesses (CC+Codex), lead-with-easy-adoption + readme-brevity (the README/site front doors), and the docs rules (per-adapter + dedup + tiers); it exists so the INTEGRATION across them is checked as one thing, not missed piecemeal.
Dual Language Tests
Guidance only — Harness/eval test SCRIPTS must run whether authored in JavaScript (.mjs / .cjs / .js) or TypeScript (.ts / .mts / .cts) — neither language is second-class. vigiles test / vigiles eval discover both (the *.harness.* / *.eval.* glob in src/adapters/claude-code/run-scripts.ts) and run a TypeScript script through tsx when installed, else Node's native type stripping (Node >= 22.6), failing with an actionable message when neither is available. The typed vigiles/vigiles/eval API and the zero-setup CLI scripts are two on-ramps to the same tiers. vigiles's OWN unit test suite, by contrast, is uniformly TypeScript (every src/**/*.test.ts, run by vitest), so the project dogfoods the typed path; the .mjs files under examples/harness/ stay JavaScript on purpose as the copy-paste CLI-fallback demos (see docs/testing-matrix.md).
Analogical Transfer
Guidance only — The way to find a non-obvious check in the (young, ad-hoc) agent-harness space is ANALOGICAL TRANSFER: take a mature CS principle and map it onto the harness — even when it looks crazy at first, because the looks-crazy transfers are where the useful checks turn out to be. The organizing thesis they roll up into: MINIMIZE THE HARNESS STATE-SPACE / make invalid states UNREACHABLE — via four instruments (construct=spec/types so invalid states can't be expressed; verify=lint so they're detected+rejected; gate=hooks so they can't be entered in-loop; test=evals so the remaining valid space is proven). The agent-world adaptation: when the author is a model, 'make invalid states UNREPRESENTABLE' (type-system, edit-time, human audience) becomes 'make invalid states UNREACHABLE IN THE LOOP' (gating hook, loop-time, format-agnostic) — which is why the leverage is in the HARNESS, not in the authoring format. FILTER every transfer by don't-cry-wolf: keep only ones that yield a DETERMINISTIC, high-signal check or gate that shrinks the state space; if it needs a model or emits a fuzzy score, it's a cute analogy, not a check. Worked examples: object-capabilities + taint → the lethal-trifecta forbidden-state check; effect systems → declared-vs-observed effects; totality → every hook decides every event; MDL → derivable-content-is-redundant.
Doc Tiers
Guidance only — THIS REPO IS PUBLIC (open-source on GitHub) — every file in it, including this one, is a public disclosure. There are TWO documentation audiences here, and neither carries product strategy. (1) PUBLIC-USER — README.md + docs/*.md: what a USER needs to ACT, written as behaviour and benefit, no internal mechanics, no half-finished spikes. (2) CONTRIBUTOR — CLAUDE.md (compiled from CLAUDE.md.spec.ts) + code comments: how the shipped code WORKS and why it is shaped that way, so someone can change it safely. THE TEST BEFORE WRITING ANYTHING: does this describe something that SHIPS? If yes, it belongs in one of the two tiers. If it describes what we intend to build, how we would win, what a competitor is doing, or how the thing would be sold, it does not belong in this repository — see no-product-strategy-here. Never link a public page into internal notes (no-internal-links-in-public-docs).
Audit Side Effect Free
Guidance only — vigiles audit and every DEFAULT lint/report path MUST be SAFE TO RUN ANYWHERE — it reads, reports, and runs only confined or own-repo-trusted checks; it never mutates the user's world and never executes a STRANGER's code unconfined. The dividing line is provenance + cost, not 'no execution'. A static/deterministic check (structural health, description-overlap, reference resolution) runs directly. Two ORTHOGONAL axes, not one: PROVENANCE protects the HOST (foreign code → bubblewrap-or-skip), CONFINEMENT protects your EXTERNAL STATE (your own hook/server reaching a real Postgres/API; this is the STATE-protection half that provenance does NOT cover). A plain audit EXECUTES NOTHING — it is a deterministic READ (four rings + fixes + report), safe + identical on every OS. The TWO executing checks (live MCP + trigger-rate) are OPT-IN behind ONE consent (decideExecute): at a TTY audit ASKS ONCE (bundled prompt, discloses cost) and REMEMBERS (audit.measure); headless it stays a read + a one-line nudge. There is NO execution flag — audit is a LOCAL report (like Lighthouse), NOT a CI step (CI uses vigiles lint); AUTOMATION tests the harness via the vigiles testing API + skills, never the report verb. On consent: LIVE MCP is own-repo only (never a stranger's server) because STARTING a server connects to a backend and deny-all-net would break the tools/list it performs; the trigger-rate STUBS skill bodies so no skill PROCEDURE ever runs. THE SAFETY BATTERY is DELIBERATELY NOT an audit check (narrowed 2026-06-27, 'no half-made shit pre-release'): running ARBITRARY hooks safely needs CROSS-PLATFORM confinement, and that's parked — bubblewrap is Linux-only, the env-scrub ephemeral floor + macOS sandbox-exec are the unbuilt exit criterion — so rather than ship a Linux-confined/Mac-unconfined ring, the battery lives in the vigiles testing API (guardrail-check/assertBlocksDisasters, confined by src/sandbox.ts) where you opt in EXPLICITLY (a test you wrote, no zero-config-safety promise to break); audit re-promotes a Safety ring only once one confinement works the same on macOS+Linux. The two ORTHOGONAL axes still hold for what audit DOES run — PROVENANCE protects the HOST (live MCP is own-repo only), and the deterministic read touches nothing. So audit is trustworthy and zero-surprise EVEN pointed at a prod-wired repo: the default reaches nothing (a read), and execution happens only on an explicit consent that discloses what it will touch. This is what lets audit be the zero-config gateway.
No Orphan Docs
Enforced by: vigiles/orphan-docs
Why: Every .md in an opted-in directory (vigiles scans docs/, declared via the .vigilesrc.json orphans block) must be referenced from at least one other markdown file — README, a compiled spec's Key Files, or another doc. Orphan docs rot silently because nothing tells the agent they're still load-bearing. The rule is OPT-IN (off unless the orphans block is declared; include defaults to docs/) because 'unreferenced' only signals rot in a hand-cross-linked corpus, not a nav-managed doc site (Docusaurus/MkDocs) where the page graph lives in config — an OSS sweep confirmed ~100% false positives there. Inverse of stale-reference detection: stale-ref catches specs pointing at missing files, orphan detection catches existing files that no spec points at. Mechanical check in src/core/orphans.ts, surfaced by vigiles lint.
Recompile On Spec Change
Guard: *.spec.ts → npx vigiles compile
Why: Recompile instruction files when any spec changes.
Regen Types On Config Change
Guard: eslint.config.*, package.json, pyproject.toml → npx vigiles generate types
Why: Regenerate type definitions when linter configs or package.json change.
Format Check
Guard: **/*.ts → npm run fmt:check
Why: Verify formatting on TypeScript file changes.