mirror of
https://github.com/zernie/vigiles.git
synced 2026-09-14 20:53:57 +08:00
v25
11 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
900dc7e06a |
refactor(spec)!: name things for what they are; retire the vigiles/experimental subpath (#169)
Three exported symbols were named for something other than what they are, and
two mechanisms were doing one job.
RENAMES (old names kept one major, @deprecated, removed one major AFTER this one):
claude() → instructionFile() compiles CLAUDE.md AND AGENTS.md, so a
name from one harness was never right
instructions`` → prose`` builds a prose FRAGMENT; the plural read
as "the instruction file", which is what
instructionFile() builds
agent() → experimental_agent() the shape is not settled, and the tag was
never there (git log -S empty)
SkillSpec.result: → postcondition: `result` already meant the subagent output
contract one screen down; the doc comment
had to spend a line disambiguating them
The alias window is not politeness. Measured: the previous rename shipped with
ZERO overlap (18.1.1 old names only, 19.0.0 new only). A container rollback put
the old node_modules under new sources, PreToolUse stopped loading, and a
PreToolUse that does not load denies EVERY Bash command — including the one that
fixes it. One window per major makes that pair unrepresentable.
SUBPATH RETIRED. vigiles/experimental is gone; its members moved to the door
their feature already uses. Of two mechanisms for one promise the subpath was the
weaker: it marks the import line, out of view by the time anyone reads the call.
Measured on the only user-facing example — with the prefix aliased away at
import, the marker survived at 0 of 5 call sites. So the prefix stays, the advice
to alias it away became an explicit prohibition, and the six define* entry points
are DECLARED prefixed rather than gaining it at re-export.
That de-aliasing is what unblocked moving the naming check into ESLint, and the
ORDER was load-bearing: measured before it, dropping the public/internal
exemption produced 14 findings, all from the re-export indirection. Measured
after, across all of src/ with no exemption: 22 tagged declarations, 0
unprefixed. The rule turns on silent.
EXPORT CLEANUP. vigiles/linting export * → curated, 103 → 79 symbols; its header
claimed to be curated with an `export *` one line below, and claimed the builders
were also at the package root (191 exports there, zero matches). ClaudeTool and
HookEvent deleted — zero references, and both had drifted off the dialect they
claimed to mirror; HookEvent listed 5 events, two of which do not exist, against
the 31 the vendor documents.
DOCS. experimental.md deleted and distributed per feature: the skill half into
skills.md §Status, the hook half into a new compiled-hooks.md §Status, and the
emit half — 111 of its 177 lines, a topic doc wearing a stability page's clothes
— into docs/emit-channel.md, which gains the "which channel when" comparison it
never had. The one cross-cutting sentence went to the README.
TWO GATES WERE GREEN AND WRONG, both found by this work:
- api:check reported "verified for 11 entries — no drift" while reading
dist/experimental.d.ts from a PREVIOUS build (tsc does not clean dist/) for a
source file that no longer existed. Now asserted against package.json
exports, with a mutation proving exit 1 on a phantom entry.
- check-export-prefixes caught MY error: the R3 services first landed on
vigiles/eval, where `paid_` means "this call can bill you" — and
startServices takes a ContainerRuntime and calls no model. Naming it
paid_experimental_startServices would have redefined paid_ as "expensive in
some sense". They went to the package root.
SEVENTEEN review findings landed on this branch, all real. Two mechanical
closures came out of them: src/example-imports.test.ts resolves every relative
import under examples/ instead of matching a string, and the naming rule now
ENUMERATES the three export paths it visits rather than describing its coverage
in the abstract. One attempt is recorded as REFUTED: teaching the doc gate to
flag our own exports as free variables measured 66 findings across 93 blocks, so
that gate's original scoping was right.
BREAKING CHANGE: vigiles/experimental is removed. Its exports are unchanged in
name and behaviour and are now reached from `vigiles` (the emit channel, the R3
disposable-service tier). Every renamed symbol keeps a working @deprecated alias
for this major.
|
||
|
|
41169e379a |
fix(audit,testing): split Tested/Evaluated, make coverage evidence honest, and stop three silent-success paths (#129)
* feat(audit): split Tested into Tested + Evaluated, with a `not measured` state `Tested` collapsed deterministic harnesses and real-model evals into one number, and its finding read "N surfaces with no vigiles test/eval" — one sentence for two prescriptions three orders of magnitude apart in cost. A harness (`*.harness.mjs`, `*.test.*`) is free, runs in milliseconds on every push, and asks whether a gate still catches what it claims. An eval (`*.eval.mjs`) spends real model calls, runs on a schedule, and asks the one question a deterministic read cannot: does this skill fire at all? Folded together, a repo with complete deterministic coverage and no evals scored identically to a repo with neither. - test-coverage: one discovery pass, partitioned by suffix into a harness tier and an eval tier; the union `covered`/`untested` fields are unchanged. Mirrored in the browser-safe twin. - scan: carries `untestedHarness` / `unevaluated` / `evaluable` (optional and additive; a producer predating the split falls back to the union). - audit-score: `Tested` (deterministic) and `Evaluated` (real-model), both advisory. `Evaluated` has THREE states — a number, a measured `0`, and `not measured` when the executing checks were skipped and no eval exists. Scoring 0 there would report the absence of a check as its result, which is the pattern this tool exists to name elsewhere. In that state the ring names the command that would measure it. - cli: threads the existing execute-skip signal into the ring. - report/HTML/JSON: both rings render; the inventory carries both counts. Grade weighting is unchanged — both rings are advisory and neither enters `reportDeductions`. Audited on this repo before and after: A (100/100) both times; only the diagnostic rings move (Tested 67 → Tested 58 + Evaluated 22). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012omt6sirxNMyZN2x5RhK2u * docs(site): the audit grades six categories, not five Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012omt6sirxNMyZN2x5RhK2u * fix(lint): a clean union must not read as 'firing verified' Every surface can have a deterministic harness while nothing has ever measured that a skill fires. The gate is unchanged (still the union), but the ✓ now says so instead of implying the question was asked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012omt6sirxNMyZN2x5RhK2u * test(site): regenerate the browser-parity fixture for the Evaluated ring The audit now emits a sixth category, so the Node-generated expected report and the in-browser one differ until the fixture is regenerated — which is what the test's own header prescribes for an intentional engine change. Regenerated with `node site/scripts/gen-parity-expected.mjs`. The diff adds the Evaluated ring and its two inventory counts and nothing else: grade and overall are untouched, which is the check that the split did not move the weighting. * fix(coverage): `Tested` measured a STRING, not a test DEFECT. A surface counted as covered when its path or namespace appeared as a SUBSTRING anywhere in any discovered test file (`content-reference`, in src/test-coverage.ts). Two halves, and the second was worse. REPRODUCTION (a real repo: 37 skills, 14 hooks). Appending ONE line to an existing harness file: // probe: skills/argument-arc moved the audit's untested count 33 -> 32. A comment. Not a test. Reverting the line put it back. So `Tested` was trivially gameable and the gaming was indistinguishable from real work. The false NEGATIVE: that repo's `.claude/pipeline/skills.harness.mjs` genuinely asserts over all 21 of its pipeline skills, but builds each path at runtime (`join(root, ".claude", "skills", name, "SKILL.md")`), so it contains ZERO literal `skills/<name>` strings (grep: 0 matches) and covered NOTHING. Generality was PENALISED: 21 trivial files each naming their own subject would have scored better than the one harness that actually tests all 21. This is the exact substitution vigiles names in other people's repos — presence of a surface taken for presence of the property, the way `noExplicitAny` in a config is taken for the rule being enforced. The tool was committing it in its own scoring. FIX, three parts (new src/coverage-evidence.ts — pure and browser-safe, so the disk detector and its in-browser twin cannot drift on what decides a grade): 1. DECLARATION. A test file may carry `vigiles:covers <surface> ...`, mirroring the existing `vigiles:ignore-test` marker convention on the other side. A runtime-path harness can now say what it covers; an accidental mention cannot. Declaration beats inference — the same move as declaring `allowed-tools` instead of inferring capability. 2. PROVENANCE. Every coverage decision now carries HOW it was reached — declared / colocated / mention — and the counts are printed by `formatUntestedReport`, by `formatScanReport`, and carried in `AuditReport.inventory.coverageEvidence` (additive; schema version unchanged). A repo whose coverage rests entirely on substring matches can finally see that about itself; "28 covered" and "28 names that happen to appear in a file" used to print identically. 3. The bare substring detector is KEPT, deliberately, and said so in the code: it is the zero-config path that makes an ordinary `foo.test.ts` naming `skills/foo` count without anyone learning a marker, and dropping it would flip honestly-covered surfaces to "untested" overnight. What it no longer reads is COMMENTS. A path in a comment is prose about a test; a path in executable code is the test reaching for the surface. Prose isn't policy, including ours. It is also now visibly the weakest tier in the report, and when it is the ONLY evidence the report says so in words. Grade weighting is untouched. TESTS prove both halves, each watched failing first: with the fix reverted, "a surface named ONLY in a comment is NOT covered", "a runtime-path harness that DECLARES its surfaces is counted" and "a declaration OUTRANKS a mention" fail (3 failed | 20 passed); with it, 23 passed. Also guarded: a mention in CODE still counts, and a `"https://..."` string literal survives comment-stripping. Docs (untested-skill/-subagent/-hook) updated — untested-skill.md previously recommended "add the skill's path in a comment", which was advice to exploit the defect. * fix(site): key the browser-test prebundle to dist, so a stale cache can't lie DEFECT. `site/src/demo/browser-parity.browser.test.ts` guards node:zlib vs pako divergence by comparing an in-browser AuditReport against one generated in Node. `site/vitest.config.ts` pre-bundles the engine (`optimizeDeps.include` on `@engine/scan-files` / `@engine/audit-report` / `pako`) and Vite caches the result in `site/node_modules/.vite`. That cache SURVIVES `npm run build` — Vite invalidates on config/lockfile changes, not on the mtimes of an aliased path outside node_modules. So the browser side can compute against a PREVIOUS engine while Node computes against the current one. REPRODUCTION (observed 2026-08-07, twice — once by accident, once deliberately). Accidentally: an engine change added a report field; the parity test failed showing the field present on the Node side and absent in the browser. I read that aloud as a genuine engine divergence and started hunting a pako bug that does not exist. `rm -rf site/node_modules/.vite` -> 42/42 pass. Deliberately, to prove the fix: plant an extra `inventory` field in src/audit-report.ts, `npm run build`, regenerate the expected fixture, run the browser suite with a warm cache. - ORIGINAL config: `1 failed | 41 passed`, diff showing `"probeStaleCache": 1` on one side only — a field-level diff that reads exactly like a real divergence and says nothing about the cache. - THIS config: `42 passed`, cache dir rotated `.vite-engine-8baf73f9e373` -> `.vite-engine-a52a27e0137e`. Reverting the plant returns the fixture to its committed bytes. FIX. Key the prebundle cache DIRECTORY on a fingerprint of `dist/` (`site/vite.engine-stamp.ts`; `cacheDir: node_modules/.vite-engine-<stamp>`), sweeping the directories keyed to other fingerprints so they don't pile up. WHY a key and not the alternatives. Comparing the cache's mtime against `dist/` and forcing a re-bundle, or clearing `.vite` in the test script, both work and both leave the bad state REACHABLE — a comparison someone can get wrong, or a script step bypassed by running `vitest` directly. Keying the directory removes the state instead: a different `dist/` is a different path, so "prebundle built from another engine" cannot be reached, and an unchanged `dist/` still gets a warm cache. By construction rather than by check — the argument this project makes everywhere else about removing a capability rather than policing its use. The test's header instruction ("pako and node:zlib disagree — do NOT paper over it") was correct for one of the two states and actively misleading for the other; it now says which state is no longer reachable and why, and the assertion carries the same note in its failure message. * fix(site): make the parity-fixture generator emit prettier-clean JSON DEFECT. `site/scripts/gen-parity-expected.mjs` wrote the fixture with `JSON.stringify(x, null, 2)`, which is NOT what prettier prints for JSON — it keeps short arrays exploded across three lines where prettier collapses them. CI gates on `prettier --check .`, so every regeneration of the fixture committed a file that reddened CI later and far from the cause. It just did: branch head |
||
|
|
3017f70ef5 |
feat: JVM/Go linters + audit/init/compile hardening + adoption UX (#114)
Closes #107, #109, #110, #111, #113. Vlad's 5 issues (skill allowed-tools round-trip, detekt/ktlint/checkstyle/golangci-lint catalogs, 2 scan false-positives, non-JS harness guide, testGlobs docs), a 14-bug dogfood batch, adoption/demo UX, and prevention refactors (one markdown-it fence oracle replacing 5 regex toggles; one js-yaml detekt-config parser replacing 3). LinterAdapter port added to the roadmap as P0. |
||
|
|
18d7cfcae4 |
docs: honest versioning, README scannability, and docs restructure (#67)
Fix the version claim (v12, not 0.x; the 0.0.0-semantically-released placeholder was the source), reframe the eval section around cost measurement, trim the README for scannability (Diátaxis link buckets, tighter reconciliation + proofs), reorganize the docs index into Guides/Reference/Explanation and add six missing docs, and consolidate overlapping docs (related-tools→comparison, inline-mode→markdown-mode, agent-setup+agent-workflows). Relocate the eval-architecture design-of-record ADR from docs/ to research/ per the doc-tiers rule, and fix the library entry points (vigiles/linting is the compiler surface) plus the Codex setup path. |
||
|
|
15c0607a5b |
feat!: eval lock (CI staleness gate) + full Codex parity for hooks & inject (#53)
* feat!: eval lock (CI staleness gate) + per-adapter harness versionKey Real-model evals run on your subscription, so they only run locally — never in CI. The eval LOCK closes the resulting gap: a committed integrity stamp that lets CI verify the committed numbers still match the current inputs WITHOUT a model call (distinct from the gitignored, local-speed eval cache). - `vigiles eval --update` (local): records each NAMED eval's report to a committed `.vigiles/eval-locks/<name>.lock.json` + prints the per-number delta. - `vigiles eval --check` (CI): recompute the input hash, fail "stale" on a mismatch — binary-free, deterministic. Green no-op until the first lock is committed (smooth adoption). - The clean split: the lock stores only the model's observed behavior; the script's own assertions re-run live against the replayed report, so a threshold-only edit is a valid replay while an input change is stale. - Honest scope: the harness version is provenance, NOT a hash input (CI's pinned claude ≠ a dev's local claude would false-trip), and there is no nightly run — model/harness drift is a local `--update` concern. Also extracts the harness version reduction to a per-adapter port method (`HarnessRuntime.versionKey`): Claude Code keys major.minor (~quarterly), Codex opts out with "" (its minor is patch-cadence, ~weekly). This fixes the local cache churning weekly for Codex and is where the cadence decision belongs. Whole-package coherence: the GHA gains `command: eval-check`, `vigiles init` scaffolds the eval-check CI job, and this repo dogfoods it. Wired at both eval seams (runEvalWith + measureTriggerRateWith), 100%-covered, docs updated (internal research + public guides). BREAKING CHANGE: `HarnessRuntime` (exported on `vigiles/adapter`) gains a required `versionKey(raw: string): string` method. Third-party adapters must add it; return "" to opt out of version partitioning. First-party adapters (claude-code, codex) are updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVmD8n7P62fuJ91aQh9yY9 * feat: eval-lock agent awareness (skill + nudge hook) + cohesive-delivery rule Close the loop on the eval lock so the AGENT knows it exists and maintains it — without the invasive option of editing the user's CLAUDE.md. - test-harness skill documents the lock lifecycle (--update/--check, commit the lock, re-run after editing a skill). - A PostToolUse nudge hook (hooks/eval-lock-nudge.sh → hook-runtime eval-lock-nudge): after editing a SKILL.md / *.eval.* with committed locks present, inject a NON-BLOCKING reminder to re-run `vigiles eval --update`. Self-gated (silent until a lock is committed), never blocks (the gate is CI's `eval --check`). Wired into the vigiles plugin; runHook + unit + .sh tests. - NO init edit to the user's CLAUDE.md — verified that's not how plugins deliver context (no plugin-level instructions file exists; the channel is a hook/skill). Docs + dedup: - research/agent-context-delivery.md captures the VERIFIED CC/Codex context-delivery mechanics ONCE (SessionStart additionalContext is the plugin channel; CLAUDE.md is the user's file; skill descriptions always-in-context; Codex AGENTS.md + hooks but inject deferred) so we stop re-researching it. - Per-adapter guides (harness-testing-claude-code/-codex.md) explain how the reminder reaches the agent per harness (CC injects; Codex inject is a documented follow-on), with the agnostic guide linking DOWN to them (no duplication). Cohesive-view rule: - New CLAUDE.md `cohesive-feature-delivery` rule — a definition-of-done CHECKLIST (CLI × GHA × init × agent-awareness × both adapters × per-adapter deduped docs × tests) so a feature's INTEGRATION across surfaces is checked as one thing, generalizing cohesive-cli-surface + harness-parity + great-agent-flow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVmD8n7P62fuJ91aQh9yY9 * test: CLI e2e for the eval-lock invalidation flow (update → check → stale) Closes the gap flagged in review: the joined CLI flow wasn't tested as one thing. A real *.eval.cjs fixture (model-free — drives runEvalWith from dist with a fake runner) run through the built CLI proves the whole loop: `eval --update` writes a committed lock, `eval --check` with the same inputs replays green (no model), and an input change makes `eval --check` go red ("stale"). Binary-free, no subscription — exactly how it runs in CI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVmD8n7P62fuJ91aQh9yY9 * docs: scannable eval-lock docs + prose-clarity & easy-adoption rules Make the eval-lock docs readable at a glance and frame upkeep as agent-driven, not a manual chore. Docs (scannable rewrite — ASCII flow diagram, comparison tables, ✅/❌/⚠️ signposts, short sentences, no walls of text): - docs/harness-testing.md — the lock how-to now leads with "you mostly won't touch this by hand; the test-harness skill records it + a hook reminds you," then a diagram + tables. - docs/harness-testing-claude-code.md / -codex.md — the per-harness nudge delivery as tables; the Codex inject caveat is one clear ⚠️ row. - docs/eval-architecture.md — the "what's stale vs a free replay" split as ✅/❌ bullets. - skills/test-harness/SKILL.md — Step 5 now tells the agent to run `eval --update` + commit the lock as part of its job (so it IS done for you). - README — the Quick-start reassurance leads with "Adoption is smooth: one command, then your agent does the rest," names skills AND hooks, no chores. Two new CLAUDE.md rules: - prose-clarity — public prose must be clear + scannable (lead with the point, one idea per sentence, define/drop jargon, tables/bullets/ASCII/Mermaid over walls, emoji as signposts, one bold per point). Prefer ASCII/Mermaid; never commit generated raster images into docs. - lead-with-easy-adoption — docs (README first screen especially) must make it clear up front that adoption is low-friction because skills + hooks do the work; frame every feature's upkeep as agent/skill/hook-driven, not homework. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVmD8n7P62fuJ91aQh9yY9 * docs: prose-clarity sweep across 13 user-facing guides Apply the new prose-clarity rule to the prose-heavy public guides: lead with the point, one idea per sentence, tables/bullets/ASCII over walls of text, ✅/❌/⚠️/ℹ️ signposts, one bold phrase per point, jargon glossed or dropped. Net −578 lines — shorter and far more scannable, with meaning unchanged. Swept: for-plugin-authors, what-vigiles-catches, measuring-skills, skills, harnesses, comparison, faq, sandboxing, safety, agent-setup, agent-workflows, verifying-instruction-files, compiled-hooks. The onboarding pair leads with the low-friction "one command, the agent does the rest" framing. Reference docs (cli, testing-api, spec-format, …) and the per-rule docs were deliberately left alone — they're already table-shaped, and churning them is high-risk / low-value. Verified: no heading text changed (anchors intact), no `research/` links added to public docs, the validation-rules matrix is complete (28 rule links → 28), safety claims retained, command refs resolve (self-command-refs), no orphan docs, fmt clean. Produced by parallel rewrite agents under strict preserve-anchors/links/ code constraints, then verified + spot-read. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVmD8n7P62fuJ91aQh9yY9 * feat!: encode hook inject support into HookProtocol.injectableEvents Codex inject was documented as "deferred" but the official Codex hooks docs confirm `additionalContext` is honored on SessionStart / UserPromptSubmit / PreToolUse / PostToolUse / SubagentStart — the same shape as Claude Code. The gap existed because inject support was a prose deferral, not a tested contract: nothing in the adapter conformance kit asserted a shellHooks harness could actually deliver an inject hook. Encode it as a required port field so the gap can't recur silently: - HookProtocol gains `injectableEvents: readonly string[]` — the events that honor `additionalContext` injection, per-adapter. - claudeCodeHookProtocol: [SessionStart, UserPromptSubmit, PostToolUse]; codexHookProtocol: + [PreToolUse, SubagentStart]. - Conformance rejects a shellHooks adapter with an empty list; the adapter-contract suite asserts every shellHooks adapter declares the events vigiles' shipped hooks use (PostToolUse, SessionStart) — the test that would have caught the original miss. - `compile` warns on an inject hook only when its event isn't injectable on the target harness (react output stays CC-confirmed-only). - Correct every "Codex inject deferred" reference across docs, research, and CLAUDE.md; document injectableEvents in the adapter API + authoring guides. BREAKING CHANGE: HookProtocol (exported on vigiles/adapter) gains a required `injectableEvents` field — a third-party adapter must declare it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: refresh HANDOFF for the eval-lock branch + inject-port encoding Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: fan out hook install + the CI workflow to every declared harness A repo can enable BOTH harnesses (`harness: ["claude-code","codex"]`), and instruction files already mirror to both — but `compile` installed a hook into only ONE harness (`installHooks` ignored `config.harness` and auto-detected a single adapter), and the scaffolded CI workflow hardcoded the Claude Code binary. So an inject/gate hook that works on both could land in `.claude/settings.json` while silently missing from `.codex/config.toml` — the "works on CC, absent on Codex" failure the harness-parity rule exists to prevent. - `resolveHarnessAdapters` (adapter-registry.ts) returns the FULL declared set: `--harness=` flag → that one; else config `harness` list → all of them; else auto-detect → the detected one (de-duped by name). - `installHooks` loops over it, merging the SAME compiled hook into every enabled harness's native config (JSON / TOML), with per-harness warnings. - The `init` CI workflow is harness-aware: the deterministic harness job installs `@anthropic-ai/claude-code` and/or `@openai/codex` per declared harness (`harnessTestBinaries`). Skills compile byte-identical across harnesses and subagents are CC-only by design, so hooks + the CI binary were the real fan-out gaps. Tests: resolveHarnessAdapters units (fan-out / flag-singular / detect / alias-dedup), a both-harness hook-install e2e (the same hook in both configs), and harness-aware workflow e2es (codex-only, both). Documented as §4 in research/multi-harness-compile.md + the compiled-hooks guide. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: wire vigiles's nudge hooks into .codex/config.toml during init Closes the last Codex-parity gap in agent-awareness: Claude Code gets the eval-lock + refs nudge hooks from the global marketplace plugin, but Codex has no global plugin store, so it got skills only — the proactive nudges never reached a Codex user (the "works on CC, silently absent on Codex" failure). Codex config is repo-committed (the idiomatic place), so `init` now wires the two PostToolUse nudges into `.codex/config.toml`: - `codexPluginHooks` (pure) declares them as DIRECT `npx vigiles hook-runtime …` commands — no plugin root, no vendored bash script. The runtime entrypoints read the event JSON on stdin and emit the `additionalContext` shape Codex honors on PostToolUse (confirmed + encoded in HookProtocol.injectableEvents). - `applyCodexPluginHooks` (pure, unit-tested) merges them idempotently: re-runs replace in place, and the user's own Codex hooks + every other config key are preserved. `wireCodexHooks` in cli.ts does only the IO. - Safety: only an INTENTIONAL exit 2 blocks an edit (the refs nudge under `unmarked-refs: error`); an npx-resolution failure exits non-2, so a missing dep never blocks. Loud, documented deferral (no-silent-skips): the SessionStart lint summary (plain stdout, unconfirmed on Codex) and the compile-on-edit / pre-edit guards (filename-gated bash, no harness-neutral entrypoint yet) stay manual on Codex. Also: the remaining pre-release Codex item — running the eval tier against the real `codex` binary once — is added to the launch-readiness P0 list in the roadmap. Tests: codexPluginHooks shape + applyCodexPluginHooks (fresh / idempotent / preserves user config); the planPluginInstall codex notes/vendors updated. Verified end-to-end that `init --harness=codex` writes the config and a pre-existing `.codex/config.toml` (model + a user hook) is preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test: update codex install e2e for the new vendors:true (repo hooks) The codex install plan now writes nudge hooks into .codex/config.toml, so `vendors` flipped to true. Updated the unit assertion last commit but missed this e2e (run by both the `e2e` and `test` CI jobs). The skills-are-global checks below are unchanged and still stand. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: cover the non-Actions lock message branch + address Codex review The `test` job's coverage gate failed: `emitLockMessage`'s console.warn/ console.log branches (the non-GITHUB_ACTIONS path) are unreachable in CI, where GITHUB_ACTIONS is globally set. Added a wiring test that DELETES the var to exercise both branches regardless of environment. Also addresses two Codex review findings on the eval-lock work: - P1 (soundness): `evalArmsInputs` omitted `spec.stubs` from the hash, though tool stubs are written onto PATH and are model-facing input — so a changed canned CLI output after `--update` left the same `inputsHash` and `--check` replayed stale results. Now folds a canonical (name-sorted) view of the stubs into the hash; a new wiring test proves a stub change → stale. - P2 (collision): two eval names normalizing to the same slug (`foo/bar` vs `foo bar` → `foo-bar.lock.json`) could let `--check` replay the WRONG eval's report. `readLock` now rejects a lock whose stored `name` differs from the requested one, degrading a collision to a safe miss (→ stale/re-run), never wrong data. Unit-tested. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: two more eval-lock soundness gaps from Codex review - An unnamed eval in `--check` mode called `produce()` (ran the model), breaking the no-model-in-CI contract: in CI there's no subscription, so it would hit the model or fail for missing auth. Now `check` mode throws a clear, actionable error for an unnamed eval (never reaching the model); `off`/`update` keep producing (the eval just isn't gated locally). - `spec.ephemeralEnv` toggles the trial's run environment (scrubbed env + throwaway HOME vs the inherited process env) — a model-facing input that was omitted from the lock hash, so a report recorded under one mode could be replayed under the other with the same inputsHash. Now folded into the hashed inputs. Both proven by new wiring tests (unnamed-check throws without calling the runner; an ephemeralEnv toggle → stale). Coverage holds at 100% lines/ statements/functions (verified with GITHUB_ACTIONS=true). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: fold the harness identity into the trigger-rate lock hash Codex review (P2): a trigger-rate eval recorded with one harness driver and later switched via `measureTriggerRate(spec, { evalDriver: codexEvalDriver })` hashed identically (prompts/model/plugin unchanged), so `--check` could replay a Claude-recorded report for a Codex run — defeating the gate for the Codex eval path this PR adds. `EvalDriver` gains an optional `harness` identity (defaults to "claude-code", so existing single-harness locks are unaffected); `claudeEvalDriver` and `codexEvalDriver` declare theirs, and `measureTriggerRate`/`...With` thread it into the hashed trigger inputs. A new wiring test proves a harness switch → stale. API surface regenerated (additive `harness?` on EvalDriver). This closes the input-hash bug class on the trigger-rate seam (the runEval seam's stubs + ephemeralEnv were fixed in the prior two commits). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: refresh HANDOFF — PR #53 merge loop + the 5 review fixes Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: three more Codex-review findings (matchers + plugin-root path hash) - Codex nudge hooks used Claude tool names (`Edit|Write`) in their matcher, but Codex's edit tool is `apply_patch` — so the eval-lock + refs nudges wired into `.codex/config.toml` would never fire. Matcher → `^apply_patch$`. - The compiled-hook FAN-OUT to Codex regex-wraps the raw matcher without translating tool names, so a CC-authored `tools("Edit","Write")` gate emits `^(Edit|Write)$` into Codex config and silently never fires though compile reports success. vigiles has no cross-dialect tool-name map yet, so install now WARNS LOUDLY when a tool-matched hook is fanned to a non-CC harness (no silent non-firing success); a real mapping is the follow-up. - For eval arms using `plugin`, `resolveHarness` expands `${PLUGIN_ROOT}` to the checkout's ABSOLUTE path before the settings are hashed, so a lock recorded at `/home/dev/...` was falsely STALE when `--check` recomputed it at `/home/runner/...` in CI. `stripPluginRoot` normalizes the prefix back to a token → location-independent hash. New wiring test: the same plugin at two different abs paths replays (not stale). Coverage holds at 100% lines/statements/functions (verified GITHUB_ACTIONS=true). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: guard stripPluginRoot against a hookless plugin's undefined settings A plugin that ships only instructions/skills (no hooks) resolves to settings:undefined; JSON.stringify(undefined) is undefined, so the prior .split() threw before the eval could run. Guard the non-string case (pass it through). Regression I introduced in the plugin-root normalization last commit; new test covers a hookless-plugin arm (update + check, no throw). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test: add a lock-hash COMPLETENESS guard for both seams The Codex review kept finding the same class of bug: a model-facing input omitted from the lock hash, so `--check` replays stale results (stubs, ephemeralEnv, driver-harness, plugin-root all slipped through). Our tests proved the MECHANISM (record→replay, task-change→stale) but never asserted the input set was COMPLETE. This adds a structural guard: for each input that steers the run, mutate ONLY it and assert the lock inputsHash CHANGES — one parametrized test per seam (runEval's evalArmsInputs + trigger-rate's triggerInputs). A new input added to EvalSpec/TriggerRateSpec without hashing it now fails a row HERE in CI, instead of a reviewer finding it later. Covers task/model/tools/fixture/ arm.files/arm.settings/arm.model/interceptTools/stubs/ephemeralEnv and prompts/irrelevantPrompts/model/harness. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
d8c639f8da |
refactor(cli)!: consolidate the verb surface (13→8) + docs/README overhaul (#46)
* docs: sharpen README lint/guard copy - Replace internal "7 catalogs" jargon with "7 linters (ESLint, Ruff, Clippy, and four more)"; Cedar stays in the subdoc - Drop the plugin-audit paragraph from the Lint section (secondary use case, still reachable from More) - Rewrite the Guard intro to lead with the concrete "fail open" failure and name what vigiles emits (exit code, JSON, AST-backed matcher) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: surface the $0/subscription cost story in the first paragraph The "no metered tokens, runs on your Claude subscription" wedge was buried in the last line of the Eval section. Pull a crisp version into the opening paragraph (above the fold); the Eval section keeps the detailed promptfoo/DeepEval contrast as proof. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: surface agent-writes-the-harness flow, reorder More, drop internal links - Quick start now leads with "then your agent writes the harness for you": the three model-invocable skills (test-harness/strengthen/ edit-spec) and the plain-English ask each one answers - Reorder More so CLI sits above the leaderboard (whose link points into the CLI docs); trim the now-redundant Skills bullet to a pointer - Stop linking internal research/ docs from the README (the moat link and the Research index) per public-vs-internal-docs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(compile): auto-refresh an existing harness.gen.ts compile now keeps a whole-harness registry in sync as a side effect, so the user never hand-runs generate-harness. Gated on the file already existing (compile maintains a registry you opted into, never imposes one) and cheap (parses specs, no linter spawn — unlike generate-types/ generate-schema, which spawn every linter and stay on the config-change guard). A duplicate agent name fails the compile. Docs: note the auto-refresh in the compile section and mark the generate-* artifacts as dev-toolchain output rarely run by hand. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(src): add nested CLAUDE.md with a high-bar-for-new-commands rule Directory-scoped guidance loaded when working in src/: adding a new CLI verb or hook-runtime subcommand must clear a high bar (genuinely new mental action, no overlap with an existing command, siblings collapse to one verb + subcommand, runtime entrypoints stay hidden). Complements the root cohesive-cli-surface rule, placed next to the code that defines the command surface (cli.ts, cli-commands.ts). Spec-managed (require-spec + integrity); dogfoods vigiles compile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: drop the "(generated)" annotation from the API reference link Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(build): stop src/CLAUDE.md.spec.ts shadowing the root spec + add docs rule The nested src/CLAUDE.md.spec.ts (added last commit) compiles under rootDir:src to dist/CLAUDE.md.spec.js — the exact path loadSpec resolves the root CLAUDE.md.spec.ts to, so recompiling the root spec loaded the src spec and clobbered CLAUDE.md. Exclude src/*.md.spec.ts from tsc so both load via the tsx fallback (as the root always has). Also adds the no-internal-links-in-public-docs guidance rule (public docs never link research/*.md; keep the fact in research, don't link it), reconciles the old "link to research" line in public-vs-internal-docs, and files the deterministic lint-rule version as a P1 roadmap item. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: separate public/internal indexes + surface eval safety - docs/README.md no longer links into internal research/ (public docs don't point at the private record) - research/README.md is explicitly labelled the internal record - README Eval section gains a "Safe to repeat" note (sandbox + ephemeral dir + egress + interceptTools, past-the-sandbox case flagged) linking docs/safety.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: stop public docs linking internal research/ + add safety FAQ Strip every research/*.md link from docs/*.md (the no-internal-links-in- public-docs rule) — ~60 links across 16 guides plus the docs/ index; prose kept readable, no orphaned research doc (all still cited from CLAUDE.md keyFiles / research/README.md, orphan-docs green). Add a FAQ to docs/safety.md answering the side-effects-beyond-the-sandbox question honestly: network via egress allowlist, a real DB/redis/browser via a disposable container you stand up (turnkey provisioning is a committed next step), irreversible externals via interceptTools, and the no-Docker deterministic/record-replay tiers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(cli)!: consolidate verbs — measure/explain→scan flags, refs→lint, generate-*→generate <kind> De-sprawl the human verb surface from 13 to 8 (the high-bar-for-new-commands rule, dogfooded). All breaking: - `measure <dir>` → `scan <dir> --trigger` (the model-gated behavioral column folded into scan; trigger-rate + collision matrix unchanged) - `explain <dir> [name]` → `scan <dir> --explain [name]` (the deterministic WHY, a sibling of --fix-plan on the same ScanReport) - `refs <file>` → folded into `lint` (the unmarked-refs check is already a lint rule; the PostToolUse hook keeps its shared collectRefIssues entry) - `generate-types|schema|harness` → `generate <kind>` (one verb + subcommand; the three dev-toolchain generators grouped, not N hyphenated siblings) scan reuses handleMeasure/handleExplain directly behind --trigger/--explain. Docs, help text, the regen guard, tests, and the self-command-refs dogfood all updated together (cohesive-cli-surface); 1588 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: split the GitHub Action reference out of cli.md into its own doc cli.md was 718 lines (kitchen sink). Extract the ~108-line GitHub Action section into docs/github-action.md (quick start, inputs, output channels, versioning) and leave a short pointer; retitle the cli.md intro. Update the README More bullet (CLI · GitHub Action) and the docs index. cli.md → 619 lines. orphan-docs + self-command-refs green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(lint): add prefer-compiled-hooks rule — one nudge toward compiled hooks A single repo-level recommendation (one finding regardless of hook count): when a plugin ships hand-written hook commands that aren't compiled vigiles/hook artifacts, nudge toward compiling them — the message links docs/compiled-hooks.md and points at guardrail-check for verifying the ones you have. A discovery nudge, not a defect: the hand-written shell lane stays first-class, so it fires ONCE (never per-hook), defaults to warn (info- level ℹ; informational, doesn't fail CI), and is opt-out. Detector (manualHookCount: a command that isn't a hook-runtime invocation) lives in scan.ts and is shared by scan + lint (one-detector-no-drift). Doc + matrix rows added (rules-docs-in-sync). Honest caveat in the doc: it's form-based, so it stays a recommendation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(research): record the eval-startups critique mapped onto positioning New research/eval-startups-positioning.md captures Thomas Liao's "why eval startups fail" critique and its implications: vigiles dodges the trap (harness-eval not model-eval, tooling not services, eval already a feature), but it's a yellow flag on the public ecosystem-benchmark as a MOAT (Goodhart-gameable → keep it a thin acquisition flywheel) and it independently validates the attestation/safety direction (capability-diff, guardrail proof) as the only defensible eval-shaped niche. Linked from research/README.md, measurement-authority.md (whose benchmark thesis it challenges), and strategic-synthesis-2026-06.md (whose Tier-2 conformance/attestation play it validates). Internal-only per no-internal-links-in-public-docs; orphan-docs green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: purge internal positioning from public docs (jargon, name-drops, benchmark) Public docs (README + docs/) now state user-facing value, not vigiles's internal strategic vocabulary: - strip "moat" from 14 places → user-facing wording (engine / cost advantage / differentiator / cross-reference) - remove all bare research/*.md name-drops from docs/ (the backtick form the link-strip + deterministic check miss); scope-descriptions reworded to "configured directories" rather than naming research/ - README: drop the "ranks the hyped ecosystem: what works vs hype" benchmark banner from the Eval row + kicker (depth stays in measuring-skills.md) — per eval-startups, the benchmark is a flywheel, not a headline - docs/cli.md: "moat #2 (research/…)" → "the capability-diff check" Codified in CLAUDE.md: public-vs-internal-docs now has a strategic- vocabulary clause (moat/measurement-authority/flywheel stay internal; public docs name the benefit), so this doesn't regress. orphan-docs + self-command-refs green; the public↔private split stays docs/ vs research/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(readme): apply multi-persona review fixes From a multi-persona README review (power user / staff eng / plugin author / non-TS user / eng manager): - non-TS users: a visible "No TypeScript needed — lint + inline comments work on any CLAUDE.md" line (was invisible; contradicted progressive adoption) - plugin authors: surface "vigiles scan ranks a marketplace, no key" above the fold (was buried in More) - eng-manager cost: hero now says deterministic layers run free in CI; merged kicker+affordability nails "real-model on a dev's own subscription, never a CI token" - staff-eng skepticism: the 2/7→7/7 claim now reads "measured, not asserted"; the typed-pipeline claim is concretized to the real pipe()/needs() mechanism - fixed a broken blockquote and trimmed the safe-to-repeat para to hold length Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(readme): capture the README design direction as an editor-facing comment A top-of-file comment block records the framing the README must hold: lead with benefits, present compiled specs as the easy/auto-enforced next step (skills write them, hooks compile them; JS→TS evolution, not a wall), keep all four instruments first-class incl. Eval, stay scannable + under the length cap, and no internal vocabulary/research links. Guides future edits. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(readme): benefits-forward rewrite + specs-are-easy framing Per the README direction: lead with what the user GETS, and present the typed spec as the frictionless auto-enforced next step, not a TS wall. - hero: stronger, concrete failure modes (stale ref / guard fails open / skill never fires), keeps Agent=Model frame, cost above the fold; dropped the redundant centered pitch (merge) - table cells condensed to punchy benefit one-liners - "You don't hand-write specs — your agent does. TypeScript for your harness: opt-in, gradual, auto-compiled, never a chore" (skills write them; JS→TS evolution) - de-dupe cost (drop "not metered tokens" from the Test cell) - "Not for you if …" honest-scope line - docs/verifying-instruction-files.md: back the claim — "you rarely hand-write one; edit-spec/strengthen write it, a hook recompiles on save" Eval stays a first-class instrument. fmt + self-command-refs + orphan-docs green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(readme): don't lead callouts with "typed spec" — benefit first Casual readers shouldn't be greeted by "a typed spec is a program" / "TypeScript for your harness" in the prominent callouts. Reframe both to lead with the outcome and demote the spec to a soft, opt-in aside: - specs-are-easy block: "You don't hand-write any of this — your agent does … you never touch it by hand" (drop the TypeScript-for-your-harness headline) - Lint blockquote: "Bad states stop compiling … turn it up only when you want it" (drop "Markdown is prose; a typed spec is a program") - the edit-spec bullet says "edits the source", not "the typed spec" "No TypeScript?" reassurance and the optional .spec.ts mention stay. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: refresh HANDOFF for the CLI-consolidation + docs/README overhaul session Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: prettier-format HANDOFF Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(ci): update the stale generate-types ref the verb rename missed The CI "Check generated types compile" step ran `node dist/cli.js generate-types` — renamed to `generate types` in the verb consolidation, so the job failed with "Unknown command". self-command-refs didn't catch it because it didn't scan .github/; add .github/ to its scan scope so a stale `cli.js <cmd>` ref in a workflow fails the dogfood from now on. Also saves the prefer-compiled-hooks-vs-require-hook-spec decision to research/hook-modes-and-testing.md (the nudge, not a requirement; the write-hook authoring skill is the real authoring lever, unbuilt). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(lint): eslint-ignore src/*.md.spec.ts (excluded from tsconfig) The dist-collision fix excluded src/*.md.spec.ts from tsconfig, so eslint's type-aware project service can't resolve src/CLAUDE.md.spec.ts and errors "not found by the project service". It's a tsx-loaded build input (like the root CLAUDE.md.spec.ts, which isn't linted either), so ignore it in eslint to match. Latent since the exclude landed; surfaced now that the earlier generate-types step no longer fails first. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
2393b155c6 |
fix: repair broken main — 100% coverage gate + CLAUDE.md integrity/format conflict (#38)
* fix: repair broken main — 100% coverage gate + CLAUDE.md integrity/format conflict PR #37 left main red on two CI jobs: - test job: the eval.ts ephemeral-env + interceptTools compose path (the `if (overlay) Object.assign(env, overlay)` line) was unexercised, dropping statement coverage to 99.79% and failing the 100% gate. Add a unit test that drives both features together and asserts the intercept overlay survives the env scrub. - check job: `vigiles compile` emits markdown like `skills/*` but prettier re-escapes it to `skills/\*`, desyncing CLAUDE.md's integrity hash. The compiled CLAUDE.md is a vigiles-owned exact-format artifact (hash-gated), so add it to .prettierignore alongside the other tool-generated files and recompile so the committed bytes match the spec. The spec stays formatted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: update session state timestamps Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: tighten README — drop "pillar", footnote the Latin, sharpen the sell - Move the "Quis custodiet ipsos custodes?" epigram to a footnote on the name; open with a real one-line pitch instead. - Drop the "pillar" jargon everywhere; lead with the two verbs (Lint / Test). - Lint hook now leads with a renamed file + script (the relatable case), not a disabled eslint rule. - Rename the Test heading "does your harness fire?" → "do its job?" and lay out the full scope as bullets (block / trigger w/ recall+precision / behaviour via evals / safety) — firing is only one of them; evals are now explicit. - State plainly that tests are deterministic (no model, no API key) and evals run on your Claude subscription, never metered API tokens, with links. - Cut ~45 lines of wall-of-text (Test section more than halved). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: make README a scannable front door + add promptfoo eval comparison Rewrite the readme-brevity rule (in CLAUDE.md.spec.ts → recompiled) to demand a WOW-on-first-screen, phone-scannable front door: short (~140 line target), no walls of text, great formatting, links out for detail, and a required promptfoo comparison in the evals section. Fix the README to match: add a compact 3-column cost table contrasting promptfoo/DeepEval (metered API SDK, per-token) against vigiles ($0 extra, runs on your Claude subscription, most tiers need no model at all). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: move Quick start above the fold (hook → what → get-it-now → why) Place the slim Quick start right after the what-it-does table, before the Lint and Test deep dives, so a skimmer can try `npx vigiles init` in seconds; the deep-dive sections now read as the "why it's worth keeping". Encode the rule in readme-brevity (CLAUDE.md.spec.ts → recompiled): install path above the fold, verbose setup kept in a <details> so the top stays lean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: add docs-quality rule + align lint/test guides with the new README - New `docs-quality` guidance rule (CLAUDE.md.spec.ts → recompiled): the docs/ reference docs are the HOW the README defers to — NOT held to README brevity (depth is their job), but to a doc-appropriate polish bar: open with a one-line "what this is" + an up-link to the README, stay scannable (headings, Contents, tables, runnable code), cross-link siblings + "See also", and stay consistent with the front door (terminology, the promptfoo cost contrast, no stale paths). - Polish the two linked guides accordingly: the testing guide gains the same "README has the pitch; this is the full guide" up-link + a sibling link to the lint guide; the lint guide drops the "Pillar 1" opening (consistent with the README dropping "pillar") and gains the sibling link to the testing guide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: add npm repository/homepage/bugs/keywords/license metadata The package is live on npm (vigiles@5.0.0) but package.json lacked discovery metadata: no repository (so npmjs.com had no link back to GitHub), no homepage, bugs, keywords, or license, and a stale description (the old "compile .spec.ts" framing). Add them and update the description to the current "lint & test the harness" positioning so npm search and the npm↔GitHub link are correct. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: scrub "pillar" from the docs and CLAUDE.md spec (use "layer") Following the README, drop the "pillar" jargon from the user-facing docs and the CLAUDE.md positioning/rules — "layer" throughout (the linting layer / the testing layer), matching the front-door voice. Recompiled CLAUDE.md; anchors and cross-links updated consistently. research/ left as-is (internal design vocab). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: regenerate generated.d.ts (sync npm scripts + project files) Regenerated after the package.json metadata change; also picks up pre-existing drift (api:report/api:check/docs:api scripts, vendor-coverage.test.ts) the committed file was missing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: remove the deprecated demos (examples/demo + plugin-test-demo) The two demos were deprecated and no longer the surfaced front door. Delete them and every reference: the demo/demo:plugin npm scripts, the doc callout in the harness-testing guide, the examples/harness README block (now points at npx vigiles test/eval), and the .gitignore/.prettierignore lines. Regenerated generated.d.ts (scripts + project files). The roadmap item is updated to "build the ONE polished front-door demo" (the deletion is done; the polished demo + GIF remains as the real next step). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(roadmap): record Sogen evaluated-and-rejected as a sandbox backend Note in the rejected/parked section so it isn't relitigated: Sogen is a syscall-level Windows/Linux binary emulator for malware/DRM research (host isolation "might not be perfect") — wrong workload/guarantee/gap for confining Node+shell. The transferable idea (snapshot + deterministic replay) we already do at the filesystem/trace layer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
2c64c62b32 |
feat!: fix the init/install flow (marketplace plugin, devDep, real spec, version) (#33)
* feat!: fix the init/install flow (marketplace plugin, devDep, real spec, version) Rework `vigiles init` so a fresh install actually works and stops polluting the consumer repo. Each item below was a confirmed bug: - Plugin via marketplace, not vendoring (#1/#7): drop `npx skills add` (which vendored .agents/skills + .claude/skills symlinks + skills-lock.json into the working tree). `init` now installs the Claude Code plugin through the marketplace — `claude plugin install vigiles@vigiles` when the CLI is on PATH, else prints the two `/plugin` slash commands — globally into ~/.claude/plugins/, nothing written to the repo. Internal dev-only skills (generate-logo, pr-to-lint-rule, enforce-rules-format, audit-feedback-loop) move to dev/skills/ (a contributor-only `vigiles-dev` plugin) so consumers get only the consumer skills (test-harness, migrate-to-spec, strengthen, edit-spec, generate-rule, linter-docs). - Verify pillar really sets up (#2): scaffold a spec for every instruction file instead of only printing "migrate". An existing hand-written file is never compiled over (would clobber it) — it gets a spec + a migrate-to-spec next step. - Dependency management (#3/#4): add/upgrade `vigiles` in devDependencies (moving it out of dependencies / off a stale `github:` pin) so the scaffolded harness resolves `vigiles/testing`. - Stale CI workflow (#5): detect an old bare-`npx vigiles` workflow and warn loudly with the fix instead of silently saying "already exists". - Honest commit hint (#6): list only files actually written this run. - `--version` (#8): print the version number, not the help banner. - Action vs npm versioning (#9): document that the Action tag (@v1) and the npm CLI version (3.x) are separate lines; align the plugin version to 3.0.0. Also replaces the awkward `--pillars=verify|test|both` with `--verify` / `--testing` flags (deprecated `--pillars` kept as an alias) and adds an optional `--harness=claude,codex` override on top of auto-detection. Per the user's direction, no legacy git-commit upgrade-path detection (#10) — there are no existing external users. BREAKING CHANGE: the Claude Code plugin now installs via the marketplace (`/plugin marketplace add zernie/vigiles` + `/plugin install vigiles@vigiles`) instead of `npx skills add`, and ships only consumer-facing skills — the four vigiles-development skills are no longer installed for consumers. The `--pillars` flag is superseded by `--verify`/`--testing` (still accepted as an alias). https://claude.ai/code/session_01AV4WainLabYw85s6KPjGdD * feat(init): per-harness install decision + deterministic install smoke tests Make the plugin/skill install a pure, per-harness decision (planPluginInstall) and prove the whole install outcome with a deterministic CI smoke test — the gap that let the v3 install bugs ship unnoticed. - planPluginInstall(harnesses, { hasClaude }) is a pure function returning the per-harness install plan, so a test asserts WHICH commands run with no network and no real claude/codex binary: - Claude → plugin marketplace (claude plugin install → ~/.claude/plugins/) - Codex → cross-agent `skills` CLI with -g (npx skills add zernie/vigiles -a codex -g → ~/.codex/skills/) BOTH install globally — neither vendors into the repo (the issue #1 fix holds for both harnesses). Codex hooks (.codex/config.toml [hooks]) are not auto-wired yet; the plan says so out loud. - Harness-aware spec targets: a Codex-only project scaffolds AGENTS.md (not CLAUDE.md); claude+codex scaffolds both. - Deterministic installation smoke tests: run `init` against realistic todo fixtures (Claude / Codex / both) and assert the full outcome AND the negatives — spec scaffolded, CLAUDE.md preserved, dep moved to devDeps, and nothing vendored (.agents/skills, .claude/skills, skills-lock.json absent). This is the regression guard for the class of bugs that shipped before. Why they slipped through: every prior init test used --no-plugin on an empty synthetic dir and asserted single files; nothing ran a full install on a real project or asserted the no-vendoring / dep / preserve-content invariants. https://claude.ai/code/session_01AV4WainLabYw85s6KPjGdD * fix(init): collapse a CLAUDE.md⇄AGENTS.md mirror to one spec If CLAUDE.md and AGENTS.md are the same artifact — a symlink (`ln -s CLAUDE.md AGENTS.md`) or kept byte-identical by rulesync/Ruler — then scaffolding a spec for each means two specs fighting over one file and a guaranteed integrity-hash collision (the "Compose With Sync Tools" rule). `init` now runs the existing detectInstructionMirror() and collapses the pair to a single canonical spec (the real file for a symlink; CLAUDE.md for synced content, which the sync tool fans out to AGENTS.md), printing why. An explicit --target is still honoured as-is. Covered by two deterministic smoke tests: a symlinked AGENTS.md and a byte-identical (synced) pair each produce ONE spec, never a second. https://claude.ai/code/session_01AV4WainLabYw85s6KPjGdD * feat(init): compile into a sync tool's source slot + real Codex install + gated e2e Two follow-ups from review, plus a correction the live test surfaced. Sync-tool redirect (was: warn-only): - When rulesync/Ruler is detected, `init` now REDIRECTS the scaffolded spec into the tool's source slot (`.ruler/AGENTS.md.spec.ts` → compiles `.ruler/AGENTS.md`) via composeCollisions, instead of scaffolding a root CLAUDE.md spec that the tool would overwrite (staling the integrity hash). vigiles compiles upstream; the tool distributes. "Compose, don't fight." - findSpecs now uses `dot: true` (ignoring .git) so a spec in a dot-dir source slot is discovered by compile/audit/the recompile hook. - init() derives the spec's `target` field from the basename, so a spec in a subdir/slot validates and compiles correctly (this was already broken for any `--target=sub/dir/FILE.md`). Codex install corrected against the REAL `skills` CLI: - The command is `npx skills add zernie/vigiles -a codex -g -y` — needs `-y` (non-interactive) and installs to the global store `~/.agents/skills/`, NOT the documented `~/.codex/skills/`. Fixed the plan, messages, and docs. - New gated e2e test (src/cli-install.e2e.test.ts, network tier) runs the real install into an isolated $HOME and asserts it lands globally, never in the repo. Self-skips where npx/network is unavailable. This is what caught the wrong path. Smoke tests updated: a Ruler project redirects into .ruler/ (no root spec). https://claude.ai/code/session_01AV4WainLabYw85s6KPjGdD * feat!: consolidate the pillar vocabulary to Lint + Test The two pillars were referred to inconsistently (verify / audit / linting / pillar 1 vs testing / test / pillar 2). Consolidate on two names used consistently across flags, commands, the Action, and docs: - Lint pillar — verify instruction-file references. - flag `--lint` (/`--no-lint`); command `vigiles lint` (alias of `audit`); export `vigiles/linting`. The GitHub Action's default command is now `lint`. - Test pillar — test the harness. - flag `--test` (/`--no-test`); commands `vigiles test`/`eval`; export `vigiles/testing`. Backwards-compatible: `--verify` / `--testing` / `--pillars=both|verify|test` still parse as aliases, and `vigiles audit` still runs. The internal SetupPlan field `verify` is renamed to `lint`. Docs (README, CLI ref, agent guides), the scaffolded CI workflow's job name, and the interactive prompt are all updated to the Lint/Test vocabulary. BREAKING CHANGE: the init pillar flags are now `--lint` / `--test` (the old `--verify` / `--testing` remain as deprecated aliases), the canonical references-verification command is `vigiles lint` (`audit` kept as an alias), and the GitHub Action's default `command` input changed from `audit` to `lint`. https://claude.ai/code/session_01AV4WainLabYw85s6KPjGdD * feat!: drop the deprecated aliases (clean break) Following the Lint+Test consolidation, remove the back-compat shims now that this is a major release: - `vigiles audit` command removed — `vigiles lint` is the only name. - `--verify` / `--testing` init flags removed — use `--lint` / `--test`. - `--pillars=both|verify|test` removed. `vigiles audit` now reports "Unknown command"; the old flags are ignored. The stale-workflow detector recognises `vigiles lint` (not `audit`) as the modern API, so a workflow still running `vigiles audit` is flagged for update. Tests, docs, the Action input description, and the scaffolded workflow are updated. BREAKING CHANGE: the `audit` command and the `--verify` / `--testing` / `--pillars` init flags are removed. Use `vigiles lint` and `--lint` / `--test`. https://claude.ai/code/session_01AV4WainLabYw85s6KPjGdD --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
cf40a0f96c |
docs: clarify pillar decoupling and surface install in harness section (#32)
* docs: clarify pillar decoupling and surface install in harness section
State the two-pillar decoupling plainly (neither pillar gates the other)
instead of framing it by which file each pillar doesn't need. Add a
concrete install command to the "Test your harness" setup block so it
anchors to `npx skills add zernie/vigiles` / `vigiles/testing` instead of
a vague "Install vigiles" prompt.
* chore: gitignore locally-installed dev skills
Ignore .agents/, .claude/skills/, and skills-lock.json so locally
installed skills (e.g. obra/superpowers) used for internal development
stay out of the repo and don't show as uncommitted changes.
* chore: vendor obra/superpowers skills for internal dev use
Add the superpowers skills under .agents/skills/ (with .claude/skills/
symlinks) and skills-lock.json for internal development. Verified via
`npm pack --dry-run` that none of these paths ship in the npm package or
the Claude Code plugin (.claude-plugin/plugin.json ships only ./skills/),
so they're part of the repo but not part of what vigiles ships.
Reverts the prior .gitignore exclusion of these paths.
* chore: enable superpowers via plugin marketplace instead of vendoring files
Replace the vendored obra/superpowers skills (.agents/, .claude/skills/,
skills-lock.json) with a project-scoped plugin declaration in
.claude/settings.json (extraKnownMarketplaces + enabledPlugins). Claude
Code fetches the plugin into its ephemeral cache on startup, so the repo
carries the enablement config, not 60+ third-party files.
* docs: add production-grade GHA + CLI as a tool requirement
Add the "prod-grade-gha-cli" guidance rule to the spec (compiled into
CLAUDE.md): the CLI and GitHub Action are first-class, production-grade
surfaces — CLI is the single source of truth with every Action input
mapped to a real flag, the Action is a composite over the published CLI
that works as uses: zernie/vigiles@v1, declares outputs via
$GITHUB_OUTPUT, ships a full documented workflow, and is dogfooded in CI.
* feat!: production-grade GitHub Action + CLI flag parity
Make the GitHub Action and CLI first-class, production-grade surfaces
(satisfies the new prod-grade-gha-cli rule).
CLI:
- Add --max-rules=<n> and --catalog-only flags (src/cli-flags.ts,
applyConfigFlags) so every Action input maps to a real CLI flag instead
of a config-file-only knob. Pure + unit-tested.
GitHub Action:
- Rewrite action.yml as a composite action over the published `npx
vigiles` CLI. The old node20 entry pointed at dist/action.js, which is
gitignored and never committed, so `uses: zernie/vigiles@<ref>` could
never actually run — only npm did. The composite reuses the same tested,
published artifact.
- Declare the `valid` output and set it via $GITHUB_OUTPUT (drops the
deprecated ::set-output). Add `version` (incl. `local` for dogfood) and
`working-directory` inputs.
- Remove the now-dead src/action.ts entry.
CI / release:
- Dogfood the Action in ci.yml via `uses: ./` with version: local and
assert the `valid` output.
- release.yml maintains a floating `v1` major tag so `uses:
zernie/vigiles@v1` tracks the latest release.
Docs/tests:
- Full Action reference in docs/cli.md (copy-paste workflow, permissions,
inputs, output, @v1-vs-@main versioning) + README pointer.
- Structural test (src/action-yml.test.ts) locks in the composite/outputs/
no-set-output contract.
BREAKING CHANGE: the GitHub Action is now a composite action and no longer
ships dist/action.js. Inputs/outputs are unchanged in name where they
overlap (command, paths, max-rules, catalog-only) but the action now adds
`version`/`working-directory` inputs and a `valid` output. Pin to
`zernie/vigiles@v1` going forward.
* feat: PR comments + job summary output, JS/TS harness scripts
GitHub Action output:
- Post an automatic STICKY PR comment on pull_request events (found by a
hidden marker and updated in place, never duplicated) + always write a
job summary ($GITHUB_STEP_SUMMARY), alongside the existing inline
annotations. New `comment` / `github-token` inputs; best-effort so a
fork PR without write access degrades to a warning. Dogfooded in ci.yml
(check job granted pull-requests: write).
Dual-language harness/eval scripts:
- `vigiles test` / `vigiles eval` now discover and run scripts authored in
JavaScript (.mjs/.cjs/.js) OR TypeScript (.ts/.mts/.cts). A TS script
runs through tsx when installed, else Node's native type stripping
(>=22.6), with an actionable error otherwise (src/adapters/claude-code/
run-scripts.ts: interpreterArgs/detectNodeCaps/scriptGlob).
Requirements + docs:
- Extend the prod-grade-gha-cli rule (three output channels) and add the
dual-language-tests rule to the spec (compiled into CLAUDE.md).
- Document the comment behavior + pull-requests: write in docs/cli.md;
rewrite the JS-vs-TS section of docs/testing-matrix.md.
Our own unit suite remains 100% TypeScript (every src/**/*.test.ts);
examples/harness/*.mjs stay JS on purpose as zero-dep CLI demos.
* docs: slim README to a billboard landing page
Cut the README from ~207 to ~118 lines, moving it out of the "long but
neither pitch nor index" middle into a focused landing page (Knip/esbuild
shape), per a survey of dev-tool READMEs (Ruff/Biome/Vitest/ast-grep/…)
and standard-readme/Diátaxis guidance.
- Pitch once, up top: tagline + one "why" paragraph + the single
two-pillar table (was restated 4x across header/nav/table/TOC).
- Keep the two concrete "first taste" artifacts: the stale-CLAUDE.md
snippet and the runHook example.
- Collapse the CLI, Skills, and Related Tools sections + the duplicated
promptfoo rebuttal into one-line links (the detail already lives in
docs/). Delete the <details> table of contents.
- Drop the README section anchor from docs/verifying-instruction-files.md
(now points at the README top).
Verified: `vigiles audit` passes with zero orphan docs (every doc the
README no longer links is still indexed by docs/README.md or CLAUDE.md).
* docs: document how the test tiers run in CI
Capture the CI tier mapping in docs/testing-matrix.md (it previously
lived only in discussion): which tier runs in which ci.yml job, that CI
runs every tier except evals, that evals are manual ($/non-deterministic),
and the cheapest-first best practice + the per-tier test:unit/integration/
e2e scripts that enable the split.
* docs: recast both README pillars as Without/With with real output
Replace the prose-heavy pillar pitches with tight "Without vigiles / With
vigiles" comparisons that show ACTUAL vigiles output (captured from real
runs, not invented):
- Pillar ①: `vigiles audit` flagging a rule that "exists but is disabled
in eslint config" (the valid-≠-true differentiator), a passing rule, and
a typo with a did-you-mean suggestion.
- Pillar ②: the runHook assertion + a real `vigiles test` pass summary,
framed around a silently-broken --no-verify guard.
Shorter and more concrete; shows the payoff instead of describing it.
* feat: refs-hook nudges the agent to mark references on save
Close the authoring-time half of pillar 1: an automatic, in-loop nudge so
the agent expresses references as verifiable vigiles marks instead of
unverifiable prose. Ships in the plugin — end-users author nothing.
- New `unmarked-refs` rule (default "warn"). The PostToolUse refs-hook
(hooks/refs-nudge.sh → `vigiles refs-hook`) fires on every CLAUDE.md /
AGENTS.md / SKILL.md edit and, by severity: "warn" → injects a
non-blocking nudge into the agent's context (mark these refs); "error" →
blocks the edit (exit 2); false → off. Wired into .claude-plugin/
plugin.json so it's on by default after `vigiles init`.
- Refactor the detection into core: collectRefIssues (shared by the
`vigiles refs` CLI and the hook) + the pure refsHookAction severity map,
unit-tested.
- Honest scope: catches code-shaped backticked refs; plaintext prose is
undecidable (the floor). Hooks are Claude Code / Codex only; other
harnesses fall back to instructions + the `vigiles audit` CI backstop.
- Docs: the from-file-save flow in verifying-instruction-files.md, a new
rules/unmarked-refs.md, and the rule added to the cli.md / docs index.
* test: dogfood the refs-hook through the runHook tier
skills-dogfood.test.ts only proves the hook script exists; this drives the
actual built `vigiles refs-hook` via the repo's own runHook unit tier and
asserts it FIRES correctly — nudges (non-blocking, additionalContext) by
default, blocks (exit 2) under unmarked-refs:"error", stays silent when
off, and no-ops on a clean or non-instruction file. Closes the "test your
harness, don't trust it on vibes" gap for the new hook.
* fix: scope refs-hook nudge to linter-rule names (kill false positives)
Dogfooding the hook on the repo's own SKILL.md files exposed heavy
over-firing: skills/test-harness/SKILL.md got 23 nudges, all on API names
(`runHook`, `runEval`, …) that are documentation prose, not stale
references. The `isCodeShaped` heuristic flagged every backticked
camelCase/Pascal/SCREAMING identifier — far too noisy to ship at
warn-default.
Tighten to high-signal only: flag a slash-scoped name with NO file
extension (`eslint/no-console`, `@typescript-eslint/no-explicit-any`,
`boundaries/dependencies`) and nudge toward `enforce()` / inline
`<!-- vigiles:enforce -->`. Bare identifiers and file paths are no longer
flagged (paths re-introduce the noise — the same SKILL.md files cite
`docs/harness-testing.md`, `package.json`, …; symbol marking stays opt-in
via an explicit `vigiles:symbol`).
Re-dogfooded: test-harness 23→0, strengthen 2→1 (keeps the one real rule
`@typescript-eslint/no-unused-vars`), migrate-to-spec 0. Tests + docs
updated to the narrowed scope.
* test: real-claude dogfood proves the refs-hook fires in a session
Install-and-verify: with the claude CLI present, drive the deterministic
harness tier (real claude + scripted mock model, no API key) so the mock
Writes a CLAUDE.md naming `eslint/no-console`, the PostToolUse refs-hook
fires, and requestContains asserts the non-blocking nudge reached the
model's context. Closes the last gap — previously verified only through
the runHook unit tier. examples/harness/refs-nudge.harness.mjs runs in CI
via `npm run test:harness` (claude is installed there) and skips cleanly
without the binary.
* feat: vigiles init sets up both pillars, interactive-or-not, via the v1 Action
Make onboarding smooth and complete (the smooth-adoption rule):
- BOTH pillars by default. Pillar 1 (spec + types + compile) as before, PLUS
Pillar 2 — scaffold a starter vigiles.harness.mjs the user adapts to a
real hook. `--pillars=verify|test|both` scopes it.
- Interactive when a human's at a TTY (prompts: pillars / CI / plugin),
NON-INTERACTIVE for agents / CI / piped input or `--yes` — so "set up
vigiles" from a Claude Code / Codex prompt Just Works without hanging.
Pure plan logic in src/setup-plan.ts (parseSetupArgs/shouldPrompt/
resolvePlan), unit-tested; IO stays in cli.ts.
- CI wiring now creates a real `.github/workflows/vigiles.yml` using the
production `zernie/vigiles@v1` Action (verify job + a harness job when
Pillar 2 is on), instead of appending a raw `npx vigiles audit` line to
an existing workflow.
- New flags: --yes/-y, --pillars=, --no-plugin. Docs updated (cli.md init
reference, agent-setup.md agent flow, README quick start).
* test: CI-test the init command's new behaviors end-to-end
Spawn the real CLI in a temp project and assert the new init behaviors,
offline via --no-plugin (no network skills-add): default sets up both
pillars + a zernie/vigiles@v1 workflow (asserts the spec, vigiles.harness.mjs,
and the workflow YAML contents); --pillars=test scaffolds only the harness
(no spec/workflow); --pillars=verify only the spec. Also add --no-plugin to
the existing bare-init spawns so they stay offline and deterministic.
* docs: make the "agent installs + tests skills" flow first-class
Viewed from an agent handed "install vigiles and test my skills", the
README install path was clear but the test-skills path was muddy — the
only pillar-2 example was a hook, measureTriggerRate (the "does the skill
fire?" primitive) was never named, and the model-invocable test-harness
skill was buried as 1-of-8.
- README: name measureTriggerRate (recall + precision) in the tier line,
and surface that `init` installs the test-harness skill so "test my
skills" fires it.
- Add the `great-agent-flow` guidance rule: the agent-told-a-short-prompt
experience must be discoverable from the README front door and
frictionless (one non-interactive install, the skill fires the test,
the skill-testing path is high-signal not buried).
* fix: no blanket claude-gate; skips are loud, never a silent green
Two fixes for `vigiles test` / `vigiles eval`, behind a stated principle
(the new no-silent-skips rule):
1. Drop the blanket `claude`-gate in handleRunScripts. Unit-tier runHook
tests need no model, so they now ALWAYS run — previously the whole
command bailed if `claude` was absent, so a scaffolded vigiles.harness.mjs
+ `npx vigiles test` looked broken on a machine without claude.
2. Make skips LOUD. The runner classifies each script pass/skip/fail
(exit 0 / SKIP_EXIT_CODE 77 / else), tallies them separately ("1 passed,
1 skipped"), shows `⊘ SKIPPED`, and a skip never fails the run (anyFailed).
Add a `skip(reason)` helper (vigiles/testing) and convert the four
deterministic examples from `console.log(...); process.exit(0)` (a silent
fake ✓) to `skip()`. Up front, `vigiles test` notes when claude is absent.
Verified end-to-end with claude off PATH: the unit example runs+passes, the
deterministic example reports ⊘ SKIPPED, summary "1 passed, 1 skipped",
exit 0. Adds the no-silent-skips guidance rule + skip-aware runner tests.
* feat: vigiles test --no-skip (fail on skip) + README agent prompt
Answer "shouldn't a skip fail loudly?" — yes, where you assert the
capability is present. A skip passes by default (tiers need different
capabilities not present everywhere; failing-by-default would make the
command red in any partial env — this repo's harness job legitimately
skips the egress tier), but `vigiles test --no-skip` makes any skip a
failure for the CI job that asserts everything must run: a green-with-skips
is itself untested surface. The skip stays loud + tallied either way.
Extends the no-silent-skips rule; integration-tested.
Also (per the agent-flow ask): add a paste-into-Claude-Code/Codex prompt to
the README quick start — install + verify + write/run a harness test with
good defaults, asking about CI gating / real-model eval / --strict.
* fix: cover skip() under the 100% gate; flush skip/--no-skip docs
The no-silent-skips commit broke CI's coverage gate (step 13): skip() in
harness-assert.ts (a coverage-included file) calls process.exit, which is
unreachable in-process → dropped below 100%. Mark it with the repo's
`v8 ignore` convention (same as the spawn boundary); it's exercised via a
child process in cli.test.ts / run-scripts.test.ts. Verified locally with
`npm run coverage`: 100% stmts/funcs/lines, branches 92.43% (≥90 floor).
Docs flush (before merge): document the loud-skip / unit-runs-without-claude
/ `--no-skip` behavior in docs/testing-matrix.md and the test-harness skill's
"Run it" step, so the agent's authoring guide matches the runner.
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
7f2e8a1629 |
Add self-evolving spec system: proof algorithms + evolution engine (#16)
* Add self-evolving spec system: proof algorithms + evolution engine
Six deterministic proof algorithms (monotonicity lattice, NCD similarity,
Bloom filter, fixed-point convergence, Merkle DAG history, property-based
testing) and an evolution engine that gates AI-proposed spec mutations
through the full proof suite before accepting them.
- src/proofs.ts: core algorithms (no external dependencies, Node built-ins only)
- src/evolve.ts: mutation operators, fitness function, selection engine
- src/proofs.test.ts: comprehensive test suite
- research/self-evolving-specs.md: design document with architecture + theory
* Add code search research: grep vs embeddings vs AST-based approaches
Compares ripgrep, ast-grep, embeddings (Cursor/Augment), tree-sitter
graphs (Aider repo-map), LSP, and code graph databases. Includes
SWE-bench evidence, performance benchmarks, and the emerging industry
consensus on layered search.
* Add code clone detection: Zhang-Shasha tree edit distance + Bloom pre-filter
WIP — separate from NCD rule dedup. This detects structural code clones
(Type-1/2/3) via tree edit distance on token-based ASTs. Could power
future check() assertions for codebase-wide duplicate detection.
* Drop check()/every() and similarity.ts, simplify to enforce/guidance
vigiles owns spec compilation and linter cross-referencing, not
architectural linting. Filesystem assertions (check/every) and code
clone detection (similarity.ts) belong in external tools like
ast-grep, Steiger, or jscpd — reference them via enforce().
- Remove CheckRule, FilePairingAssertion, check(), every() from spec.ts
- Remove executeAssertion/executeChecks from compile.ts
- Remove similarity.ts and similarity.test.ts entirely
- Simplify evolve.ts mutations to guidance↔enforce only
- Update monotonicity lattice: guidance(0) < enforce(1)
- Update CLAUDE.md.spec.ts with new architecture + key files
- Add proofs.test.js to npm test script
- All 224 tests pass (4 pre-existing pylint/rubocop failures)
* Unify CLI to 3 commands: init, compile, audit
Aggressive simplification — no backward compat since we have no users.
- init = old setup (auto-detect, scaffold/migrate, generate-types, compile, CI wiring)
- compile = spec → markdown (unchanged)
- audit = verify hashes + discover coverage gaps + strengthen suggestions
Removed entirely:
- adopt (the hash already detects manual edits; "edit the spec")
- migrate (init auto-detects existing CLAUDE.md and handles it)
- check, discover, strengthen, setup as standalone commands
generate-types stays as plumbing for CI freshness checks.
Updated:
- src/cli.ts: new dispatch, new audit function, removed dead code
- src/action.ts: GHA defaults to "audit" instead of "check"
- src/cli.test.ts: renamed test blocks, updated command calls, fixed E2E ordering
- docs/agent-workflows.md, agent-setup.md, spec-format.md: all command refs
- CLAUDE.md.spec.ts: new cli.ts description
Tests: 253 pass / 4 pre-existing pylint/rubocop failures
* audit: wire findSimilarRules for NCD-based duplicate detection
First proof algorithm wired into the CLI. audit now runs
findSimilarRules() from src/proofs.ts on each spec independently,
catching near-duplicate rules (NCD < 0.3) that bloat the spec.
- Only scans within a single spec (cross-spec duplicates are legit)
- Reports top 5 pairs per spec with distance scores
- New test: creates a spec with two paraphrased "use logger" rules
and verifies audit catches them
- Information-theoretic (gzip-based), no LLM, fully deterministic
Tests: 254 pass / 4 pre-existing pylint/rubocop failures
* Add research: agent integration and FP techniques for deterministic AI
Two research docs gathering 2025-2026 findings on where coding agents
fail and what deterministic tooling can provably catch. Each doc ends
with 10 concrete vigiles improvements scoped to the compiler + linter
cross-reference surface (no architectural linting).
* Add research: FP for the agent harness (railway for skills)
Separate doc aimed at applying railway / effect patterns to Claude
Code itself — skills as Result pipelines, hooks as effect handlers,
sessions as event logs, settings via lenses — rather than to the
code agents produce. 10 ideas, priority-ordered.
* Fix CI: rename check→audit, clean lint errors, regenerate types
- ci.yml check job now runs `vigiles audit` (check was renamed)
- src/cli.ts: drop unused async on check() (no await inside)
- src/evolve.ts: replace dynamic delete with rest-destructure (no-dynamic-delete)
- src/proofs.test.ts: drop inferable <number> type args, replace `as any`
with typed ClaudeSpec factory
- .vigiles/generated.d.ts: include evolve/proofs files missed by previous commits
* Harden evolution engine against malformed mutations and history tampering
Two issues flagged in PR review:
- merge with duplicate sourceIds would bypass merge semantics and turn
into a rename. Reject at applyMutation with a clear error. Test added.
- getHistory() exposed the mutable MerkleHistory instance, letting
callers append unvalidated entries and corrupt the audit trail.
Introduce a ReadonlyMerkleHistory interface (length, head, getNodes,
verify, toJSON) and return that from getHistory() instead.
* Clone mutation payloads and proof receipts before storing engine state
Two further reference-sharing bugs flagged in PR review:
- applyMutation's add/merge cases stored mutation.rule / mutation.mergedRule
by reference, so a caller mutating the original mutation object after
acceptance could silently alter engine state without re-running proofs.
Clone via new cloneRule() helper before assigning.
- propose() passed proofResult.receipts directly into history.append while
also returning proofResult to the caller, meaning later caller-side
mutation of result.proofs.receipts would retroactively edit stored
history nodes. Shallow-clone each receipt before appending.
Tests cover both isolation properties.
* Close remaining reference leaks and fix stale migrate hint
Three review comments addressed:
- EvolutionEngine constructor: shallow-spread of initialRules still
shared Rule references with the caller. Deep-clone each rule via
cloneRule() so post-construction mutation of initialRules can't
alter engine state.
- MerkleHistory.getNodes(): returned a fresh array but reused live
HistoryNode references, so callers could mutate specHash/mutation/
proofs on appended entries. Deep-clone each node (including
mutation.ruleIds and every proof receipt).
- validate.ts require-spec hint: told users to run `vigiles migrate`,
a command that no longer exists. Point at `vigiles init --target=<path>`
which is the actual remediation path now.
* Harden Merkle history head() and constructor provenance checks
Three more review comments addressed:
- MerkleHistory.head() returned the live HistoryNode; callers with a
ReadonlyMerkleHistory handle could still mutate specHash/mutation/
proofs on the latest entry. Deep-clone symmetrically with getNodes().
- EvolutionEngine constructor accepted options.history as-is, never
verifying the chain or that its head matched initialRules. A stale,
tampered, or mismatched history would produce misleading provenance
for new mutations. Now verify() the chain on construction and throw
on failure; also compare head.specHash against the rules' spec hash
and throw on mismatch.
- cli.ts header comment still listed dropped commands (check, discover,
adopt) — updated to reflect the actual three primary commands plus
generate-types. (Declining to restore adopt itself per explicit
no-backwards-compat direction.)
Tests: head() defensive copy, tampered-chain rejection, mismatched-
head rejection.
* audit: add --summary/--json/exit codes, wire maxTokens, fix hash/init routing
Ships most of the practical-ideas batch:
- audit --summary: single-line output for SessionStart hook injection
("vigiles: 3 stale / 2 duplicates / 5 undocumented rules")
- audit --json: structured AuditReport for CI integration
- Exit codes: 0 clean, 1 warnings (duplicates), 2 hard errors (stale hash)
- GitHub Actions annotations: ghAnnotate() helper + aggregate error/warning
annotations when GITHUB_ACTIONS=true
- maxTokens per-spec field (mirrors maxSectionLines pattern), wired through
compile.ts so `spec.maxTokens ?? options.maxTokens` fails compile on budget
excess. maxSectionLines was already enforced — maxTokens is the missing
hard cap the ETH Zurich ~2000 token signal maps to
- init template cleanup: drop imports of removed check/every and the
strengthen-command reference that no longer exists
- init dispatcher: --target bypasses the setup wizard and goes straight to
the plain scaffolder, so `vigiles init --target=<file>` is a reliable
remediation for require-spec (fixes the broken path the validator hint
now points at)
- Closest-match suggestions for unknown linter rules via Levenshtein
(short-string edit distance is more appropriate than NCD for typos):
"Rule "no-consol" not found in eslint. Did you mean: "eslint/no-console"?"
- proofs.ts: removals now fail monotonicity unless explicitly allowlisted
(prevents pure `remove` mutations from passing the proof suite on neutral
fitness and silently deleting constraints)
Sub-functions refactored to accept silent? and return counts so audit can
assemble the AuditReport without double-printing.
* Unblock merge mutations and harden similarity check against bad data
Two review comments addressed:
- The prior "removals fail monotonicity" fix had an ironic side effect:
every merge mutation also fails monotonicity because merges remove both
source rules by design. Fix by building a per-call allowWeaken set in
propose() that adds the current mutation's sourceIds when the mutation
type is merge, so valid merges pass the proof suite without requiring
callers to pre-seed the engine allowWeaken.
- ruleToText() in proofs.ts was exhaustive at the TS type level but had no
runtime fallback. A legacy _kind:"check" object or a JS caller would
return undefined, which then crashed NCD / compressedSize with an opaque
error. Add an explicit default branch that throws a structured error,
and wrap findSimilarRules() in runProofSuite's try/catch so the failure
surfaces as a clean "ncd-dedup failed" proof receipt instead of crashing
the whole audit.
Tests: merge round-trip with default options, unknown-kind rejection.
* Guard bloom+fitness against bad rule kinds; reject weakening merges
Two more review comments addressed:
- runProofSuite's NCD try/catch was incomplete: bloom-overlap and fitness
also call ruleToText (via ruleToBloomFilter / findSimilarRules) and
would throw the same "Unknown rule kind" error on legacy data, still
crashing audit/propose. Wrap all three phases so a malformed rule
surfaces as three clean proof-receipt failures and fitness falls back
to a neutral value, instead of propagating a throw.
- The merge path in applyMutation now verifies that the merged rule is
at least as strong as its strongest source. Without this check, the
per-call allowWeaken trick (added so normal merges pass monotonicity)
let callers merge two enforce() rules into a guidance() rule and pass
the proof suite on neutral/improved fitness — a silent enforcement
downgrade. Merges that would weaken are now rejected before they
reach the proof suite.
Tests: weakening-merge rejection, fitness-throws structured-failure path.
* Clone mutation/proofs payloads inside MerkleHistory.append
Symmetric to the getNodes()/head() read-side clones. The previous fix
protected callers of read APIs from mutating history, but the write
side was still sharing references: `append(specHash, mutation, proofs)`
stored the caller-provided objects as-is, so a caller that later edited
the same mutation object (e.g. to reuse it for the next append) would
retroactively alter the already-stored node. That breaks the
append-only / tamper-evident contract in normal usage patterns and can
make verify() fail unexpectedly later.
Shallow-clone mutation (including ruleIds) and each proof receipt on
write. Test: append, tamper with the caller's mutation + proofs
objects, assert the stored node is unchanged and verify() still passes.
* Close remaining PR review comments: acceptNeutral, getRules, mergedId, compileRule
Four active bugs from the review thread backlog:
- acceptNeutral was short-circuiting the entire fitness check, so any
proof-passing mutation was accepted regardless of score — silently
letting spec quality regress. Now relaxes strict `>` to `>=` only.
- applyMutation merge path did not check whether mergedId collided with
an unrelated existing rule. Merging two rules into an existing third
rule would silently overwrite and drop its constraints. Reject the
collision unless mergedId matches one of the sources (rename in place).
- EvolutionEngine.getRules() was shallow-copying the map but returning
live Rule references — the same reference-leak class fixed everywhere
else. Deep-clone each rule on read.
- compile.ts compileRule() switch had no default branch, so a runtime
Rule with an unknown _kind (legacy compiled JS artifact, JS caller,
cast bypass) returned undefined and the rule was silently dropped
from the compiled output with zero error. Fail fast with a structured
error naming the unknown kind.
Tests: regression-rejection under acceptNeutral, getRules deep clone,
mergedId collision rejection, mergedId rename-in-place allowed, and
compileClaude throwing on unknown kinds. Also updated the preexisting
"proposeAll continues on reject" test to use a strengthen that actually
improves coverage (the old test was implicitly relying on the buggy
short-circuit).
* Scope audit duplicate detection to requested files; clone allowWeaken Set
Two more review comments addressed:
- vigiles audit <file> was running findDuplicateRules against all specs
in the repo regardless of the paths passed in. Targeted CI checks
would fail on unrelated duplicates elsewhere. Now findDuplicateRules
accepts an optional scopeFiles parameter, and audit only passes it
when restArgs is non-empty (so bare `vigiles audit` still scans
everything). For each scope file, only `<file>.spec.ts` is scanned.
- EvolutionEngine stored options.allowWeaken by reference, so a caller
mutating their own Set after construction could silently change
acceptance policy and let a weaken mutation pass that would otherwise
be rejected. Clone via `new Set(options.allowWeaken ?? [])` on construction.
Test verifies that adding a rule id to the caller's Set post-construction
does not affect the engine's decision.
* Normalize audit scope paths before matching spec files
Targeted audit (e.g. `vigiles audit ./CLAUDE.md` or an absolute path)
was comparing `specPath === \`${f}.spec.ts\`` verbatim, so
common equivalent path forms silently skipped the intended spec and
reported zero duplicates. Resolve both sides to absolute paths via
`resolve(process.cwd(), ...)` before matching so "CLAUDE.md",
"./CLAUDE.md", and "/abs/CLAUDE.md" all hit the same spec.
* Canonicalize rules hash and guard baseline fitness in propose
Two more review comments addressed:
- computeHash(JSON.stringify(this.rules)) is insertion-order dependent,
so two logically identical rule maps built in different orders would
hash differently and falsely trigger the "head does not match
initialRules" check in the constructor. Replace with a new
canonicalRulesJson() helper that sorts rule ids and sorts keys
within each rule before stringifying. Applied to all three callsites
(constructor check, constructor genesis append, propose() append).
Test verifies reordered rules are accepted against the same history.
- propose() called this.getFitness() BEFORE the guarded runProofSuite,
so a legacy/malformed rule in engine state would throw from
ruleToText and crash the mutation flow instead of returning a
structured rejection. Wrap the baseline fitness call in try/catch;
on failure, return an EvolutionResult with accepted=false, a
baseline-fitness proof receipt, neutral FitnessResult, and the error
message. Test uses an internal cast to inject a _kind:"check" rule
and verifies the rejection.
* action: delegate audit to the CLI for full flow parity
Previously the GitHub Action ran only checkFileHash when command=audit,
silently skipping spec validation, duplicate-rule detection, coverage
reporting, and strengthen suggestions. That created a behavior gap
where `vigiles audit` locally would fail on a commit that the action
accepted as green.
Fix by spawning `node dist/cli.js audit [paths...]` as a subprocess
with stdio inherited and GITHUB_ACTIONS=true in the env, so the CLI's
full audit flow (including GH annotations + exit-code discipline 0/1/2)
runs inside the action. One source of truth.
Removed the now-unused runCheck() and the checkFileHash import.
* Add inline mode: vigiles:enforce HTML comments for gradual adoption
New src/inline.ts parses `<!-- vigiles:enforce <linter>/<rule> "<why>" -->`
comments out of any markdown file. vigiles audit runs each comment's
linter reference through the same checkLinterRule path that spec-mode
rules use, so inline mode gets existence checks, closest-match typo
suggestions, disabled-rule detection, and GitHub Actions annotations
for free.
Only `enforce` is supported inline. guidance rules are tautological
there — the prose around the comment already is the guidance.
Wiring:
- audit() runs verifyInlineRules() over every instruction file,
reports each rule with line numbers, emits GH annotations on error,
and counts inline errors into AuditReport so exit code 2 triggers
on any failed inline rule
- validate.ts require-spec now treats a file with at least one
`<!-- vigiles:enforce ... -->` comment as spec-equivalent so users
don't have to add a disable comment alongside the new feature
- The compiled-file hash marker (`<!-- vigiles:sha256:... -->`) is
explicitly skipped so it doesn't trip the "unknown vigiles marker"
diagnostic
New docs/inline-mode.md walks through the format, when to use it,
what it does and does NOT check, and how to graduate to spec mode.
README gains one short section linking to the doc; the Quick Start
block also got a cleanup pass for stale command names (setup→init,
strengthen→audit, check→audit).
CLAUDE.md.spec.ts updated with src/inline.ts and docs/inline-mode.md
in keyFiles, and the smooth-adoption rule now mentions inline mode
as a fallback path. CLAUDE.md recompiled.
Tests 278 → 288 (10 new in inline.test.ts + 1 hash-marker skip test).
* Resolve audit scope via compiled-from header for multi-target specs
findDuplicateRules scoped its filter by concatenating `${file}.spec.ts`,
which works for the primary target but silently skips secondary
targets in multi-target projects. A spec like CLAUDE.md.spec.ts that
compiles to both CLAUDE.md and AGENTS.md would leave
`vigiles audit AGENTS.md` scanning nothing because `AGENTS.md.spec.ts`
does not exist, and duplicate detection would report a false negative.
Read the `<!-- vigiles:sha256:... compiled from <spec> -->` header
(the same pattern validateSpecs already uses) to map each scoped file
back to its real source spec. Fall back to the naming convention when
the file has no hash header.
* Skip fenced code blocks and inline code spans in inline parser
Two review comments addressed:
- parseInlineRules scanned every line for vigiles:enforce markers
without tracking markdown code fences, so illustrative examples in
docs (including docs/inline-mode.md itself) were picked up as live
rules and either verified or reported as malformed. Track ```/~~~
fence state with length-aware open/close semantics so markers
inside code blocks are skipped.
- Even outside fenced blocks, prose containing a backtick-wrapped
example like `<!-- vigiles:enforce ... -->` would still trip the
malformed-marker diagnostic because my regex matched across the
backticks. Strip inline code spans from each line before running
ENFORCE_RE / MARKER_RE so backtick-wrapped samples are invisible
to the parser.
- validate.ts require-spec was using a loose /<!--\s*vigiles:enforce\s/
regex to decide whether inline mode satisfied the spec requirement,
which would accept malformed or in-fence markers that parseInlineRules
rejects. Replace with hasInlineRules(), which now delegates to the
real parser — so a file can only bypass require-spec if it has at
least one genuinely parseable rule.
Tests: fenced code blocks (backtick and tilde), nested fence lengths,
inline code spans in prose, ellipsis placeholders, and the
hasInlineRules fence behaviour (5 new tests, total 288 → 295).
* Separate validation errors from hashErrors; snapshot supplied Merkle history
Two review comments:
- check() was collapsing validateSpecs failures into the same
errorCount the audit flow then relabeled as "hashErrors", so a
require-spec failure in a file with a perfectly valid hash would
emit the wrong GH annotation and the wrong remediation text ("stale
hash — run vigiles compile"). Split the check() return into
`hashErrors` and `validationErrors`, thread both through
AuditReport, emit separate GH annotations, and add the validation
counter to the --summary output.
- EvolutionEngine constructor was still storing options.history by
reference — even with the verify()+head-match guards, a caller that
retained their own reference could append to the underlying chain
after construction and bypass propose()'s proof + fitness gates.
Snapshot via toJSON/fromJSON on construction so the engine's history
is an independent copy. Test asserts caller-side append is invisible
to engine.getHistory().
* Skip inline verification for spec-managed files
When an instruction file has a sibling .spec.ts (or a compiled-from
header in its content), spec mode is the source of truth and any
literal `<!-- vigiles:enforce ... -->` in the compiled output is an
example in prose, not a live rule. Previously audit would still run
verifyInlineRules on every file, so a bogus illustrative marker in
spec-generated markdown could trip the inline-errors counter and exit
with code 2 despite the spec being intact.
Guard the loop with both checks:
- existsSync(`<file>.spec.ts`) → sibling spec exists
- /<!-- vigiles:sha256: ... compiled from .+? -->/ matches content
Only files that satisfy neither go through verifyInlineRules, which is
exactly the set of hand-written instruction files in inline mode.
Test: compile a spec whose sections contain a literal enforce marker,
then audit the compiled output — asserts the bogus rule ID is NOT in
stdout and exit code is not 2.
* Scope NCD dedup to new pairs and exclude removed rules from bloom
Two real bugs in the proof suite that had been marked outdated before
I could fix them:
- findSimilarRules(after) flagged every near-duplicate pair in the
post-mutation spec, including ones that already existed in `before`.
A repo with historical duplication would therefore fail the ncd-dedup
proof on every unrelated mutation, blocking all evolution. Diff
against findSimilarRules(before) and only fail on pairs newly
introduced by the candidate change.
- Bloom overlap built its baseline from all `before` rules, including
ones the candidate mutation removed. For merges — which by design
remove source rules that share tokens with the merged rule — the
new merged rule would collide against its own sources and the merge
would be rejected for the very similarity it was meant to
deduplicate. Skip rules in `before` that are not in `after`.
Tests verify both:
- Pre-existing duplicate pair does not block an unrelated addition
- Bloom-overlap passes for a merge whose merged rule shares tokens
with the removed sources
* Update self-evolving-specs doc for current proof semantics
Stale since the check()/every() drop and the proof-suite fixes:
- Diagram: NCD "no duplicate rules" → "no NEW duplicate pairs (pre-
existing dups ignored)"; bloom "fast approximate matching" →
"token overlap vs surviving rules only (excludes removed)"
- Monotonicity: drop the middle `check (1)` rank, note that
removals also require allowWeaken
- Mutation operators table: add `remove`, clarify that `add`/`reword`
check for NEW dup pairs only, and document the merge-specific proof
(sources allowlisted, merged rule ≥ strongest source, bloom excludes
removed IDs)
- Fitness formula: coverage is enforced/total (no more "checked rules")
- Property-test mutation generators: drop references to `check` kind
- Acceptance walkthrough: update proof names to reflect current
"no new duplicate" semantics
* Add SessionStart hook that injects vigiles audit --summary
The hook pack was missing its most valuable integration: injecting
a one-line audit summary into Claude's context at session start and
after every compaction. The `audit --summary` command was already
built — it's a one-liner like
vigiles: 3 stale / 2 validation errors / 1 duplicate
— but nothing was wiring it into SessionStart. Without this hook the
agent has no automatic way to notice drift between sessions.
New hooks/session-start.sh:
- Matches both `startup` and `compact` events, so the injection fires
every time context is compacted (effectively free on tokens)
- Guards against non-vigiles projects by checking for "vigiles" in
package.json (stay silent otherwise)
- Runs `npx --no-install vigiles audit --summary` so it only triggers
when vigiles is actually installed locally
- Writes the single-line output to stdout; Claude Code prepends
SessionStart hook stdout to the session context
* Closest-match suggestions for CLI-based linters
tryNodeResolver emitted "did you mean" hints for ESLint/Stylelint
typos using the enumerable rule Set, but tryCliCheck fell back to
a plain "Rule X not found in Y" for the CLI-based linters (ruff,
pylint, rubocop, clippy). Users typo'ing "ruff/F44" would get no
suggestion at all, despite the same edit-distance helper being
sitting right there.
Add a getCliRuleSet() cache that shells out once per (linter,
basePath) to enumerate the rule list for each CLI linter:
- ruff: parse `ruff check --show-settings` linter.rules.enabled
- pylint: parse `pylint --list-msgs-enabled` symbol codes
- rubocop: parse `rubocop --show-cops` cop names
- clippy: read Cargo.toml [lints.clippy] section (same source as
generate-types)
tryCliCheck's catch branch now runs closestRuleNames against that
set and appends a "Did you mean: ..." hint to the error message.
Result cached so subsequent misses on the same linter are instant.
All six linters now have parity: inline and spec-mode typos get
the same closest-match feedback regardless of whether the linter
is Node-resolved or CLI-shelled.
* Add research: formal proofs for AI agents
Bottom line: Dafny is the only formal method where "LLM writes code +
spec + proof" is remotely credible today (AWS ships it in production
for ESDK/S3 ShardStore/crypto libs; DafnyBench has GPT-4 at ~68%).
Lean 4 dominates olympiad math (DeepSeek-Prover V2 ~88% miniF2F,
AlphaProof at IMO silver) but has near-zero production software
footprint and zero demonstrated transfer to real code. Coq/Rocq,
F*, TLA+, Liquid Haskell, Verus, Kani are all covered but less
suitable for LLM-driven code verification.
Recommendation for vigiles: ship a `dafny()` enforce target first
(references to .dfy files verified at audit time), then prototype a
`verify()` rule builder that emits Dafny obligation files behind a
flag. Proof-gated evolution engine is a research preview, not a
product — LLM proof agents still have <50% pass rate on hard
benchmarks.
* Format formal-proofs research doc
The research doc written by the background subagent didn't pass
prettier — CI fmt:check failed on it. Apply prettier --write; no
content changes, just whitespace.
* Regenerate .vigiles/generated.d.ts for src/inline.{ts,test.ts}
Stale since commit
|
||
|
|
38098d44bb |
Improve README examples and add prose section guardrails (#15)
* Improve README examples and add prose section guardrails README: Replace weak no-console examples with real-world rules people actually struggle with (@typescript-eslint/no-floating-promises, import/no-internal-modules). Rewrite the Problem section to show what CLAUDE.md files actually look like — messy prose with rot buried in paragraphs, not unrealistically clean Enforced-by markers. Compiler: Add two guardrails to prevent people from cramming entire existing docs into prose sections: - Reject # and ## headers inside section content (breaks document structure since sections compile to ## headings) - New maxSectionLines option caps per-section length, forcing authors to break content into named sections * Skip code fences in section header validation Track ``` and ~~~ fence boundaries so that # comments inside code blocks don't trigger false-positive header errors. * Leverage TS type system for stricter specs #1 + #6: Wire generate-types into enforce/file/cmd signatures. Added KnownLinterRules, KnownProjectFiles, KnownNpmScripts augmentation interfaces that generate-types populates via declaration merging. StrictLinterRule/StrictFile/StrictCmd computed types narrow builder functions when generated types are present, falling back to broad types when they're not. Scoped plugins (@typescript-eslint etc.) are handled as KnownLinterRules keys. #4: Reserved section key validation. Sections named "commands", "keyFiles", or "rules" now produce a compile error — use the dedicated spec fields instead. #5: Conditional maxSectionLines. ClaudeSpecInput uses a discriminated union so maxSectionLines is only valid when sections is defined. Per-spec maxSectionLines takes precedence over compile options. #7: Phantom types for compile pipeline stages. RawSpec, RefsValidated, LintersVerified, ReadyToEmit branded types track which validations have been applied via a unique symbol brand. CI: Added generate-types freshness check — runs generate-types and fails if git diff is non-empty. * Add plugin hook for type freshness + generate-types --check Plugin: Added PostToolUse hook script that reads the edited file path from stdin JSON. When a linter config or package.json changes, runs generate-types. When a .spec.ts file changes, runs compile. Keeps generated types and compiled specs fresh without manual intervention. CLI: Added --check flag to generate-types. Compares generated output against the existing .d.ts file and exits 1 if stale — no git diff needed. CI step updated to use this instead of the git diff approach. * Update README with type narrowing docs and --check flag Document how generate-types narrows enforce/file/cmd via declaration merging. Add generate-types --check to CLI section and GHA examples. * Add NoInfer to builder functions + spec file naming validation #9: Wrap enforce(), file(), cmd() parameters with NoInfer<> to prevent TypeScript from silently widening string literals to string when passed through variables. One-line change per function, real safety gain. #11: Spec files must follow the <output>.spec.ts naming convention. compileClaude and compileSkill now validate that the spec filename ends with .spec.ts and maps to a .md output (e.g., CLAUDE.md.spec.ts → CLAUDE.md). Added SpecPath<T> and OutputPath<T> utility types for type-level spec↔output path relationships. * Add multi-target output + document satisfies pattern Target field: Specs can now compile to CLAUDE.md, AGENTS.md, or any custom .md target. Pass an array to emit multiple files from one spec (e.g., target: ["CLAUDE.md", "AGENTS.md"]). The CLI writes each target with its own heading; validations and linter cross-referencing are shared. Defaults to CLAUDE.md for backward compat. satisfies pattern: Documented how teams can use TypeScript's satisfies keyword to enforce that specs always include required sections/commands. This is a convention, not a feature — vigiles just provides the types. Also updated CLAUDE.md, README, and example spec to mention AGENTS.md support and use the stronger no-floating-promises example. * Add require-spec validation rule New opt-in rule that checks every instruction file (CLAUDE.md, AGENTS.md, SKILL.md) has a corresponding .spec.ts file. Nudges migration from hand-written markdown to typed specs. Disabled by default in recommended pack, enabled in strict pack. Disable per-file with <!-- vigiles-disable require-spec --> comment. * Remove rule packs and legacy validation rules Gutted the v1 validation system. Removed: - Rule packs (recommended/strict) and extends config - require-annotations (compiler generates these) - max-lines (compiler has maxTokens) - require-rule-file (compiler cross-references linters) - no-broken-links (compiler verifies file/cmd/ref) - require-structure (compiled output is correct by construction) - mdschema integration, linter resolvers, CLI rule checkers, config-enabled checkers, schema presets, annotation typo detection Kept: parseClaudeMd (useful metadata), require-spec (enabled by default — checks every .md has a .spec.ts), basic validate/ validatePaths/readClaudeMd/expandGlobs/findInstructionFiles. -2365 lines, +180 lines. validate.ts went from ~1100 lines to ~330. validate.test.ts went from ~1975 lines to ~500. README: Added Claude Code Plugin section documenting the PostToolUse hook and validation rule documentation. * Rename parseClaudeMd → parseRules, readClaudeMd → readInstructionFile These functions work on any markdown instruction file (CLAUDE.md, AGENTS.md, etc.), not just CLAUDE.md. * Fix critical adoption gaps CLI: Wire require-spec into vigiles check — the rule existed but no CLI command ever called validate(). Now check runs hash verification, filesystem assertions, AND spec validation. CLI: Add --target flag to vigiles init. Users can now scaffold specs for any target: vigiles init --target=AGENTS.md creates AGENTS.md.spec.ts. Plugin: Remove broken v1 hook (vigiles CLAUDE.md was an invalid command that exited 1). Keep the post-edit.sh hook which handles auto-regen. Skills: Fix stale vigiles CLAUDE.md reference in enforce-rules-format. * Add adoption strategy research doc + CLAUDE.md goal New research doc mapping the complete adoption journey from zero to fully integrated (5 levels: discovery → scaffold → CI → editor → agent → multi-target). Documents pain points, gaps, and the incremental vs. strict enforcement dial. CLAUDE.md: Added adoption principle — every surface must work together, support both incremental migration and strict enforcement. * Dogfood: compile CLAUDE.md from a typed spec Created CLAUDE.md.spec.ts at the project root — the source of truth for the project's own CLAUDE.md. The compiled output has a vigiles hash comment (<!-- vigiles:sha256:... -->) that serves as a "do not edit directly" marker. CLI: Fixed loadSpec to handle root-level specs via tsx fallback. Added tsx as dev dependency for on-the-fly .ts spec loading. Handles ESM/CJS double-default interop. Also fixed unused import in examples/CLAUDE.md.spec.ts. * Add CLI integration tests + fix executeAssertion test New test file: src/cli.test.ts with 15 integration tests covering: - vigiles init (default + --target=AGENTS.md + custom targets) - vigiles compile (no specs, project root spec) - vigiles check (no files, missing spec, spec exists) - vigiles generate-types (generation + --check freshness) - Multi-target compilation E2E (CLAUDE.md + AGENTS.md from one spec) - vigiles discover (linter coverage output) - Plugin hook (syntax check, empty input handling) Fixed executeAssertion test: src/cli.test.ts now exists so the "fails when paired files are missing" test needed to use src/action.ts instead of src/cli.ts. Test count: 192 passing (up from 177). * Add PreToolUse hook, setup wizard, and clear installation flow PreToolUse hook: Blocks direct edits to compiled .md files (those with a vigiles hash comment). Exit code 2 tells Claude Code to block the action. Stderr message redirects the agent to the .spec.ts source. Setup wizard: vigiles setup is a one-command installation path that creates a spec, generates types, and compiles — with next-step guidance. Supports --target=AGENTS.md for non-CLAUDE.md targets. README: Rewrote Quick Start to lead with vigiles setup. Made plugin installation prominent — "Install the plugin" with clear explanation of what the two hooks do (PreToolUse blocks compiled file edits, PostToolUse auto-recompiles on changes). Compiler: Added blank line after hash comment for prettier compat. Updated HASH_RE and multi-target heading swap regex to match. Tests: 198 passing. Added setup CLI tests and pre-edit hook tests (syntax validation, exit-0 for non-md, exit-2 for compiled md, exit-0 for hand-written md). * Setup wizard handles GHA + simplified README + adoption doc rewrite Wizard: vigiles setup now auto-adds a CI step (vigiles check + generate-types --check) to existing GHA workflows. Finds ci.yml, main.yml, or test.yml and appends the step. Falls back to printing manual instructions if no workflow found. README: Quick Start is now just `npx vigiles setup` — one command. No separate installation steps. Plugin install and migration are secondary paths below. Adoption doc: Complete rewrite. Clarified scope (vigiles = markdown targets only, sync tools = non-markdown distribution). Documented the wizard as the single entry point. Updated pain points table. Specified the README structure the project should follow. * Swap README examples to universally recognizable rules Replace @typescript-eslint/no-floating-promises with no-explicit-any throughout. Everyone knows "don't use any" — it's the most relatable TS rule and the "disabled to unblock a deadline" story is universal. Clarified that enforce() checks both existence AND enabled status. A disabled rule is a compile error, not just a warning. * Setup wizard detects existing files + cleaner CI message When a hand-written CLAUDE.md already exists without a spec, the wizard now detects it and suggests the migrate-to-spec skill instead of blindly creating a blank template. Cleaned up the fallback CI message to show one command instead of two separate lines. * Agent-specific wizard guidance + agent workflows doc Wizard: When target is AGENTS.md, the setup wizard now explains that Codex/Copilot don't support plugins — CI is the enforcement mechanism. Suggests manual compile after spec edits. Points to the plugin as optional if also using Claude Code. New doc: docs/agent-workflows.md covers the full workflow for each agent type (Claude Code with plugin, Codex/Copilot CI-only, multi-agent with dual targets, non-markdown format sync). * Setup wizard auto-detects project agents and configs The wizard now scans the project before asking what to do: - Finds existing instruction files (CLAUDE.md, AGENTS.md) and checks if they have specs - Detects agent tools via config files (.claude/, .cursorrules, .windsurfrules, .github/copilot-instructions.md) - Checks package.json for sync tools (rule-porter, rulesync) - Notes symlinked instruction files - Auto-determines targets based on what's detected If existing hand-written files are found, suggests migration instead of blindly creating blank specs. If non-markdown agent configs are detected without a sync tool, suggests installing rule-porter. Docs: Added auto-detection table to agent-workflows.md. Tests: 3 new auto-detection tests (201 total passing). * Add thorough hook routing tests Pre-edit hook: already had 4 tests (syntax, non-md pass-through, compiled md blocks, hand-written md passes). Post-edit hook: added 4 new tests: - Config file routing (eslint, pyproject, Cargo, package.json, etc.) - .spec.ts file routing (triggers compile) - Unrelated file skip (no action triggered) - Existing empty input test Tests verify the bash case-matching logic works for each file type without actually running the slow npx commands (PATH set to /nonexistent so npx isn't found, but the routing still executes). 204 tests total passing. * Add edit-spec skill + improve PreToolUse hook guidance PreToolUse hook: now gives step-by-step instructions when blocking a compiled file edit — read the spec, edit it, compile, verify. Points to the edit-spec skill for format guidance. New skill: edit-spec walks the agent through editing a .spec.ts file. Covers all field types (sections, keyFiles, commands, rules), all three rule types (enforce/check/guidance), common errors, and the compile+verify workflow. This completes the smooth editing flow: User says "update CLAUDE.md" → agent tries Edit on CLAUDE.md → PreToolUse blocks with actionable steps → agent reads the .spec.ts, edits it → PostToolUse auto-compiles → CLAUDE.md regenerated * Zero-friction adoption story in README + updated goals README: Lead with the two-command install + "your agent starts editing specs automatically." Quick Start now shows what happens AFTER install (agent redirected, auto-compile, auto-regen types, CI catches drift) instead of just listing commands. Adoption goals updated in both CLAUDE.md.spec.ts and research doc: - Works on first run with zero config - Zero workflow change — agent edits specs automatically after install - Start permissive, tighten later — guidance rules first, enforce later - Every surface tells you the next step — no dead ends * Self-maintaining messaging, agent setup doc, fixture E2E tests README: Added "It's self-maintaining" paragraph — hooks keep types and compiled output in sync automatically. No manual reruns needed. New doc: docs/agent-setup.md covers non-interactive installation by agents. Key insight: agents can't run `skills add` but CAN write hooks directly to .claude/settings.json. Includes recommended prompt and common mistakes section. New fixture: fixtures/example-project/ — minimal project with package.json, source files, and a hand-written CLAUDE.md for E2E testing. New E2E tests: 5 tests using the fixture project covering the full adoption flow — setup detection, init, generate-types, check, and the complete write-spec → compile → check cycle. 209 tests total passing. * Add strengthen command + ASCII adoption flow + self-documenting specs vigiles strengthen: scans guidance() rules and suggests enforce() upgrades by matching keywords to enabled linter rules. Bridges the gap between zero-config permissive setup and full enforcement. Init template: now includes a how-to-edit section that compiles into the instruction file. Agents learn how to edit specs from the file itself — no skills installation required for basic editing. README: Added ASCII adoption flow showing the journey from zero-config → auto-evolve → full enforcement. Rewritten Quick Start emphasizes that it just works after install. CLI usage: added strengthen to command list. Tests: 2 new strengthen tests (211 total passing). * Wizard auto-installs Claude Code hooks + commit suggestion Setup wizard now writes PreToolUse and PostToolUse hooks directly to .claude/settings.json — no separate `npx skills add` step needed. The wizard detects whether it's a Claude Code project and installs hooks automatically. After setup, prints a ready-to-run git add + commit command so the user can commit everything in one shot. README: Simplified Quick Start to truly one command. No more two-step install. * Add PreToolUse hook to project settings (dogfooding) The wizard auto-installed the PreToolUse hook that blocks direct edits to compiled CLAUDE.md. This is correct — our CLAUDE.md is compiled from CLAUDE.md.spec.ts and should be edited via the spec. * Wizard tries full plugin install, falls back to direct hooks The setup wizard now tries npx skills add zernie/vigiles first, which installs both hooks AND skills (edit-spec, migrate-to-spec, etc.). If the skills CLI isn't available, falls back to writing hooks directly to .claude/settings.json. This gives auto-compile + block edits but not the guided skills. In that case, prints a hint to install the full plugin later. This clarifies the mental model: - Plugin (via skills add) = hooks + skills (the ideal path) - Direct hooks = auto-compile only (the fallback) - Codex/AGENTS.md = no hooks needed, CI enforces * Fix audit gaps: stale hooks, missing docs, wrong comments Critical fixes: - .claude/settings.json: replaced broken v1 PostToolUse hook (npx vigiles CLAUDE.md) with proper conditional auto-compile logic - docs/spec-format.md: added target field + maxSectionLines docs Minor fixes: - src/spec.ts: prove() → check() in JSDoc comments (2 occurrences) - CLAUDE.md.spec.ts: added ALL research docs, docs, and test files to keyFiles so they're referenced in compiled output. Added cli.test.ts, all research/*.md, all docs/*.md. * Add migrate command, multi-target GHA, stronger README pitch vigiles migrate: Parses a hand-written CLAUDE.md (or AGENTS.md), extracts sections/rules, and scaffolds a .spec.ts. Handles enforced rules (preserves linter references) and guidance rules. Supports --target for AGENTS.md with auto-detection. GHA action: Now writes all targets from multi-target specs, not just the primary output. Same heading-swap logic as the CLI. README: Rewrote the opening pitch. "Markdown can't be validated. TypeScript can." Positions vigiles as essential infrastructure — not using it with AI agents is like not using ESLint with JavaScript. docs/spec-format.md: Added target field and maxSectionLines docs. research/feature-ideas.md: Added TODO items for exhaustive rule coverage type and variadic check() assertions. Tests: 214 passing. 3 new migrate tests (hand-written → spec, no-overwrite, AGENTS.md target). * Fix CI failures + all Codex review comments Check failures fixed: - require-spec now only applies to CLAUDE.md/AGENTS.md, not SKILL.md (skills are hand-written by design) - Removed check() assertion from example spec (was running against real project files and failing on types.ts, linters.ts, etc.) - Recompiled all specs with fresh hashes Codex review #1 — escape \${ in migrate template literals: migrate() now escapes \${ to prevent template interpolation in generated .spec.ts files. Markdown with shell examples like \${HOME} would otherwise cause ReferenceError. Codex review #2 — recompute hash for secondary targets: Multi-target compilation (cli.ts + action.ts) now strips the old hash, swaps the heading, and recomputes via addHash(). Previously secondary targets got a stale hash from the primary target. Codex review #3 — escape rule titles in migrate string literals: migrate() now escapes backslashes and quotes in rule titles to prevent syntax errors in generated .spec.ts files. Tests: 215 passing. Added SKILL.md exclusion test for require-spec. * Fix CI: prettier-compatible SKILL.md frontmatter The compiler now outputs blank lines after opening --- and before closing --- in YAML frontmatter, matching prettier's formatting. This prevents prettier from reformatting the compiled output, which would invalidate the hash and cause vigiles check to fail. Fixed compileSkill test to match the new frontmatter format. * Severity levels for require-spec + require-skill-spec Two rules, three severity levels each: - require-spec: "warn" (default) — CLAUDE.md / AGENTS.md need specs - require-skill-spec: "warn" (default) — SKILL.md needs specs - Levels: "warn" (print but exit 0), "error" (fail), false (off) Warnings print with ⚠ prefix, errors with ✗. Warnings don't fail CI. Users upgrade "warn" → "error" in .vigilesrc.json when ready. Also fixed: - generate-types --check: normalized comparison ignores formatter whitespace differences - Compiler SKILL.md frontmatter: blank lines match prettier format - validate() result now includes separate warnings array * Fix all PR review comments + CI failures + severity levels CI fix: - Added .prettierignore for .vigiles/generated.d.ts (prettier was reformatting the generated file, making --check always fail) PR review fixes (9 open comments): - P1: validateSpecs now loads .vigilesrc.json config (require-spec: false was being ignored) - P1: vigiles check now discovers AGENTS.md files (was only finding CLAUDE.md and SKILL.md) - P1: GHA step insertion appends at end of file instead of fragile regex after last run: line (fixes run: | block corruption) - P2: Multi-target files with vigiles hash skip require-spec (the hash proves they came from a spec) - P2: Migration gate fixed (condition was always true) - P2: Section header/fence detection allows up to 3 leading spaces per CommonMark spec - P2: Hook installation appends to existing arrays instead of skipping New features: - --strict flag: sets require-spec and require-skill-spec to "error", creates .vigilesrc.json - --no-gha flag: skip GHA step insertion - Severity levels: "warn" | "error" | false for both rules - require-skill-spec rule for SKILL.md files (default: "warn") - Wizard summary shows upgrade path (warn → strengthen → strict) * Fix last PR review comment: validate spec filename matches target Spec naming check now verifies the filename matches the primary compile target (e.g., CLAUDE.md.spec.ts must target CLAUDE.md). Previously it only checked for .md extension, allowing mismatches like CLAUDE.md.spec.ts with target: "AGENTS.md". Also applied consistent naming check to compileSkill. * Recompile CLAUDE.md from spec * Fix lint errors + verify source spec exists on check Lint fixes in spec.ts: - Fixed eslint-disable comments (no-empty-interface → no-empty-object-type) - Added block disable for no-redundant-type-constituents on StrictFile and StrictCmd (intentional — escape hatch pattern requires it) - Added disable for no-unnecessary-type-assertion on enforce() cast Codex review: validateSpecs now verifies the source spec referenced in the hash comment still exists. Catches orphaned compiled files where the spec was deleted but the .md remains. 0 lint errors, 217 tests pass. * Fix Windows path splitting + git add comma in setup summary validate.ts: Use path.basename() instead of split("/").pop() for extracting filename. Fixes require-spec silently never running on Windows where resolve() returns backslash paths. cli.ts: Setup summary git add command now lists spec paths as separate entries instead of a comma-joined string. Fixes copy-paste producing invalid git commands for multi-target setups. * Fix generate-types test: use temp dir instead of project root The --check test was regenerating types into the project's own .vigiles/generated.d.ts, which could differ in CI and cause the subsequent generate-types --check CI step to fail. * Fix: generate-types test was overwriting project's .d.ts in CI Both generate-types tests were running against process.cwd(), which wrote to the project's .vigiles/generated.d.ts during the test run. In CI, the generated output could differ from the committed file, causing the subsequent generate-types --check CI step to fail. Both tests now use isolated temp dirs. * Fix 3 Codex review comments: path.basename, spec glob, action discovery compile.ts: Use path.basename() instead of split("/") for spec filename check. Fixes Windows path handling. cli.ts: findSpecs() default glob changed from **/*.spec.ts to **/*.md.spec.ts. Prevents matching unit test files (*.spec.ts) which would cause load failures and noise during setup. action.ts: Check discovery now includes **/AGENTS.md alongside CLAUDE.md and SKILL.md. Multi-target specs that emit AGENTS.md are now verified by the action's check command. * Fix compile test: use temp dir instead of project root The compile test was running against the project root, which could modify CLAUDE.md and cause CI hash checks to fail. Now uses a temp dir with a minimal spec that imports from dist. * Fix CI: use node dist/cli.js instead of npx vigiles npx vigiles fails with exit code 127 (command not found) in CI because the repo's own bin isn't linked in node_modules/.bin. Use node dist/cli.js directly for both generate-types --check and vigiles check steps. * Fix CI: generate-types check fails because CI has more linters CI installs pylint, rubocop, ruff, clippy — so generate-types discovers more rules than the locally-committed .d.ts which only has eslint. The --check comparison will always fail. Changed CI step to regenerate types and verify they compile (tsc), instead of checking exact file match. --------- Co-authored-by: Claude <noreply@anthropic.com> |