Files
zernie e8fa3b6b78 refactor!: pre-release polish — surface freeze, spec-first defaults, auto-adopt (#48)
* chore: refresh HANDOFF — PR #47 merged, surface-freeze still open

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhTWXGSinhKGqrgTvPMxTG

* refactor!: freeze pre-release public surface + collapse markdown ladder

Pre-release surface freeze (research/pre-release-focus.md):

- Mark the experimental typed-composition cluster @internal in vigiles/spec
  (pipe/pipeStep/needs/start/andThen + Supplies/Handoff/KnownAgentName/
  Pipeline/PipeStep/NeedsContract) and effect()/EffectRegion. They stay
  exported at runtime (no removal) but are excluded from the published API
  docs and flagged unstable in the committed surface reports, so a later
  change to them is not a breaking change to a documented-stable API.
- Note evolve.ts as @internal/research-stage (already off every public entry).
- Markdown cut: drop the "Level 0/1/2" ladder framing across public docs;
  collapse to two on-ramps (plain markdown via inline enforce comments ->
  typed spec). Frontmatter mode is KEPT, still works, and its full docs +
  the old ladder are PARKED via an HTML comment in docs/markdown-mode.md
  (matching the README's parked-section convention) — nothing deleted,
  trivially un-parked post-launch.
- Ship STABILITY.md (0.x: CLI stable; library API evolving; experimental
  surfaces marked) + a one-line README pointer.
- Regenerate etc/*.api.md to reflect the @internal tags (api:check green).

BREAKING CHANGE: public symbols in vigiles/spec are re-tagged @internal
(typed-composition combinators, effect()). They remain importable but are no
longer part of the frozen, supported surface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhTWXGSinhKGqrgTvPMxTG

* docs: surface the author-time-vs-runtime wedge + citeable authorities

- README hero: state the differentiator positively — vigiles proves the
  harness "before the agent ever runs" (author-time / pre-run), ending on
  verify/test/measure (no negative opener, per the README direction rules).
- pre-release-focus.md Positioning lock: fold in the canonical one-line
  wedge (author-time vs runtime, public-safe phrasing, no competitor names)
  and the citeable harness-engineering authorities (Karpathy "automate what
  you can verify"; OpenAI "the harness is hard"; arXiv AHE structure-beats-
  prose = enforce > guidance) for the launch article.

The AHE prediction-loop note already lives on the roadmap A2 optimizer item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhTWXGSinhKGqrgTvPMxTG

* docs: lead the README hero with the concrete pain (untested harness)

Reframe the hero to go straight to the developer pain: you installed
plugins and wrote skills but don't know if they work — a library with no
tests. Replaces the "100x coder / 1x verifier" lead (kept as a backup hook
in the positioning lock) with the concrete plugins/skills situation +
silent-fail triad, ending on the verify/test/measure benefit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhTWXGSinhKGqrgTvPMxTG

* docs: lead the guide openings with the concrete pain, not the mechanics

Apply the README's pain-first hook to the subdoc openings:
- verifying-instruction-files.md: open with the CLAUDE.md-drifts pain
  instead of "vigiles's linting layer ... checks every reference".
- for-plugin-authors.md: add the installer pain hook (do your skills fire
  for the people who install it?) above the audience-routing intro.
- harness-testing.md: sharpen the lead to the concrete "does it actually
  block? does your skill fire?" question.

Each keeps its README uplink + "what this doc is" line (docs-quality).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhTWXGSinhKGqrgTvPMxTG

* docs: codify the pain-first principle + fix a stale doc link

- README direction comment: add rule 1c — lead with the concrete pain in
  the reader's own situation (the specific silent failure + the "library
  with no tests" anchor), for the hero AND every subdoc opening; reconcile
  with 1b (a pain about the reader's situation is a hook, not an apology).
- Fix a stale link: src/vendor.test.ts -> src/adapters/claude-code/vendor.test.ts
  (moved in the adapter relayout).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhTWXGSinhKGqrgTvPMxTG

* chore: refresh HANDOFF — surface freeze + launch polish shipped on branch

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhTWXGSinhKGqrgTvPMxTG

* docs(positioning): not a linter — spec-first + eject, don't fight on structure-lint

Lock the decision (Positioning lock + roadmap launch-readiness): vigiles is
NOT a markdown/structure linter and won't contest that crowded, commoditized
lane. The bold differentiated play is author-time typed specs the agent
writes + evals you can afford. Markdown demotes from on-ramp to floor /
eject-target. Names the enabling build — a one-command `vigiles eject`
(strip the integrity header -> clean owned markdown) so "always ejectable"
is true and the two spec objections (TypeScript friction, lock-in) dissolve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhTWXGSinhKGqrgTvPMxTG

* feat: add `vigiles eject` + reframe spec-first (managed, but ejectable)

The escape hatch that makes "adopt a typed spec, you can always eject" true,
and the positioning shift that leans on it.

- `vigiles eject [file]`: the inverse of compile. Strips the vigiles:sha256
  integrity header -> plain hand-owned markdown, removes the managing spec
  (--keep-spec to keep it), and adds a `vigiles-disable require-spec` marker
  so lint stays quiet. Pure helpers (parseIntegrityHeader/ejectMarkdown in
  core/integrity.ts) + handler + dispatch + help + docs/cli.md. New verb in
  cli-commands.ts (additive). Unit + e2e tests.
- Positioning reframe: don't contest the structure-lint crown. README hero
  sub-line + spec mentions lead with "your agent writes the spec, and you can
  eject anytime"; markdown demoted to floor/eject-target (still the zero-TS
  on-ramp). README direction rule 1c codifies the pain-first hook.
- adopt-spec skill: faithful-by-default + eject note; dropped the Level 0/1
  ladder pitch; fixed stale commands (check->lint, generate-schema->generate
  schema, Action @main command:check -> @v1 command:lint).
- Closed the scan gap: skills/ now in the self-command-refs dogfood, which
  caught two rotted `vigiles generate-types` refs (now `generate types`).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhTWXGSinhKGqrgTvPMxTG

* feat(scan): nudge toward the real-model trigger tier; agent-safe by default

When a plain `scan` finds model-invocable skills AND a model is reachable
(ANTHROPIC_API_KEY, or an authenticated Claude Code session via CLAUDECODE /
CLAUDE_CODE_ENTRYPOINT), surface the real-model `--trigger` tier that measures
whether those skills actually FIRE:

- A human at a TTY is OFFERED setup (scaffold a trigger-prompts.json, or run an
  existing one — the real-model run is always an explicit yes).
- An agent / CI run (non-TTY / --json / --no-interactive / --yes) gets a
  one-line, non-blocking hint and never a prompt (great-agent-flow).

Pure core (hasModelAccess / decideTriggerSuggestion / formatTriggerHint /
scaffoldTriggerPrompts) + unit tests; CLI wiring + env-gated e2e in
scan-cli.test.ts; docs in docs/cli.md.

Also records (HANDOFF) that real-model tiers ARE runnable in Claude Code web:
the claude CLI is authenticated via session OAuth (no API key), proven by
`claude -p` and a live `scan --trigger` run (test-harness recall 100%).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhTWXGSinhKGqrgTvPMxTG

* feat: --strict gates broken surfaces (CI red); fix `lint <dir>` crash

The actual "edit a skill/subagent/hook badly → CI goes red" payoff.

- `vigiles init --strict` now promotes the HIGH-PRECISION, FP-safe structural
  rules to `error` (not just require-spec): subagent-tool-contract,
  subagent-frontmatter, hook-events, hook-script-exists, mcp-config,
  mcp-tool-resolves, mcp-hook-target-resolves, disallowed-tools-contract,
  description-overlap. So a typo'd tool, a dead hook script, a misconfigured
  MCP server, or two colliding skill descriptions FAIL `vigiles lint` (exit 2).
  Never clobbers a user-set severity; leaves the lenient/recommendation rules
  (frontmatter-valid, skill-frontmatter, prefer-compiled-hooks, untested-*) at
  warn. Honest limit documented: CC itself loads a name-less / broken-YAML
  skill, so vigiles gates the subagent/hook/MCP defects + skill collisions that
  genuinely break, not skill content that still works. Proven: clean subagent
  exit 0, typo'd tool exit 2.
- FIX: `vigiles lint .` (a directory arg) crashed with EISDIR (it read `.` as a
  file). findInstructionFiles now expands a directory arg to the instruction
  files inside it; a file arg and a non-existent arg pass through unchanged.

Tests: setup-plan (strict gate set), cli (lint-dir no-crash + strict exit-2
regression).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhTWXGSinhKGqrgTvPMxTG

* feat: init gates broken surfaces BY DEFAULT (no --strict needed)

Fold the structural gating into a plain `init` — no confusing "strict mode" to
remember. A default `init` now writes the 9 HIGH-PRECISION, FP-safe structural
rules as `error` (subagent-tool-contract, subagent-frontmatter, hook-events,
hook-script-exists, mcp-config, mcp-tool-resolves, mcp-hook-target-resolves,
disallowed-tools-contract, description-overlap), so a broken subagent / hook /
MCP ref or a skill collision FAILS `vigiles lint` out of the box — while a
well-formed plugin stays green (never cries wolf).

`--strict` is reframed to the WORKFLOW-FORCING tier a clean repo can still fail
because the work isn't done yet: require-spec, untested-skill/subagent/hook,
frontmatter-valid, skill-frontmatter. Opt-in by design (progressive adoption),
so a first CI run isn't red just for not having written a spec yet.

Never clobbers a severity the user already set. vigiles dogfoods it — the 9
gate rules added to its own .vigilesrc.json (self-lint stays green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhTWXGSinhKGqrgTvPMxTG

* feat(init): interactive init offers the workflow tier (recommended, opt-out)

Per the chosen default model: structural gating is always on; the WORKFLOW
tier (require specs + a test per surface) is the recommended default a human
opts OUT of, never forced on a silent run.

- Interactive `init` now asks "Also enforce specs + a test per surface
  (recommended)? [Y/n]" (default yes) → sets strict. SetupAnswers/applyAnswers
  carry it; resolvePlan honors it.
- A bare NON-interactive `init` (agent/CI, no one to ask) stays structural-only
  unless `--strict` — so an automated setup never silently turns an existing
  repo's CI red just for not having written a spec yet.
- README agentic-install prompt + docs/cli.md updated: the agent asks before
  enforcing the workflow tier, structural gating is the safe default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhTWXGSinhKGqrgTvPMxTG

* chore: refresh HANDOFF — spec-first defaults + eject + scan nudge; PR is the next step

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhTWXGSinhKGqrgTvPMxTG

* test(init): make the interactive setup Q&A unit-testable (inject ask)

The interactive init prompts were the one untested-and-fragile bit of the
setup flow (readline can't run in CI's non-TTY env, so the questions/defaults/
answer-mapping had zero coverage). Extract the Q&A into a pure
collectSetupAnswers(ask) with an injected AskFn; promptSetup is now just the
readline IO shell. Unit-tested with a fake ask: all-defaults, lint/test pillar
selection, and declining CI/plugin/strict — deterministic, no TTY, no model.

Now the init flow is covered end to end: pure plan logic + the interactive
Q&A (unit) + non-interactive init (e2e in cli.test.ts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhTWXGSinhKGqrgTvPMxTG

* docs: install-enforcement model — rule groups (not presets) + rationale

Capture the reasoning, not just the outcome, before implementing.

- New `install-enforcement-model` rule in CLAUDE.md (via the spec): rules are
  organized into GROUPS by confidence (structural / workflow / nudge), NOT a
  relaxed/standard/strict preset MENU — the linter best practice (Clippy/Ruff/
  Biome; "presets are a poor emulation of rule groups"). default = structural
  at error; --strict adds workflow; --report-only is an orthogonal severity
  dial. Installation is EXPLICIT ABOUT TRADEOFFS (one source reused across the
  CLI prompt / agent AskUserQuestion / summary).
- Reconcile smooth-adoption + progressive-adoption to the new stance:
  "permissive = doesn't force specs/TS + doesn't cry wolf, NOT ignores breakage."
- research/install-enforcement-dx.md: the full design record — the linter
  landscape (verified), why we rejected a preset menu, the group→rule mapping,
  the agentic-install DX (no prior art), and the philosophy reconciliation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XhTWXGSinhKGqrgTvPMxTG

* refactor!: rename require-spec → require-instructions-spec; rule groups; init auto-adopt

Rename + narrow the rule, name the install rule-groups, and make `init`
faithfully adopt existing instruction files so the rule is satisfied by
construction.

- require-spec → require-instructions-spec, NARROWED: only a `.spec.ts`
  satisfies it — inline `vigiles:enforce` / `vigiles:` frontmatter no longer do.
  Disable marker is now `<!-- vigiles-disable require-instructions-spec -->`.
- Name the rule groups in src/setup-plan.ts (STRUCTURAL_RULES / WORKFLOW_RULES /
  NUDGE_RULES) and add `--report-only` (writes the whole gate at warn).
- prefer-compiled-hooks defaults off; require-skill-spec un-deprecated (the
  consistent require-<surface>-spec parallel, default off).
- New src/core/adopt.ts: adoptMarkdown/adoptToSpec faithfully convert an existing
  CLAUDE.md/AGENTS.md into a claude() spec (heading → verbatim section, no rule
  inferred, always compiles). Wired into init()/setupPillar1; round-trip tested
  (byte-identical below the integrity header).
- Sweep stale Level-0/1/2 markdown-mode comments; docs + CLAUDE.md recompiled.

BREAKING CHANGE: the `require-spec` lint rule and config key are renamed to
`require-instructions-spec` and narrowed — only a `.spec.ts` satisfies it (inline
comments / `vigiles:` frontmatter no longer do), and the per-file disable marker
is now `<!-- vigiles-disable require-instructions-spec -->`. No automatic alias;
update `.vigilesrc.json` and any disable markers.

* fix(init,eject): honor interactive strict; don't delete a shared spec on eject

Addresses two Codex review findings on #48 + the fmt:check CI failure.

- setup(): write `.vigilesrc.json` and the summary from the RESOLVED plan
  (`plan.strict`), not the raw `--strict` flag — so an interactive "yes" to the
  workflow tier is actually persisted instead of silently dropped.
- eject(): only delete the header's `compiled from` spec when it EXCLUSIVELY
  belongs to the file being ejected (`<file>.spec.ts`). A mirrored secondary
  target (e.g. AGENTS.md compiled from CLAUDE.md.spec.ts) names the shared source;
  deleting it would break the primary's recompile. Keep it + warn instead.
  Regression test added.
- Format HANDOFF.md (the fmt:check failure).

* fix(init): non-destructive adopt, deferred compile, test-only & strict config gating

Install-DX hardening from PR #48 review + a self-audit across user/project types.

- Auto-adopt is now NON-DESTRUCTIVE: `init` writes the spec but never overwrites
  your CLAUDE.md/AGENTS.md. Run `vigiles compile` to opt the file into spec
  management (byte-faithful; review the diff; `eject` reverses it).
- Defer the compile when `vigiles` isn't resolvable yet (a fresh repo hasn't run
  `npm install`) — print the next step instead of a "failed to load" error.
- Test-only setup (`init --test` / `--no-lint`) records the harness but writes NO
  lint rules — honors the positive-flag contract (Codex review).
- setup() writes config from the RESOLVED plan, so an interactive "yes" to the
  workflow tier persists (was dropped).
- Next steps reordered (install → compile → strengthen → test); `--report-only`
  in `init --help`; README + docs surface non-destructive adopt. New e2e tests.

* fix(adopt): don't drop intro text when a literal ## Overview already exists

Codex review (P2): a file with intro prose before the first heading AND an
explicit `## Overview` section produced two sections keyed "Overview" — building
the sections object overwrote the synthesized intro, silently dropping it (and a
later `compile` would remove it from the file).

Extract a shared `allocKey` dedup helper and use it for the synthesized Overview
too, so it becomes "Overview (2)" on collision instead of clobbering. Regression
test added.

* fix(eject): preserve frontmatter on skill/agent eject; keep shared multi-target specs

Two Codex review P2s on the eject feature.

- ejectMarkdown no longer prepends the require-instructions-spec disable marker
  before a body that leads with YAML frontmatter (a compiled SKILL.md / subagent):
  doing so displaced the frontmatter and lost the surface's name/description/tools.
  The marker is instruction-file-only, so a frontmatter-led body ejects verbatim.
- eject deletes the header's spec only when NO other compiled output still
  references it (a directory scan), instead of a basename heuristic. A multi-target
  spec (`target: ["CLAUDE.md","AGENTS.md"]`) or mirror now keeps its spec until the
  LAST output is ejected — ejecting either the primary or a secondary no longer
  orphans the others. The trailing marker hint prints only when a marker was added.

Tests: frontmatter-preserved eject (unit) + eject primary/secondary/last-output of
a multi-target spec (e2e). Docs note the two safety behaviors.

* fix(eject,scan): path-safety on spec deletion; gate scan prompt on stdin TTY

Two Codex review P2s.

- eject: the `compiled from <path>` header is untrusted — a hand-edited/forged
  header naming `package.json` or a `../` path would make rmSync delete an
  unrelated file. Only remove a `.spec.ts` that resolves INSIDE the project;
  otherwise keep it and warn. Regression test added.
- scan --trigger nudge: prompt only when BOTH stdout AND stdin are a terminal —
  `askOnce` reads stdin, so a TTY stdout with piped/redirected stdin (agents,
  pipelines) must take the non-blocking hint path instead of blocking on a read.

* docs(readme): add FAQ + docs/faq.md, tighten install prompt, lift adoption line

5-star onboarding pass (1/N).

- New docs/faq.md: the objection-handling answers (why a typed spec & is it
  optional, do I write TS, does init touch my files, CC+Codex, cost/key, non-JS
  repo, safe to run, vs promptfoo).
- README: compact FAQ section linking it; install prompt rewritten to plain
  English (names adoption, tells the agent to finish: install + compile, no flag
  jargon); the non-destructive adoption reassurance lifted above the fold.

* fix(init): honest guidance for non-JS repos (no package.json)

5-star onboarding pass (2/N). A Python/Rust/Go repo with a CLAUDE.md but no
package.json can't resolve the vigiles npm package, so the typed-spec compile path
isn't available there. Previously init told them to run `npm install` (which
wouldn't help). Now it points at the real paths:

- the deferred-compile notice and the adopted next-step both detect a missing
  package.json and recommend `npx vigiles lint` (verifies with zero install) plus
  `npm init -y && npm i -D vigiles` to enable specs.

(Dropped the post-init lint "aha" idea — a freshly-adopted file is guidance-only
with no marks to verify, so it would show nothing until `/strengthen` runs.)

Test: init on a no-package.json repo guides to lint, not a bare npm install.

* docs: add scripts/demo.sh — self-contained asciinema demo of the Lint layer

5-star onboarding pass (3/N). A zero-dependency, reproducible script for recording
an asciinema cast / GIF: it builds a throwaway repo whose CLAUDE.md points (via
inline marks) at a file that moved and a script that was renamed, then runs
`vigiles lint` to show the ✗/✓. One command to record:

  asciinema rec --command "bash scripts/demo.sh"

Tested end-to-end (real ✗ "File not found" / "Script not found" output).
Consistency spot-check: public docs carry no stale adopt/compile language and all
README-linked guides open with a README uplink.

* docs(readme): final 5-star pass — tighten spec blurb, wire demo GIF slot

5-star onboarding pass (4/4).

- Tighten the above-fold spec/adopt/eject paragraph (the new FAQ now carries the
  "do I write TS / will it touch my files" detail), and point new readers to it.
- Wire a DEMO GIF slot at the prime "see it work" spot with the exact
  asciinema/agg recording command (scripts/demo.sh) + the embed snippet.

README rendered content ≈152 non-blank lines (well within the ~200 cap); all
internal doc links resolve; command refs pass the self-command-refs dogfood.

* docs(handoff): record the 5-star onboarding-polish pass

* docs(readme): add the demo GIF — vigiles lint catching stale references

Animated terminal of `vigiles lint` flagging a moved file + a renamed script, at
the README's prime "see it work" spot. The output lines are verbatim from the real
CLI (nothing fabricated). Generated headlessly with Pillow — no recorder needed —
so it's reproducible: `python3 scripts/make-demo-gif.py` (generator committed);
scripts/demo.sh remains for a live asciinema recording.

* docs(rules): sync rule docs with this session's default changes

doc-per-rule drift caught auditing the session:
- prefer-compiled-hooks.md: default is now OFF (was documented "warn") — a
  recommendation that shouldn't fire unasked.
- require-skill-spec.md: drop the "deprecated" framing — it's the consistent
  require-<surface>-spec parallel (kept, default off), matching the code + matrix.

* fix(eject): scan the whole project tree for shared-spec references, matched by resolved path

The shared-spec guard scanned only the ejected file's directory, so a
multi-target spec emitting a secondary output in a subdirectory
(target: ["CLAUDE.md", "docs/AGENTS.md"]) was missed — eject would delete
a spec still feeding another compiled file. Walk the whole project tree
(skipping node_modules/.git/dist/build/coverage/.next/out) and match on the
resolved spec path, not the basename, so two unrelated CLAUDE.md.spec.ts in
different packages don't false-positive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-26 05:58:23 +07:00
..