Skip to content

Latest commit

 

History

History
1898 lines (1638 loc) · 223 KB

File metadata and controls

1898 lines (1638 loc) · 223 KB

Changelog

[Unreleased]

Added

  • .loom/scripts/gh-since.sh — read-only "new merged PRs / new closed issues since watermark N" helper (#1060). The Auditor's Guard-Decision Telemetry Review standing policy and ad-hoc WORK_LOG/changelog cross-referencing both need this exact query shape; composed by hand as a multi-line gh-cached + jq + shell-loop script, it always fell to the guard's slow structural path and was denied outright at the catastrophic worktree-write-confinement tier despite being 100% read-only (10 logged incidents 2026-08-06..2026-08-13). The new script consolidates every numeric comparison and jq filter inside the file itself (never on the Bash-tool command line), so its own invocation (.loom/scripts/gh-since.sh <last-pr> <last-issue>) has no guard-sensitive shell metacharacter and is registered under guards.readOnlyFastPathExtra in .loom/config.json — admitted at the guard's read-only fast path instead of ever reaching the buggy slow-path masking logic. .loom/scripts/tests/test-gh-since.sh covers both the script's own argument validation (no silent default watermark, rejects non-numeric/injection-shaped arguments) and a direct .loom/hooks/guard-destructive-generic.sh replay confirming the typical invocation is fast-path admitted while a deliberately dangerous chained variant still denies. CLAUDE.md documents the workaround (the vendored .loom/roles/auditor.md and .loom/docs/guard-hooks.md were left untouched — both are resynced wholesale and any local edit would not survive the next resync-installed.sh run).

  • anvil:paper ships regression fixtures for the underclaiming vs. bold-synthesis scoring split (#1048, Phase 2 of the #1046 decomposition). A new anvil/skills/paper/examples/ directory (paper's first) carries two hand-authored projects that report the same synthetic study, with the same evidence and the same bibliography, framed two ways: underclaiming-buried-lede/ is rigorous, fully sourced, and heavily qualified with its organizing idea demoted to the last paragraph of the Discussion — recorded review 33/44, advance: false, with a named underclaiming_buried_lede finding at blocker severity and full weight on rigor, evidence sufficiency, reproducibility, and citation hygiene; bold-synthesis-labeled/ stakes the synthesis claim in the title and first sentence with each contribution labeled demonstrated / derived / synthesis / conjecture — recorded review 43/44, advance: true, with no overclaiming deduction. The two bodies share a byte-identical Method-through-Experiments span, a byte-identical refs.bib, and a byte-identical ## Strongest claim brief section, so all ten points of separation sit in the framing dims (3, 4, 7, 9) that #1047 made symmetric. tests/test_paper_underclaiming_fixtures.py pins the recorded verdicts, the scorecard arithmetic, the evidence-dim parity, and the quoted-evidence verbatimness (via anvil/lib/evidence_check.py), so a future rubric edit that makes caution cheap or ambition costly fails a test instead of passing silently. Every organization, trace, measurement, and bibliography entry in the fixtures is synthetic — do not cite them.

  • anvil:paper names underclaiming/buried-contribution as an explicit failure mode (#1047, Phase 1 of the #1046 decomposition — guidance-layer only). rubric.md dims 3 (Clarity of contribution) and 9 (Rhetorical economy) gain a new §"Dim 3 / Dim 9 — overclaiming and underclaiming are symmetric failure modes": a technically correct, fully-sourced, heavily-qualified paper that buries its central idea after background and qualification now has a named failure mode symmetric to overclaiming, plus an "Ambition is not novelty inflation" carve-out so a labeled bold synthesis/framing/research-program claim ("we propose/organize/build/ argue/conjecture/test") is not penalized as overclaiming. paper-draft.md requires the brief to answer six strongest-claim questions (a new ## Strongest claim section, # TODO(operator)-marked when skipped) and requires the drafted opening to state that claim before the defensive literature apparatus. paper-review.md adds a named underclaiming/buried-lede finding evaluated via an inline cold-reader check modeled on (not delegated to) memo-comprehension.md's blind-read pattern — no new critic infrastructure. paper-litsearch.md gains "Component-novelty calibration" guidance so positioning against prior art calibrates ingredient-level novelty rather than collapsing a broader synthesis claim to its least-novel component. assets/example-brief.md gains a fully worked ## Strongest claim example. Prose/guidance-only; the sibling regression-fixture issue exercises these criteria against real content.

  • Plain-language-first writing framework, lib foundation (#1043, Phase 1 of 3 for epic #1042). New anvil/lib/snippets/plain_language.md codifies the 7-point framework ("the purpose of writing is to help a reader learn or decide, not to display expertise") — explain in ordinary language before naming a technical term, introduce jargon only when it earns its place, keep the plain explanation even after naming the term, allow more words when they teach, and use a cold-reader restatement test in review (worked example: anvil/skills/memo/commands/memo-comprehension.md). States legal/patent/spec terminology exceptions explicitly (claim language, normative keywords, venue-required vocabulary are exempt from the "prefer plain words" rule, not from the framework as a whole). anvil/lib/snippets/rubric.md gains a new §"Dim 9 — teaching-oriented length is not padding" clarifying that rhetorical economy penalizes padding and repetition, never patient explanation. New regression fixture tests/lib/fixtures/jargon_dense_unexplained.md plus test_zero_findings_on_jargon_dense_unexplained_fixture in tests/lib/test_rhetoric_lint.py document the boundary this snippet fills: the deterministic lint returns zero findings on polished, jargon-dense prose that nonetheless fails the plain-language test — that judgment is semantic, not something a regex scanner can make. Purely additive lib-level work; no skill's draft/review commands are wired to the new snippet yet (that rollout is #1044 and #1045).

  • Per-PR changelog discipline, enforced in the Builder → Judge cycle (#1037). The v0.11.0 cut found [Unreleased] covering ~28 items while the cycle had merged ~50 more feat/fix/security PRs with no entry — two whole new skills among them — all reconstructed by hand at release time. CLAUDE.md § "Changelog discipline" now states the contract: a feat/fix/security PR adds its own [Unreleased] entry and carries a CHANGELOG: line in the PR body (every other conventional-commit type is exempt), and the Judge verifies a CHANGELOG: yes claim against the diff — a false yes is a blocking finding, the same contradiction tier as a false TDD: yes. New scripts/check-changelog-entry.sh is the cheap deterministic pre-flight (exit 0 = entry or exemption, 1 = missing, 2 = could not check), usable live against a PR number or offline from a title + body + changed-files list. /repo:release's merged-work coverage check is explicitly retained as the backstop, not the mechanism.

Changed

  • anvil:report's report-promote step 6 now invokes ack.py's CLI shim instead of re-deriving the ack-file validation algorithm in prose (#1098). anvil/skills/report/lib/ack.py (the nine AckError.mode-value executable specification of the structured --ack-file acknowledgment contract) shipped fully tested but was never wired into report-promote.md — the command doc hand-described the identical YAML/hash checks for an agent to re-implement inline on every promotion, leaving two independent copies of a security-relevant release-gate contract free to drift. ack.py gains a python -m anvil.skills.report.lib.ack CLI entrypoint (the same uv run --project .anvil shim precedent anvil.lib.sidecar already uses in this same doc); step 6 now shells out to it as the primary path, with the nine failure modes preserved verbatim as a documented last-resort manual fallback for a session with no python/uv. No change to the acknowledgment contract itself (schema, validation rules, and operator-facing messages are unchanged) — only to which code path enforces it.

Fixed

  • anvil:memoir test suite: test_revise_never_fabricates_mapping no longer breaks on prose reflow (#1054). PR #1033 reworded the fabrication prohibition in memoir-revise.md; the wording survived intact but a hard line-wrap split the asserted phrase across two lines, so the test's raw-text assertIn failed even though the documented behavior was unchanged. The test now reads the doc through the file's existing whitespace-collapsing _flat() helper (already used at six other call sites against the same file) instead of raw _read(), matching the convention used elsewhere in this test module for phrase assertions against hard-wrapped command docs. No change to memoir-revise.md itself.

  • anvil/skills/deck/commands/deck-imagegen.md frontmatter now parses as valid YAML (#1084). The unquoted description: value contained a second : sequence (`imagery_policy: generative-eligible`), which yaml.safe_load misparsed as a nested mapping and raised ScannerError — the same class of bug fixed incidentally in ip-uspto-provisional-vision.md by PR #1085. New tests/lib/test_skill_doc_frontmatter_parses.py sweeps every anvil/skills/*/commands/*.md and anvil/skills/*/SKILL.md frontmatter block through yaml.safe_load, asserting each parses to a dict, so this class of bug can't ship silently again.

  • .loom/scripts/random-file.sh now actually excludes .loom/worktrees/ and .git/ when fd is not installed (#1112). The find-fallback path had two independent regex-anchoring bugs that silently defeated both its default excludes and its .gitignore handling: apply_exclusions() misrouted any dotted directory pattern (.git, .loom/worktrees) into the file-extension branch, producing a regex that could never match a find -type f result, and gitignore_to_regex()'s trailing-slash branch required a leading / that relative find output never has. Verified reproduction: a live worktree file surfaced as a "random" pick. The ~150-line hand-rolled glob-to-regex machinery (apply_exclusions, glob_to_regex, filter_by_gitignore, gitignore_to_regex, and find_with_glob's eval-based matching) is replaced with git ls-files --cached --others --exclude-standard (native .gitignore handling, zero regex conversion) plus a small bash-native glob matcher for DEFAULT_EXCLUDES / --exclude / --include; the fd path now lets fd honor .gitignore natively too (dropped --no-ignore-vcs) instead of re-implementing it via the same buggy post-filter.

[0.11.0] — 2026-08-13

Added

  • Corpus-provenance scope-widening check (#1033, Phase 1 of #888). A claim's cited provenance.md row must have matching-or-wider grammatical scope wherever the claim carries a universal quantifier, a superlative, or a negated existential; a narrower row is now a finding even when the underlying fact would otherwise classify VERIFIED/PARAPHRASE_OK. Adds the scope_overreach critical-flag type (§6) with calibration examples from #888's field evidence, and syncs memoir's rubric/SKILL/command restatements of the flag-type list (five flags → six). No schema change.

  • Structural-importance-announcement AI-tell class (#1026). Adds "load-bearing" as a new AI-tell word class alongside the existing self-flattering-adjective class in STYLE_GUIDE.template.md §3, wires it into anvil:paper's dim 7/dim 9 default AI-tell check, and rewords the literal "Is every paragraph load-bearing?" dim-9 boilerplate across datasheet, essay, installation, memo, paper, proposal, report, and slides rubrics so drafting agents no longer read the tell word immediately before writing.

  • Sidecar copy — a sanctioned binary/bulk-asset channel for driverless sessions (#1017). A consumer hook that blocks every Bash-channel write into the checkout (redirection, tee, cp, mv, sed -i — a worktree-isolation guard) left a manual/agent session with no sanctioned way to place bytes it did not itself compose: an agent's editing tool is text-only, so a prior version's unchanged figures/*.pdf carried forward between versions, a compiled main.pdf, or the raw stdout/stderr of a pdflatex compile pass destined for compile-log.txt had no code-enforced path — only ad hoc escape hatches like a shutil.copyfile one-off or a hand-typed "summary" standing in for real compiler bytes. Adds anvil.lib.sidecar.copy_bytes() (+ CLI shim python -m anvil.lib.sidecar copy <src> <dst> [--force] [--no-verify]), the byte-safe analog of the existing stage/commit text-sidecar shim (issue #645): copies a single file or a whole directory tree, staged into a same-parent leading-dot sibling and landed with one atomic Path.rename (the same stage-then-rename shape staged_sidecar uses for critic directories), with post-copy (size, sha256) byte-identity verification by default (SidecarCopyVerificationError, non-zero exit, staged copy left unrenamed for forensics — never lands an unverified copy). Refuses to overwrite an existing destination unless --force/overwrite=True is given, matching every other write primitive in the module. commands/paper-revise.md step 8 and commands/paper-audit.md step 4 now name this path explicitly in their non-Python-driver fallback guidance for figure carry-forward and compile-log capture, respectively. (Investigated and ruled out of scope: the deterministic detectors' --write-review sidecar writes — numeric_consistency.py, hyperlink_resolver.py, figure_content.py — already write their _review.json via their own Python write_review_dir(), which itself calls staged_sidecar(); that is already a code-enforced write, not an agent-typed one, so it needed no change here.)

  • Claim provenance — stable anchor identity for drifting corpus citations (#868). provenance.md rows (issue #597) cited their supporting corpus passage by a bare Source file + Line range, which is not a stable address: a mid-file edit of the corpus (an insertion, a reflow, an appended correction) silently shifts every row citing text below the edit point, and the citation still resolves — just to the wrong text. Only an exhaustive corpus audit that re-opens every range catches the drift, and only if it re-runs after the corpus changed; a spot-sampling reviewer reading plausible text at the stale range passes it. Canary-hit three times on walters-family-tree (a memoir project): a six-line insertion silently invalidated three rows of a terminal-AUDITED chapter, and the only fix that existed was line-count-neutral hand-editing discipline the framework neither enforced nor knew about. Adds anvil/lib/provenance_anchor.py: each provenance.md row may now carry an Anchor column — a short verbatim quoted snippet from the cited passage — which is the row's real, content-addressed identity; Line range is demoted to a hint. provenance_anchor.py check searches the WHOLE cited file (not just the hinted range) and classifies each row NO_ANCHOR / FILE_NOT_FOUND / NOT_FOUND / RESOLVED / DRIFTEDDRIFTED means the anchor text is verbatim-present elsewhere in the file, a distinct signal from a content MISMATCH/NOT_FOUND. provenance_anchor.py repoint mechanically rewrites only the Line range cell of DRIFTED rows, leaving Claim/Source file/Anchor/Notes and every non-drifted row untouched — explicitly not the "fabricating a source-line mapping" failure the drafter/reviser contract prohibits, since the anchor text itself already proves the citation is genuine. Both subcommands are purely advisory/mechanical (never gate, never score, exit 0 always), the same posture as evidence_drift.py (#857) and probe_freshness.py (#863) — see anvil/lib/snippets/provenance.md §"Relationship to #863" for the boundary between this (internal: the evidence never changed, its address did) and #863 (external: the evidence itself rotted). Documented framework-wide in anvil/lib/snippets/provenance.md (new §Section 4a "anchor-drift detection" + §Section 4b "mechanical repoint" + §Section 9 "corpus-editing expectations for consumers") since the contract is shared verbatim by memoir, essay, and paper; wired into memoir-audit's exhaustive corpus-audit sweep and memoir-revise's provenance carry-forward, paper-audit's corpus-audit sidecar and paper-revise's carry-forward, and essay-review's back-check (essay ships no audit command, so this is the only exhaustive drift check its provenance tier gets) and essay-revise's carry-forward. Backward compatible: a provenance.md row with no Anchor value (every pre-#868 row) reports NO_ANCHOR — never an error, never a false drift signal, never coerced or bulk-migrated; drift protection simply activates the next time a revise pass touches that row.

  • Audit contract — claim perishability (#863). The kind: tool_evidence contract conflated two materially different verifications: durable ones (BOM arithmetic, internal consistency — true forever unless the document changes) and perishable ones (a live URL, an HTTP status, a version pin, the SHA of a served artifact — true until the world moves). Both landed on disk as an indistinguishable VERIFIED, so a perishable claim's implicit expiry went unrecorded. The censusapi canary hit this one day apart on a partner-facing proposal: a clean 44-row audit on 2026-08-01 had two of its 41 verified claims false by 2026-08-02, and nothing on disk told the next pass to look. Three additions, all additive: a Probe model in anvil/lib/review_schema.py (target / method / observed / checked_at, plus optional claim, recheck_command, and a per-probe max_age_days) whose existence is the perishability marker — durable verifications record nothing, so there is no durable: true counterpart to retrofit; two carriers for it — Review.probes[] for schema-era critics and a standalone probes.json (ProbeLog) for the prose-era auditors that have not migrated off findings.md, unioned by the reader so the contract does not wait on the per-skill _review.json migration; and anvil/lib/probe_freshness.py, which walks every critic sibling across every version of a thread and returns a bounded re-probe checklist — STALE (older than its freshness budget) and NOT-REPROBED (verified against an earlier version and carried forward since). Documented framework-wide in anvil/lib/snippets/audit.md §"Perishable vs durable verifications"; proposal-audit (step 9b) now writes probes.json and re-probes carried-forward targets, and proposal-revise (step 6e) consumes the checklist under the issue-#749 "findings are leads, not evidence" discipline extended across time. Purely advisory, mirroring evidence_drift (#857): never gates, never scores, never a critical flag, CLI always exits 0. Backward compatible — a sibling with no probe record is reported as unknown_freshness (never a defect, never conflated with an explicit empty probes: [], which is the auditor declaring the artifact durable-only). Scoped to proposal for this initial landing, matching the #857 core-then-adoption rollout shape.

  • anvil:paper / anvil:report — BRIEF/refs evidence-drift advisory (#857). New framework primitive anvil/lib/evidence_drift.py detects when a thread's BRIEF.md or refs/** change AFTER the latest version was drafted/revised — the gap that let a censusapi thread reach AUDITED on refs later found contaminated, and a BRIEF.md retraction go unnoticed after a v1 draft, with no framework signal either time. paper-draft/paper-revise and report-draft/report-revise now record a BRIEF.md/refs/** mtime baseline into metadata.evidence_snapshot in the version dir's _progress.json at completion; paper-review/report-review compare the CURRENT thread-root mtimes against that baseline and, on drift, add a non-blocking "Evidence drift" note to verdict.md. Purely advisory, mirroring the NEVER-VISION-CHECKED posture: it never changes advance, dimension scores, or the terminal-state gate, and — unlike the pending-marker gate — is never routed through anvil/lib/convergence.py's CriticalFlag machinery, since there is nothing to resolve, only something to re-weigh. A thread with no recorded baseline (pre-#857, or no draft/revise pass since adoption) reports clean, never a false positive. Scoped to paper and report for this initial landing; adoption in the other ~11 skill orchestrators is deferred to follow-up issues, mirroring the #847 → #851 core-then- adoption rollout shape.

  • anvil:paper — pending-measurement placeholder gate (#842; Phase 1 of parent tracking issue #841). New framework primitive anvil/lib/pending_marker.py (fifth member of the deterministic-checks family alongside numeric_consistency.py, render_gate.py, marp_lint.py, revise_consistency.py) detects well-formed [PENDING <source>] / [PENDING: <source>] placeholders — a first-class convention for a load-bearing number that genuinely doesn't exist yet (a training run still running, a benchmark queued, a vendor quote not returned), documented in anvil/lib/snippets/pending_marker.md, with <!-- anvil-lint-disable: pending_marker --> suppression. A well-formed marker is scored as a known-incomplete disclosure, NOT a defect (no dimension penalty). It surfaces as a distinct, specially-resolved pending_dependency CriticalFlag — an additive schema type (no version bump) modeled on the no_go precedent, with its own priority tier in anvil/lib/convergence.py / anvil/lib/critics.py: visible in the aggregate as an outstanding dependency but never forcing Verdict.BLOCK and never deducting a dimension score. The READY/AUDITED terminal-state gate is enforced separately (via convergence.has_pending_dependency_flag / the CLI exit code), decoupled from the score/verdict path — so a reviser is never instructed to "resolve" an honest marker by fabricating the number (the fabrication failure mode #841 exists to close). Optional <thread>/BRIEF.md frontmatter pending_sources: (bare labels or {source, expected_by} mappings, parsed by anvil/lib/project_brief.py::resolve_pending_sources) lets a thread declare which sources it expects to resolve, surfaced as outstanding_sources / resolved_sources for reporting. anvil/lib/render_gate.py Check 6 carves out well-formed markers so the generic placeholder scan never double-flags them. Phase 1 wires this into anvil:paper (paper-review step 4g at review time with the terminal-state gate in step 7, paper-audit step 6b as the terminal gate, a paper-revise no-fabrication carve-out, plus a rubric.md "Outstanding dependencies (not critical flags)" section and scoring-guidance note); proposal/memo/report adoption is deferred to the Phase 2–5 sub-issues (#843–#846).

  • anvil:proposal / anvil:memo / anvil:report — pending-measurement placeholder gate adoption (#841, closing out the parent tracking issue's remaining phases). Wires the anvil/lib/pending_marker.py primitive (shipped for anvil:paper per the entry above) into the three remaining named skills, following the "adopting the convention in a skill" recipe in anvil/lib/snippets/pending_marker.md:

    • proposal-review step 4l / proposal-audit step 11b run the gate against proposal.tex (via --body, since the module's <slug>.md/ main.tex auto-detect doesn't match this skill's fixed filename); proposal-revise gets the no-fabrication carve-out; the READY/AUDITED terminal transition holds on an active marker.
    • memo-review step 4n runs the gate against <thread>.md (auto- detected); memo-revise gets the carve-out; since memo has no separate audit phase, the READY terminal transition is held by a distinct ready computation (separate from advance) documented in memo-review step 7 and memo.md's state table.
    • report-review step 4f / report-audit step 10b run the gate against report.md (via --body); report-revise gets the carve-out; report-promote additionally re-checks the gate before CUSTOMER-READY as defense-in-depth alongside the machine-checkable AUDITED precondition. Each skill's rubric.md gains the "Pending-measurement markers do not incur a dimension penalty" scoring-guidance note and an "Outstanding dependencies (not critical flags)" section mirroring paper/rubric.md. No lib or schema changes — this is skill-wiring only, reusing the distinct pending_dependency CriticalFlag type and terminal-gate contract shipped in the paper Phase 1.
  • memo-review: version-drift check (#746). New step 4m invokes anvil/skills/memo/lib/version_drift.py (pure-stdlib, deterministic) to compare <thread>.{N}/ against <thread>.{N-1}/ (and, when available, <thread>.{N-2}/) on seven mechanical metrics — word count, bold span count, bold-word share, hedge-marker count, meta-commentary count, mean sentence length, exhibit count. A finding fires only when a densification metric (bold-word share, hedge-marker count, meta-commentary count) increased in BOTH of the last two version transitions — a single-transition increase stays observational. Every finding is fixed at severity: major, scope: reduce and is echoed directly into comments.md (the load-bearing path, since memo-revise's default --scope important reads comments.md, not _summary.md); the check never sets critical_flag. Closes the sensor gap the anvil#241 dim-9 countervailing-pressure fix could not close on its own — no phase in the lifecycle could previously see a cross-version trend, so a memo that densifies a few points every revision looks, at every single review, like a static style choice.

  • New skill: anvil:memoir (#740). Chaptered narrative nonfiction reconstructed from a private evidentiary corpus (family memoirs, oral histories, biography-from-archive, interview-grounded long-form profiles) — the first artifact-class skill to compose dual-corpus claim provenance (#597) and dual voice tiers (#598) active at once in one document, chapter-thread-native (one thread per chapter, assembled via anvil:project-book), audit-mandatory with AUDITED as the terminal state (no shortcut to READY). Ships the skeleton: SKILL.md, rubric.md (9-dim /44, anvil-memoir-v1, sourcing fidelity as the dominant dim 1 at weight 7, ≥39 audit-grade advance threshold), six lifecycle commands (memoir, memoir-draft, memoir-review, memoir-audit, memoir-revise, memoir-figures), templates (BRIEF.md.example + a LaTeX chapter template with \famphoto/\fullphoto/\marginphoto photo-placement macro stubs for anvil:project-photos), ArtifactType.MEMOIR registration, 5 regenerated lifecycle agents, and a minimal synthetic worked example (grani-memoir-mini). memoir-audit is the first command to implement the exhaustive kind: tool_evidence corpus-audit critic (anvil/lib/snippets/provenance.md §Section 4) as a <thread>.{N}.corpus-audit/ sibling, coexisting with a general .audit/ sibling. Deferred to follow-up issues: cross-chapter consistency checking, a structured facts-register companion input, and a full nitas-mama dogfood worked example.

  • anvil:proposalcost_basis knob for non-hardware proposals (#840). The proposal template, rubric, and audit assumed a priced, vendor-sourced hardware BOM unconditionally (the Gossamer LAN worked example's shape), leaving a partnership/integration proposal two bad options: fabricate hardware-shaped line items, or omit mandated sections and take a structural hit — the first being outright dangerous in an outward-facing document. Adds a cost_basis: quoted | estimated | none BRIEF frontmatter knob (default quoted, byte-identical to prior behavior), mirroring the recommendation_target precedent: load_cost_basis() in the skill's lib/project_brief.py (lenient, closed-set, never raises); templates/proposal.tex.j2 section 7 branches on the resolved value (estimated keeps the three priced tables but estimate-labels every caption; none drops the BOM requirement for a short "Cost Basis" section deferring commercial terms to Open Decisions); rubric.md gains a "Dim 6 — cost_basis calibration" section so an unsourced estimate in an estimated thread isn't scored as a sourceability defect; and proposal-audit step 7 skips the vendor-quote back-check under none, substituting an estimate-basis check under estimated.

  • anvil:proposal--polish operator-directed revision flag (#862). proposal-revise's step-4 combined-advance pre-check correctly refused to revise an already-passing version, but its parenthetical recommended manual state surgery as the workaround — deleting a critic verdict sibling (destroying immutable evidence) or hand-editing _progress.json (forging state the framework derives). Adopts the --polish "<reason>" contract from anvil/lib/snippets/directed_revision.md (the memo/primer precedent): it bypasses the step-4 pre-check only, requires a non-empty reason, records metadata.revision_mode / metadata.revise_force_reason as an audit trail, composes with the existing --scope severity filter, and grants no inherited credit on the next critic pass. proposal moves from "pending" to "adopted" in the shared snippet.

  • anvil:project-bookchapter_filename resolves as a per-thread {slug} template (#864). build.chapter_filename was one fixed, project-wide bare filename joined verbatim onto every thread's resolved version dir, which cannot express anvil:memoir's slug-echo body contract (<slug>.tex, "never chapter.tex" per #295) — so every memoir thread in a book build failed the chapter-source check, staged a placeholder, and warned "resolved version has no chapter.tex" (canary: the 15-thread walters-family-tree project, which had shipped a consumer-side staging script as the workaround). Two composed changes: collect.py::resolve_chapter_filename substitutes a literal {slug} token with the thread's own slug before locating the source (a token .replace(), deliberately not str.format — a filename carrying unbalanced braces must never raise mid-build, and a template with no token passes through unchanged, so every pre-existing config is byte-identical); and config.py::BookConfig.chapter_filename_for() picks the effective per-document template, an explicit BRIEF value winning project-wide, else a slug-echo artifact type (SLUG_ECHO_ARTIFACT_TYPES, today just memoir) defaulting to {slug}.tex.

  • anvil:memoir — at-cap iteration semantics and the BRIEF override in the BLOCKED notice (#869). memoir-revise's iteration-cap check was a two-line gesture ("at cap → report BLOCKED") that never defined "at cap", never mentioned the paired BRIEF override exists, and carried the sibling skills' off-by-one parenthetical. A canary chapter landed AUDITED at iteration 4/4 — healthy, terminal, and indistinguishable in the reports from a thread one blocked pass from refusal. Step 3 now documents the full cap resolution order (BRIEF paired override → _progress.json fallback → DEFAULT_MAX_ITERATIONS), the explicit N + 1 > effective_max_iterations predicate, and a refuse/warn/proceed table at the ceiling — the combined-verdict pre-check runs first, so a clean terminus at iteration == cap is a normal AUDITED terminus, never "capped". Adds the BLOCKED-notice contract ported from memo-revise (state line, trajectory, override pointer, prior rationale echoed verbatim when an elevated cap is already active) plus an explicit "never raises the cap itself" non-goal, and mirrors the resolved max_iterations + iteration_cap_rationale into every version dir's _progress.json.

  • anvil:ip-search — prior-art search skill (#969; opt-in pre-search step code-enforced in #981; wired into the ip skills' positioning critic in #975). Derives queries from a thread's BRIEF.md §3 inventive-feature inventory, queries PatentsView / USPTO Open Data via stdlib urllib, and writes one <thread>/prior-art/<slug>.md per reference in the frontmatter shape the ip skills already parse. A drafting aid, never an attorney clearance search.

  • anvil:diff — local side-by-side prose diff viewer (#925, PR #931). A stdlib-only (http.server + difflib), word-level, read-only HTML diff between two anvil version dirs, a deslop origin/cleaned.txt pair, or two arbitrary files, with optional .review/ + rhetoric-lint overlays. Binds 127.0.0.1 only; never writes to any input path.

  • Codex CLI parity for the installer (#1010, #1014; cross-runtime parity + upgrade/uninstall ownership tests in #1023). The installer now emits Codex CLI skill registration alongside the Claude shim, generates a consumer AGENTS.md entry point, and extends anvil:help's degraded-mode fallback to cover Codex consumers.

  • Rhetoric-lint / deslop growth: 18 AI-humanizer-corpus default rules (bucket a of #920, PR #927), a sentence_variance rhythm-uniformity rule kind (#928), a deterministic no-fabrication diff gate in the deslop revise loop (#929), a long-sentence-density default rule (#771), and emphasis-density / no-meta-commentary / no-warning-emoji default rules (#760).

  • anvil:memo additions: cold-reader comprehension critic (#753, PR #770), resolution modes + net-delta reporting for memo-revise (#748, PR #766), an evidence-grade rendering rule keeping grade tags out of reader prose (#751, PR #773), and a self-derived calibration mode for memo-redteam when no strongman exists (#915).

  • anvil:paper — deterministic candidates.bib syntax lint in litsearch (#1001).

  • anvil:deck additions: pdftotext-based text-layer-completeness gate (#983, PR #988); parity now reframes BRIEF-quarantined figures instead of promoting them to only_in_memo_economic (#917).

  • anvil:project-share additions: export.cover / cover_as durable cover-note knob (#776), a report-only dangling-citation lint (#758, PR #775), and a runnable lib/cli.py argparse entry point (#768).

Fixed

  • anvil:memoir — the final revision under max_iterations can now actually be validated (#933). memoir-revise's #869 cap predicate (N + 1 > effective_max_iterations) counted the free initial draft the same as every framework-mandated repair, so the LAST version a thread could afford could be written (e.g. a fabrication fix) but never scored — the write that fixed the defect was itself the write that exhausted the budget, and the thread BLOCKED with an unvalidated, possibly still-defective final version and no visibility into why. Canary-hit twice on the same walters-family-tree session (05-quebec as a near-miss, an operator-raised cap on 03-garrard reasoning "leave room for one further pass" — i.e. manually reserving a slot the lifecycle itself did not). max_iterations now counts revisions, not version dirs: the predicate becomes N > effective_max_iterations (the draft, <thread>.1/, is never chargeable), so a chapter gets its one free draft plus max_iterations revision opportunities, and the revision that fixes the last defect always has a slot to be critiqued. Step 3 also prints an explicit pre-write budget notice before writing the version that would consume the final slot — the budget consequence is stated before the exhausting write, not only after it in the BLOCKED notice — and the BLOCKED notice's state line now names what happened (<thread>.{N} is a final version written and unvalidatable under the current cap) rather than a bare "hit the cap", so it reads distinctly from the healthy "converged, slot(s) unspent" AUDITED terminus, which never reaches this notice at all. memoir.md's Iter column and SKILL.md's override contract are updated to match (revisions consumed, not the raw version-dir count). Map-only repairs still consume a revision slot exactly as before (#869) — only whether the draft itself counts changed. The same version-vs-revision off-by-one is duplicated skill-locally across memo/paper/report/deck/slides/ip-uspto/ ip-uspto-provisional/installation/proposal/datasheet; this fix is scoped to memoir only (issue #933's canary), left as a known follow-up for the other skills.

  • Deck theme: invisible ask-slide pagination + zebra-striped tables (#906). deck-vision rasterized a 13-slide deck at 96 DPI and found two systematic anvil-deck.css defects. First, the pagination glyph (section::after) is styled var(--anvil-muted) (#6b6b6b) with no override for dark-background _class slides — measured contrast against the #1f4e7a accent background was ~1.79:1, well under WCAG's 3:1 floor for incidental text and visually absent in the render, on the ask slide (the one slide most likely to be referenced by number in a follow-up email) and the section-divider slide. Second, tables rendered with alternating-row zebra shading inherited from the imported Marp default theme, contradicting the stylesheet's own comment ("Tables — clean, no shading on alternating rows (chartjunk)"); section.ask already defeated this locally for ask-slide tables, but ordinary (light-background) slides had no equivalent reset. Adds section.ask::after and section.section::after overrides (rgba(255, 255, 255, 0.75), ~5.7:1 against the accent background) and a theme-wide section table tbody tr:nth-child(even) transparent reset.

  • slide-content-overflow no longer charges standalone-image alt text as body prose (#905). The lint's standalone-image branch was gated on len(raw_line.strip()) < 250 (anvil/lib/marp_lint.py), so a long, accessible ![alt text](path) string could push the line past the threshold and fall through to the body-paragraph branch — where the alt characters, never rendered on the slide, were double-charged as rendered prose (per-70-char text rate) on top of the image's own cost, penalizing decks for writing better accessibility text. Rekeys the branch on line structure — reusing the existing _STANDALONE_FIGURE_RE anchored whole-line image pattern (already load-bearing for figure-italic-supporting-line-too-long) instead of a length heuristic — so a standalone image line is recognized as an image regardless of alt-text length. Adds a regression test asserting identical estimated cost for slides differing only in alt-text length (40 vs. 400 chars).

  • essay-review no longer calibrates voice fidelity against the artifact under review (#890). A voice.corpus glob naturally points at the consumer's published archive — correct when drafting a new thread, circular when reviewing a revision of an already-published one, because the thread's own prior published form sits inside its own calibration base for dim 2 (Voice fidelity, weight 7 — the rubric's heaviest dimension). Flagged independently by four consecutive reviews in one session, each working around it by hand with the same unrecorded caveat — and on a two-note corpus, hand-excluding the self-match left a single exemplar with no framework signal that the base had thinned. anvil/lib/project_brief.py::resolve_voice_docs gains an optional exclude_self_slug kwarg: when the reviewing command passes the thread's own slug, its published form is dropped from the resolved corpus via two unioned sources — automatic inference (a matched filename stem, after stripping one optional leading YYYY-MM-DD- date prefix, exactly equals the slug) and an optional declared voice_corpus_exclude on the thread's documents: entry (the escape hatch for a publish-path shape the automatic rule cannot infer, e.g. a title-cased or nested filename — same scalar-or-list shape as spec_ref / code_ref). essay-review step 4 now passes exclude_self_slug=<thread> unconditionally and records the exclusion in _summary.md.voice_grounding.corpus_excluded (path + reason per dropped path), plus a corpus_thin: true note when fewer than 2 exemplars remain after exclusion — auditable instead of silent, per the four reviewers' hand-written caveat. None (the default) is a complete no-op for every other resolve_voice_docs caller (memo, report, memoir, essay-draft).

  • Shared loader for hyphenated skill lib/ packages, replacing seven copy-pasted importlib incantations (#879). Four utility skills' commands/*.md (project-scout, project-book, project-photos, help) told the operator to "Load the skill lib" and then showed a bare orchestrate.run(...) call with no import shown — silently assuming a working import already happened. That undersold two real footguns: the lib modules use relative imports among themselves (orchestrate.py does from .cluster import ...), so the directory must be loaded as a package, not just added to sys.path; and skill directory names like project-scout are not valid Python identifiers, so anvil.skills.project-scout.lib.orchestrate is a syntax error even though anvil itself is an importable package. Every vendored test suite's _<skill>_skill_lib.py helper (project-scout, project-migrate, project-book, project-photos, project-share, help, and rubric-rebackport's _skill_lib.py) independently re-derived the same ModuleSpec(is_package=True) + manual sys.modules registration trick, each maintaining its own hand-written "dependency-safe load order" list. Adds anvil/lib/skill_lib_loader.py (load_skill_lib_package / import_skill_lib_module / load_skill_lib): registers a skill's lib/ directory under a synthetic, collision-safe package name (project-scout -> project_scout_lib) and then lets ordinary import machinery resolve any requested submodule's relative imports — no dependency-order list to keep in sync, and no risk of the stub-then-exec-out-of-order failure mode that breaks graphs like project-migrate's adopt_family.py doing from .adopt_vn import _FOREIGN_TAG_SUFFIX_RE. The four affected commands/*.md now show the actual working load incantation; all seven vendored test helpers delegate to the shared helper instead of duplicating it. Companion to #877; the original repro (a consumer-install path that didn't exist) was already fixed by PR #763 — this closes the remaining invocation-ergonomics gap the curator's re-verification against main surfaced.

  • numeric_consistency's percent check no longer pairs unrelated numbers sharing a paragraph (#854). The X% of proportion check validated a percent claim against the NEAREST fraction shape in its same-paragraph (±1) window regardless of subject, so anvil/lib/numeric_consistency.py::_check_proportion_claim produced a recurring percent_mismatch false positive at the censusapi consumer: "An ink gate resolves 27.3% of readable cells" sharing a paragraph with "18/18 pages of the test enumeration district registered correctly" computed 18/18 = 100% != 27.3%, even though the two numbers describe different subjects (cells vs. pages). A percent claim now only pairs with a fraction that shares a lexical anchor — a content word present in both the claim's sentence and the fraction's sentence, via new _anchor_tokens/_sentence_span helpers — UNLESS some fraction in the window already numerically satisfies the claim within tolerance (that "prefer a matching pair over a mismatching one" behavior is unchanged and still checked first, regardless of subject). A window with only unrelated-subject fractions now silently skips instead of flagging, matching the module's documented FP-averse/conservative contract; a genuine same-subject mismatch across sentences still fires.

  • vocab_reminder's shipped-default word list now reachable in installed consumer repos (#800). anvil/lib/vocab_reminder.py's DEFAULT_WORD_LIST_PATH resolves relative to __file__ as <lib>/../templates/voice/vocab.words.txt — correct in the dev tree, but from the installed module's location (.anvil/anvil/lib/vocab_reminder.py) it resolved to .anvil/anvil/templates/voice/vocab.words.txt, a path install-anvil.sh never populated (Stage 5 copied only anvil/lib; Stage 7.9 scaffolds the same source file to a different, consumer-owned destination, .anvil/voice/VOCABULARY.words.txt, gated on essay/memo being selected). The documented "works out of the box" zero-config fallback was dead on arrival for every installed consumer, returning no words available to sample (exit 1) instead of a sample. Stage 5 now ships one narrow, unconditional copy — anvil/templates/voice/vocab.words.txt -> .anvil/anvil/templates/voice/vocab.words.txt — mirroring the "importable mirror, always refreshed" discipline already used for anvil/lib -> .anvil/anvil/lib, rather than fixing the path-resolution logic itself (the constant was correct once the file it expects actually exists at that path).

  • sidecar's .bak replace surface hardened (#885). Three narrow residuals surfaced during Judge review of PR #884, all in anvil/lib/sidecar.py's stage_replace / commit_replace / abort_replace / recover_interrupted_replace family. (1) stage_replace() unconditionally rmtreed an existing backup before its move-aside rename — including one that recover_interrupted_replace had deliberately preserved under its "never deletes content it cannot prove is preserved" invariant, which the consuming doc's very next prescribed action would then destroy silently. It now refuses with FileExistsError (naming the unpreserved files) when the backup holds content not proven present in final_dir. (2) The redundancy predicate _unpreserved_backup_entries() is content-aware rather than name-only: it recurses into subdirectories and compares each file's size + content hash against its final_dir counterpart, and both recover_interrupted_replace() and stage_replace() share the one predicate. (3) staged_sidecar() gains an allow_orphaned_backup=False default, applying the orphaned-backup guard stage_enter/commit_staged already carried; project-migrate/lib/adopt_review.py's _convert_one/_rescore_one, which legitimately manage their own same-named .bak move-aside around the call, opt in explicitly.

  • anvil:project-migrate renames foreign .md bodies and accepts --artifact-type on bare-shape migration (#878). anvil:essay names project-migrate as the supported path for migrating a consumer's post.md corpus, but bare-shape synthesis defaulted every thread to artifact_type: investment-memo and could never rename a body filename outside anvil's own historical fixed-name set — so a foreign pipeline that had independently converged on the anvil grammar (<slug>.<n>/post.md + <slug>.<n>.review/) planned the directory nesting correctly and zero body-file renames, the circular gap being that the declaration needed for the rename would have to come from the very BRIEF the run was synthesizing. build_plan() / orchestrate.run() now accept --artifact-type (validated against the two-tier #394 registry, PlanError pre-mutation on an unregistered value), applied to every synthesized documents: entry on a bare thread with no TODO marker. Independently, the planner detects a single consistent non-canonical .md body filename per bare thread and plans its rename to <slug>.md — reviewable in the dry-run plan, TODO-marked — leaving multiple distinct candidates unrenamed with an operator-facing ambiguity note.

  • render_gate glyph verification no longer counts glyphs inside LaTeX comments (#856). _verify_source_glyphs's source sweep counted every non-ASCII codepoint in .tex files, including characters used purely in comment-only section rules (%% ── 4. The Item Pool ──, box-drawing U+2500). Those glyphs never reach the PDF, so the censusapi consumer saw 541 source occurrences vs 0 rendered reported as a glyph failure — shape-identical to a real fontspec fallback silently dropping a character, and costing a manual adjudication pass. _strip_nonrendered_regions now takes a latex kwarg and strips unescaped %-to-end-of-line before the codepoint sweep, mirroring numeric_consistency's existing flavor-gated LaTeX comment mask (a bare % in markdown is a percent sign, not a comment opener); _verify_source_glyphs gates it on path.suffix == ".tex". Two regression fixtures prove comment-stripping doesn't swallow a real glyph drop.

  • render_gate placeholder patterns cover qualified [TBD …] and add [FIXME …] (#855). DEFAULT_PLACEHOLDER_PATTERNS and DEFAULT_MEMO_PLACEHOLDER_PATTERNS matched [TBD] only exactly, so qualified forms like [TBD: vendor quote] slipped past the placeholder gate silently, and there was no [FIXME …] bracket pattern at all (only the memo-side <!-- FIXME … --> comment form). Both TBD patterns now accept an optional trailing qualifier and both tuples gain the FIXME analog, with a doc comment at each tuple recording that [PENDING …] markers are deliberately excluded — handled by the dedicated anvil/lib/pending_marker.py gate (#842) — so a future contributor doesn't reintroduce the redundant pattern.

  • numeric_consistency no longer parses season/fiscal-year labels as fractions (#836). Labels like 2018/19 and 2018/2019 were matched by the bare slash-fraction pattern in _FRACTION_RES and fed to _extract_shapes as fraction evidence (a=2018, b=19), producing a bogus percent_mismatch whenever an unrelated "X% of" claim shared a paragraph — canary: the studio all-dogs-go-to-heaven memo thread, where a "2018/19 baseline" citation tripped the advisory on both memo.1 and memo.2, the standing workaround being a per-line lint-disable comment in the body. Adds _is_season_fiscal_year_label(), reusing the existing _YEAR_MIN/_YEAR_MAX calendar-year heuristic, scoped by identity check to _SLASH_FRACTION_RE only — the "of"/"out of" fraction pattern and _PAIR_RES are untouched, so 47 of 94 / 47/94 and non-year fractions like 12/45 keep working unchanged.

  • install-anvil.sh no longer copies dev-machine __pycache__ / *.pyc / .DS_Store cruft into consumer installs (#818). All four cp -R-based tree-copy helpers (copy_tree, replace_tree, copy_lib_preserving_overrides, copy_skill_body_excluding_lib) physically shipped whatever bytecode caches a dev checkout had accumulated from a normal pytest or pip install -e . run, making installs non-deterministic and risking bytecode compiled under the wrong Python version — the same bug class already fixed for project-share exports in #756. Adds a shared strip_pycache_artifacts helper (portable find -exec across BSD/GNU find) called at the end of all four copy sites, and excludes the same patterns from the override-detection hash/diff helpers (dir_hash, dir_hash_body_only, dirs_identical, dirs_identical_body_only) — otherwise a source tree carrying stray build artifacts would no longer hash identically to its (always cruft-free, post-strip) installed copy, and every untouched-since-install skill would spuriously read as consumer-modified on the very next run.

  • anvil:deckdeck-design's additive-ness gate resolved imagery_policy against the wrong BRIEF.md (#984). The deck project-org model nests two distinct files sharing the literal name BRIEF.md: a project-level BRIEF.md (frontmatter documents: list) and a thread-level BRIEF.md (frontmatter imagery_policy:). deck-design.md step 7b named its resolution source as bare "BRIEF.md" — the one call site, among four documenting the same resolution order, that omitted the <thread>/ qualifier — so a design-critic pass reasoning at the project root could read the project-level file, find no imagery_policy key, and silently fall through to the built-in deterministic-only default even when the thread's own BRIEF declared generative-eligible. Extracts the inline resolution block previously duplicated only inside imagegen.py::run_imagegen into a standalone, reusable resolve_effective_imagery_policy(thread_dir, portfolio_path, config_path=None) -> (policy, policy_source) in anvil/skills/deck/lib/imagegen.py; run_imagegen now delegates to it (behavior-preserving), and deck-design.md step 7b + the deck-audit.md "Generative-imagery audit" section are rewritten to name it as the single source of truth for the <thread>/BRIEF.md.anvil/config.json deck.imagegen.default_policy ∪ built-in-default resolution order (issue #547). imagegen_additive.py::gate_should_run's docstring cross-reference is updated to point at the new function. test_additive_gate_docs.py gains a regression assertion that deck-design.md names <thread>/BRIEF.md explicitly, and test_imagegen.py gains direct unit coverage for the extracted resolver, including a case that plants a sibling project-level BRIEF.md alongside the thread-level one to prove the resolver reads the correct file.

  • Installer fixes: version now reads from a root VERSION file, with machine-local install fields split into a gitignored sidecar (#896); *.egg-info/ appended to .anvil/.gitignore, covering uv sync's editable install (#880); __pycache__/*.pyc/.DS_Store cruft stripped from every tree-copy site (#818, PR #821); the deck_imagegen extra mirrored into the generated consumer pyproject (#953).

  • anvil:deck imagegen/lint fixes: speaker-notes imagery-prompt extraction stops at the first blank line (#955); the imagegen field-block terminator accepts multi-word bold markers (#966); figure-legibility glyph height scales by actual render DPI (#908); marp_lint standalone-image detection keys on line structure, not length (#907).

  • anvil:ip-uspto-provisional — the figurer no longer conflates numeral-drift with visual verification (#987).

  • Lib fixes: render_gate attributes overfull boxes to their \input'd source file (#967); provenance_anchor parses every claim table, not just the first (#934, PR #936); hyperlink/rhetoric-lint scans cover the whole body, not per line (#892); memo scoring tables tolerate markdown emphasis and error on zero-row scorecards (#916); the evidence-drift advisory's mtime comparison is tolerance-aware and its hand-recording fallback is dropped (#1021); the four vision critics gitignore their regenerable per-page PNGs (#738, PR #739).

Security

  • Pillow floor bumped to >=12.3.0, resolving 13 Dependabot alerts (#765).

  • Identifier scrub: real client/project identifiers in example files replaced with synthetic ones; the token-filename example now uses a placeholder address; operator home paths in spec example files genericized (#942, PR #943).

[0.10.1] — 2026-07-21

Summary

Two anvil:paper correctness fixes surfaced by the geode-fem canary: the reviser now applies the same author-voice guidance the drafter does and the reviewer ships a default self-flattering-AI-tell prose check, and the orchestrator now surfaces the previously-silent paper-vision critic so figures no longer reach terminal AUDITED without a visual-review recommendation.

Changed

  • anvil:paper orchestrator now recommends paper-vision (#731). The paper.md next-command table splits its READY/AUDITED rows by figure + .vision/-sibling presence: a thread with figures and no vision sibling is routed to paper-vision, and a NEVER-VISION-CHECKED anomaly is flagged — closing the gap where a figure-bearing paper could reach terminal AUDITED with its figures never visually inspected. Text-only and already-checked threads route byte-identically; the AUDITED definition and rubric contract are unchanged.

Fixed

  • voice.md now loads symmetrically in paper-revise (#732). The reviser — which rewrites prose across every version — now applies the same consumer .anvil/skills/paper/voice.md author-voice guidance that paper-draft already honored. Additionally, the reviewer's D7/D9 prose dimensions ship a default self-flattering / virtue-signaling AI-tell check (the "honest/candid/frank/uncomfortable X" adjective class), firing for every consumer without a hand-authored voice.md, with the semantic-work exception preserved. No rubric total/weight/threshold change (/44, D7=4, D9=4, ≥35).

[0.10.0] — 2026-07-15

Summary

Completes the anvil:spec artifact class (epic #697, all four phases) and adds anvil:help, an onboarding/orientation skill — bringing the catalog to 20 skills. Also lands a wave of consumer-install robustness fixes surfaced by the botho canary running anvil:spec against a real implementation (which found 3 real spec↔code divergences along the way).

Added

  • New utility skill anvil:help (#725). A strictly read-only orientation tool for consumers: it introspects the installed skill set (from .anvil/install-metadata.json, falling back to a .claude/skills/anvil-*/ scan) and prints a two-tier view — anvil:help overview (installed skills grouped artifact-vs-utility, the common lifecycle with its per-skill variations, a "start here" pointer) and anvil:help <skill> deep-dive (real command set, rubric total/threshold, thread layout). Describes only what is installed; writes nothing.
  • anvil:help installs unconditionally regardless of --skills= filtering (#728). A Stage 4 always-on allowlist in install-anvil.sh unions help into the selected set after the --skills= filter (dedup-safe, no-op on full installs, typos still error, and the carve-out does not widen to other utility skills) — so orientation is present precisely in the filtered installs where it is most needed.
  • anvil:spec worked example (#709), completing epic #697. A vendored, trimmed snapshot of botho's terminal-AUDITED botho-bridge-spec thread under anvil/skills/spec/examples/ — exercising the real code_ref three-way audit, the implementation-status register, and live % anvil-const: markers — plus an expected-thread.N/README.md structural-contract doc and a parse test. The anvil:spec class is now complete end-to-end: skeleton (#706), three-way verdict + register (#707), constant gate (#708), worked example (#709).
  • code_ref / spec_ref accept a list of paths/globs (#719). Multi-crate / non-contiguous implementation roots can be declared as a list, not only a single scalar glob.

Changed

  • Atomic-sidecar CLI invocations are consumer-reachable (#724). The skill command docs now invoke the sidecar shim via the uv run --project .anvil wrapper (dual-form Primary-tier headers), fixing the unreachable bare-python -m anvil.lib.sidecar gap in .anvil/-vendored consumer installs; the manual-mv manifest check is now count/ls-based (robust under restricted-stat sandboxes); and spec-audit gains a disposition_counts worked example.
  • anvil:spec audit & adoption guidance (#709). Documents the claim-driven large-tree sweep strategy (extract normative claims, then grep the resolved code_ref paths per-claim — the 405-file friction), the check_constant_consistency_multi dict[label→text] signature, and the _progress.json scaffolding prerequisite for adopted specs.

Fixed

  • Malformed code_ref / spec_ref surfaces as a major finding instead of silently degrading to tier-inactive (#718): a bad path resolves to missing=True, never a silent None.
  • Full installs prune upstream-removed / renamed skills (Stage 7.6, #720): stale skill directories from a prior install are cleaned up.
  • Audit commands cross-reference the findings.md harness-guard fallback (#717) across six audit command docs.

[0.9.0] — 2026-07-14

Changed

  • Skill rename: anvil:pubanvil:paper (#694). The research-paper skill was the least self-describing name in the catalog (pub reads as publish / public / pint); every sibling skill names its artifact. Hard rename in one PR, no deprecation alias: the skill directory (anvil/skills/pub/anvil/skills/paper/), all command names (pub-draftpaper-draft, pub-reviewpaper-review, pub-litsearchpaper-litsearch, …), the SKILL.md frontmatter/name (anvil:paper), the generated per-phase agent shims (anvil-pub-*anvil-paper-*), the installer's skill enumeration, and the README / CLAUDE.md skill tables all move to paper. Migration: switch --skills=pub--skills=paper on your next install (invoking --skills=pub now prints a clear redirect error). Existing thread directories are slug-named, not skill-named, so they are untouched. Two input-alias compatibility shims keep existing consumer data working: (1) a BRIEF.md documents: entry carrying the legacy artifact_type: pub still parses — it normalizes to the canonical ArtifactType.PAPER (input-only; nothing emits pub); (2) the rubric-rebackport / project-migrate catalogs recognize legacy pub.md bodies and artifact_type: pub BRIEF entries, mapping them to the current paper skill. The rubric_id literals stay frozen as anvil-pub-v1 / anvil-pub-v2 / anvil-pub-<venue>-v1 / anvil-pub-vision-v1: a rubric_id is a versioned identity already stamped onto existing consumer reviews, decoupled from the skill directory name — a rename does not bump the rubric version.

Added

  • New artifact class anvil:spec (Phase 1 skeleton) (#697/#706): normative technical specifications — protocol whitepapers, wire-format specs, consensus rules, API contracts — maintained truthfully against their implementation. Ships SKILL.md, a 9-dim /44 normative-correctness-dominant rubric (anvil-spec-v1; dim 1 Normative correctness at weight 7, ≥39 audit-grade advance threshold), six lifecycle commands (spec, spec-draft, spec-review, spec-audit, spec-revise, spec-figures), BRIEF + LaTeX-body templates, a README, and skeleton tests. Borrows anvil:report's / anvil:primer's lifecycle shape (parallel review+audit, source-of-truth
    • optional PDF) as a NEW skill — not a parameterization (per "skill identity = artifact identity"). Phase 1 of the #697 epic; the three-way audit verdict + implementation-status register (Phase 2 / #707), the deterministic cross-table constant-consistency gate (Phase 3 / #708), and the botho worked example (Phase 4 / #709) are deferred.
  • Optional code_ref companion input for spec threads: a BRIEF.md documents: entry may declare code_ref (a path/glob naming the implementation the spec normatively describes — commonly a glob over a multi-file source tree). The mirror image of primer's spec_ref, resolved project-root-first then consumer-root by the new anvil/lib/project_brief.py::resolve_code_ref / ResolvedCodeRef (shipped as a standalone mirror of resolve_spec_ref, not a generalized resolver — see the design note in project_brief.py). Activation follows the #428/#449 posture: declared+resolves → the spec↔implementation consistency tier is active; absent → silent-off + major finding; declared-but-missing → major finding, graceful (never a crash, never a false critical flag). ArtifactType.SPEC is registered as a skill-identity artifact type (18 registered / 11 skill-identity types).
  • anvil:spec three-way audit verdict + implementation-status register (Phase 2) (#697/#707): spec-audit now performs the full spec↔code_ref consistency sweep (every constant / struct / formula / validity-predicate claim) and, on a contradiction, emits ONE critical flag — implementation_contradicts_spec — carrying a mandatory three-way Disposition: (a) spec-wrong routes to the normal spec-revise path (fix the spec to match the code); (b) code-wrong emits a copy-pasteable operator-escalation block (quoted spec + code + a suggested consumer-repo issue) and blocks advance until the code is fixed or the operator overrides via spec-revise --override-code-wrong "<reason>" (non-empty rationale, modeled on memo's NO-GO --override-no-go) — spec-revise NEVER silently rewrites the spec toward a vestigial code path; (c) intentional-gap is register-suppressed when a matching ## Implementation status row exists, or flagged unregistered otherwise. The discrimination lives in command-doc + findings.md/ verdict.md conventions (a Disposition column + _summary.md.spec_consistency.disposition_counts) — NO review_schema.py change (single free-form flag type, deliberately not three types). Auditor discipline: never default an uncertain contradiction to spec-wrong; when uncertain, default to code-wrong (escalating a true spec-wrong costs one confirmation; silently spec-editing a true code-wrong recreates the botho near-miss). Adds the first-class ## Implementation status register (a live/target/status/tracking table in spec.template.tex + a SKILL.md contract — operator/drafter-authored, not auditor-generated); spec-review raises a major finding for an unregistered target-state claim (the prose-side twin of the audit's mechanical code-side check, with the division of labor documented in both command docs). Includes a four-fixture disposition-discrimination test (test_audit_disposition.py) covering spec-wrong / code-wrong / intentional-gap-registered / intentional-gap-UNREGISTERED (the near-miss shape), asserting a code-wrong case NEVER routes to a spec edit. No consumer-repo issue-filing automation — escalation is a human-actionable note.
  • anvil:spec deterministic constant-consistency gate (Phase 3) (#697/#708): a new skill-local anvil/skills/spec/lib/constant_consistency.py extracts marker-declared named constants (% anvil-const: name=… value=… [unit=…] — standalone or inline table-row-comment) and \newcommand definitions across the spec's multi-file LaTeX and flags same-name / different-value occurrences before the expensive content review, feeding the internal-consistency rubric dimension. Exact string-normalized comparison (a normative constant matches or is a defect — no numeric tolerance); unit-mismatch and malformed-declaration are separate lower-severity finding kinds; marker-driven only (no prose extraction in v1). Wired into spec-review as a step-3b pre-flight gate; the marker convention is documented for drafters in SKILL.md, spec-draft, and the template. Skill-local per "wait for the second consumer" — not promoted to anvil/lib/. (Only the Phase 4 botho worked example / #709 remains deferred, blocked on the upstream dogfood run.)
  • First-class operator-directed revision (--polish) (#691): the *-revise combined-verdict pre-check conflated "passed the advance threshold" with "nothing worth fixing" — once a thread advanced (and, for audit-bearing skills, the audit was clean) the command reported terminal and exited without writing, leaving operators to prompt-hack around the command to spend one more iteration on critic-enumerated minor improvements. memo's already-shipped --polish "<reason>" contract is now generalized into a shared snippet (anvil/lib/snippets/directed_revision.md) and adopted by anvil:primer as the second consumer: a required non-empty reason bypasses ONLY the pre-check early-exit (iteration cap + critic-completeness still apply), records an audit-trail-only revision_mode / revise_force_reason, grants no inherited credit (a fresh critic pair must re-score), and keeps the default no-flag behavior byte-identical. The remaining ten skills are an enumerated follow-up rollout.
  • Four deterministic PDF-render gates from the primer canary (#692): a \pandocbounded override capping images at 0.85\textheight so a tall figure plus a multi-line caption no longer overflows the page (shipped in a primer-local pandoc-defaults.yaml, co-located with the Figure N — / \captionsetup{labelformat=empty} caption convention); a source-driven glyph-verification check in render_gate.py (sweeps ALL non-ASCII codepoints — excluding Zs whitespace and non-rendered URL-target / HTML- comment regions — and asserts each survives in pdftotext output, catching silent font glyph drops by construction); an embedded-image assertion (![…]() ref count vs pdfimages -list); and check_mmdc_launchable() in render.py (a two-stage PATH-plus-probe that catches an mmdc whose puppeteer Chrome is absent, distinct from the presence-only check_mmdc_available()).
  • anvil:primer draft-time figure placement (#690): the drafter now emits ![…](exhibits/figN-slug.png) references and records a figure_plan in _progress.json; primer-figures is un-gated from the terminal AUDITED state and renders to exactly those paths, so the review/audit critics finally score figure captions and placement (rubric dims 3/7). Closes the structural gap where the figures phase could never satisfy its own "reference the image" guidance and PDFs shipped image-free.
  • anvil:primer Botho worked example (#693): a trimmed, provenance- stamped snapshot of the first real consumer run (botho#881 → PR #900) is vendored under anvil/skills/primer/examples/botho/ with a BRIEF-parse regression test, matching the shape six sibling skills already ship.
  • DataCite fallback in anvil/lib/cite.py (#696): on a Crossref 404/406 for a syntactically valid DOI, resolve() now falls back to api.datacite.org before failing, so Zenodo / software / dataset DOIs resolve to first-class BibRecords instead of being demoted to unverified leads. Stdlib-only, cache-first, cassette-tested; the paper-litsearch verified-or-dropped invariant is preserved (a DOI unknown to both registries still raises).

Fixed

  • anvil:report pandoc-defaults.yaml failed to parse under pandoc 3.x (#701): the page-number CSS used the include-in-header: - text: form, which a pandoc defaults file rejects (it expects file paths there). The content now threads through variables: header-includes, with a parse-contract test (mirroring the primer asset from #692).
  • Committed anvil/lib/*_schema.json had drifted from their pydantic sources (#714): regenerated review_schema.json (missing the NO_GO verdict from #559) and rubric_schema.json (stale pub rubric path from the #694 rename), and added a drift-guard test asserting the committed files match a fresh export_schema regeneration.
  • Internal test-pin hygiene: memo's TestSkillIdentityArtifactTypes pins were stale against the registered artifact-type set (missing ArtifactType.PRIMER) and failing on a clean tree (#702); a pre-existing rubric-rebackport catalog-drift guard failing on the unregistered primer rubric was backfilled alongside the spec registration (#706).

[0.8.1] — 2026-07-13

Summary

One new artifact class plus the upgrade-path reconciliation pass the 0.8.0 tractatus canary asked for: anvil:primer joins the catalog for long-form pedagogical explainers, and the installer now cleans up after what pre-0.8.0 installs left behind — stale unscoped agent files are pruned on a --skills= narrowing, and .anvil/ artifacts committed before the shipped .gitignore existed are detected with a remediation hint (opt-in --fix-tracked to untrack, never auto-committing).

Added

  • New artifact class anvil:primer (#686): long-form pedagogical explainers — a teach-from-intuition companion to a formal spec (the "Mechanics of MobileCoin" genre). Ships SKILL.md, a 9-dim /44 pedagogy-dominant rubric (anvil-primer-v1; dim 1 Pedagogical scaffolding / learnability at weight 7, general ≥35 advance threshold), six lifecycle commands (primer, primer-draft, primer-review, primer-audit, primer-revise, primer-figures), BRIEF + body templates, and skeleton tests. Borrows anvil:report's lifecycle shape (parallel review+audit, markdown source-of-truth + optional PDF via the shared anvil/lib/render.py pandoc-first path) as a NEW skill — not a report parameterization (per "skill identity = artifact identity").
  • Optional spec_ref companion input for primer threads: a BRIEF.md documents: key naming the formal sibling artifact a primer teaches alongside. Resolved project-root-first then consumer-root by the new anvil/lib/project_brief.py::resolve_spec_ref / ResolvedSpecRef. When active, primer-audit uses it as a spec-consistency oracle (the "Contradicts cited spec" critical flag) and primer-review runs the duplication check ("Duplicates formal spec section"); absent → the tier is silent/off and both critics record a major finding; declared-but- missing → the tier activates but degrades gracefully (a major finding, never a crash, never a false critical flag) — the standard #428/#449 activation contract.
  • primer registered as a skill-identity artifact_type (REGISTERED_ARTIFACT_TYPES + SKILL_IDENTITY_ARTIFACT_TYPES + ArtifactType.PRIMER); five anvil-primer-* lifecycle agents added to the generated agent registry (source registry 54 → 59). README skill table and CLAUDE.md skill counts bumped (12 artifact classes).
  • Tracked-artifact detection + --fix-tracked installer flag (#684): after writing .anvil/.gitignore (Stage 8.6), the installer now detects files already git-tracked under .anvil/ that the ignore patterns would suppress (the pre-0.8.0 committed-__pycache__ upgrade gap) and prints a warn + exact git rm -r --cached remediation hint. An explicit --fix-tracked performs the untracking itself — index-only, never a commit on the consumer's behalf, working-tree files untouched. Patterns are derived from the written .gitignore via git check-ignore (no second hard-coded list to drift); graceful no-op on non-git targets; --dry-run honest throughout.

Fixed

  • Stale agent files pruned on a --skills= narrowing (#685): a pre-0.8.0 unscoped install wrote all agent shims to .claude/agents/; the 0.8.0 scoped copy (#662/#675) never removed the now-unselected ones, so they kept registering in the consumer's agent picker. Stage 7.5 now prunes anvil-owned agent files whose agent_skill_for() resolution is a known skill outside the live selection — per-file removal only, non-anvil (loom-*) and unknown-prefix files untouched, no-op for full installs, --dry-run honored.

Deferred (per the #686 v1 scope guard)

  • The Botho "Botho from the Basics" worked example (examples/, dogfooded via Botho #881 once the shape lands), voice-grounding wiring, a consumer-pluggable figure-adapter registry, and the LaTeX/TikZ figure path — documented in the primer SKILL.md §Deferred.

[0.8.0] — 2026-07-13

Summary

The tractatus canary wave: anvil's first external install into a machine-checked Lean 4 papers repo (rjwalters/tractatus, --skills=pub) surfaced eleven friction reports in one day, and this release closes all of them. The arc is three-fold — (1) run the artifact, not just the PDF: pub-review gains a consumer-extensible external-artifact verification gate (build the companion proof repo, check quoted theorems against source) and multi-file \input/\include threads become first-class in review; (2) compile-contract truth: the pub compile cycle becomes a latexmk-style convergence loop instead of a fixed pass count, the render-gate input is named by its real entry-point-derived name (main.pdf), overfull boxes dedupe across multi-pass logs, and pub-audit's git-sync scope now stages the PDF it produces; (3) install ergonomics for strict-subset consumers: the agents copy is scoped by --skills=, .anvil/.gitignore ships in the box, the sidecar CLI runs warning-free, and adopted-in-place legacy threads get a --body override in both content detectors.

Added

  • External-artifact verification gate for pub-review (#663/#665): opt-in artifact_verify block in .anvil.json runs consumer-declared commands (e.g. lake build) before scoring; failures surface as fabrication-class critical flags through the unmodified Verdict.BLOCK machinery; broken declarations fail open as major findings. Byte-identical when undeclared; discovery mirrors the venue-overlay contract.
  • Multi-file LaTeX threads first-class in pub review (#654): \input/ \include resolution via the tex_includes resolver wired through review content-reads.
  • Sidecar CLI for driver-less staging (#652): python -m anvil.lib.sidecar stage|commit|cleanup — crash-safe critic-sibling writes without a Python driver.
  • --body path override for adopted-in-place threads (#670/#679): numeric_consistency and evidence_check accept an explicit body path (legacy entry points like paper.tex); default discovery byte-identical; the numeric sidecar records the override portfolio-relative.
  • numeric class option for anvil-paper.cls (#671/#680): swaps natbib authoryear → [numbers,sort&compress] for content-identical migration of numeric-cite papers (authoryear stays the default; compile-verified with plainnat across both modes); SKILL.md gains a "Migrating an existing paper" section elevating keep-original-class to a first-class path.
  • Installer ships .anvil/.gitignore (#674/#683): new Stage 8.6 writes a self-contained ignore file (__pycache__/, *.py[cod], .venv/) — skip-if-exists, dry-run honest; README documents per-worktree uv sync --project .anvil pre-sync and UV_LINK_MODE=copy.

Changed

  • Pub compile contract: convergence loop, not fixed count (#669/#678): pdflatex → bibtex → pdflatex ×2 floor, then rerun while LaTeX emits "Label(s) may have changed. Rerun" (max 5 passes, latexmk semantics); non-convergence at the cap is a critical flag; optional .aux-diff cross-check documented. pub-vision/pub-review/SKILL.md defer to pub-audit step 4 as the single source of truth.
  • pub-audit git-sync scope stages its compiled PDF (#672/#681): the staging target now explicitly includes <thread>.{N}/main.pdf alongside the audit sidecar (never the wholesale version dir); git_sync.md snippet documents the dual-role command shape for future skills.

Fixed

  • Installer agent copy scoped by --skills= (#662/#666): Stage 7.5 no longer copies all 54 agent files on a single-skill install (5 for --skills=pub); longest-prefix matching keeps ip-uspto / ip-uspto-provisional siblings separate; unknown-prefix agents still copy unconditionally.
  • Sidecar runpy RuntimeWarning silenced (#673/#682): anvil/lib/__init__.py re-exports sidecar primitives lazily (PEP 562), so every python -m anvil.lib.sidecar invocation is warning-free; public re-export contract preserved; zero doc changes.
  • Overfull-box over-count on multi-pass logs (#668/#677): render_gate dedupes by (line, amount, kind) — 18-for-6 canary inflation gone; line-less hits never collapse; verdicts unaffected.
  • Render-gate input named by its real artifact (#667/#676): pub-review step 4b and SKILL.md say main.pdf (the entry-point-derived name pub-audit actually produces), ending the spurious fail-open/fail-closed ambiguity.
  • README status line under version.sh management (#661/#675): README.md joins the managed version-file set (drift now caught by version.sh check); skill count dropped from prose; Skills table filled to all 17 (adds project-photos, project-book).
  • version.sh --tag stages CHANGELOG.md (#638/#641).
  • Consumer-path migration finish + doc-pin guard widening (#642); critic-doc consistency batch: sidecar fallback rollout (#658), findings.md scope (#664), stale counts/cross-refs (#659), orchestrator output-guard collisions (#651), fail-open uv-gated fallback (#650), rubric/manifest reconciliation (#649), tex_includes wiring for installation/proposal (#657).

[0.7.1] — 2026-07-07

Added

  • Per-skill install staleness tracking (#633/#635): the install manifest gains a skill_versions block parallel to skill_hashes; skipped-override warnings now report "last installed: vX, current: vY" and the baseline survives across runs.
  • Cross-file rubric-threshold consistency lint (#618/#631): framework test asserting rubric totals/thresholds agree across each skill's rubric, commands, and SKILL.md.
  • Blog-parity: figure-carried economic false-promotion suppression (#626): parity checks consult a figures/src CSV corpus so numbers carried by figures don't false-promote as ungrounded prose claims.

Changed

  • Voice-grounding scaffold moved under .anvil/voice/ (#617/#632): Stage 7.9 no longer writes STYLE_GUIDE.md / VOCABULARY.md / VOCABULARY.words.txt / VALUES.local.md to the consumer repo root; pre-existing root-level docs are preserved and suppress the new-location scaffold so upgrades never duplicate.

Fixed

  • Consumer-path resolution to .anvil/anvil/lib/ (#625, #634/#636): command specs, snippets, and marp/figures asset docs all reference the canonical post-#230 consumer path; render.py marp-config and mermaid-theme defaults are now __file__-relative (correct from both a source checkout and a consumer repo); figures/mermaid-theme.json gains installer override protection.
  • Marp stdin hang (#629): render helpers pass --no-stdin / DEVNULL so marp never blocks in non-TTY contexts.
  • Overflow lint accuracy (#627): the marp overflow charge is aspect-ratio- and CSS-flex-aware.
  • Mermaid v11 edge-label legibility (#628): the pinned theme forces dark edge-label text.
  • Deck prompt-journal tolerance (#630): the reader ignores unknown per-entry fields instead of failing.

[0.7.0] — 2026-07-03

Summary

Two arcs since v0.6.0: (1) the deck business-model wave — deck gains dim 10 Business-model & unit-economics credibility (/44 → /49, threshold ≥43), a dim-10-owning deck-economics adversarial critic, a fifth critical flag, an outline gate, a figure-legibility gate, and a unified overflow/fit gate; (2) the nitas-mama family-memoir enablement wave — two new utility skills (anvil:project-photos, anvil:project-book, catalog 15 → 17) plus two lib-level grounding contracts (subject voice tier, local-corpus claim provenance) that point anvil's voice and evidence machinery at a real person's spoken corpus and an on-disk ground-truth archive — and their skill-level adoption: essay and pub write and back-check per-version provenance.md claim maps, pub-audit runs the five-way corpus verification, and pub/report adopt the subject voice tier beyond the essay pilot. A blog-parity batch (opening-line em-dash lint scope, VOCABULARY.words.txt install scaffold, VALUES template schema completion) and assorted memo/ip/install hardening round it out.

Added

  • Essay adoption of the claim-provenance contract (#611/#614): essay-draft writes the provenance.md claim→source map before prose when corpus: is declared; essay-review back-checks 5–10 rows per pass (provenance_back_check _summary.md block) and gains conditional fabrication-class critical flags 9–13; essay-revise copies the map forward. Byte-identical when no corpus is declared.

  • Pub adoption of the claim-provenance contract — the audit pilot (#612/#615): pub-draft writes the map, pub-review back-checks, pub-revise copies forward, and pub-audit runs the exhaustive five-way verification against the on-disk corpus — a separate staged_sidecar for <thread>.{N}.corpus-audit/ with kind: tool_evidence findings, fabrication-class critical flags through the existing Verdict.BLOCK machinery, and a six-counter provenance_summary.

  • Pub + report adoption of the subject voice tier (#613/#616): drafters record per-subject metadata.subject_voice_exemplars; reviewers run the per-subject voice-fidelity pass with the quote-the-transcript deduction discipline (pub folds into dim 7, report into its existing dim 8 sub-step) with the conditional Misattribution critical flag (≥2 subjects); report-revise preserves subject one-liners. Rubric stamps unchanged.

  • NEW SKILL anvil:project-book (#596/#606): multi-thread book assembly — stages the .latest-resolved version of every chapter thread into a consumer-owned master LaTeX document (skill-local build: BRIEF block), two-pass compiles via compile_and_gate, and writes a per-thread BOOK_REPORT.md (state, score, audit, next command). Build-does-not-block-on-quality: EMPTY/missing threads get placeholder chapters, below-READY threads warn; marker-guarded blow-away rebuild; --dry-run.

  • NEW SKILL anvil:project-photos (#599/#603): read-only scanned-photo provenance manifest — human-authored numbering doc → deterministic manifest.json (original capture → stable name + archive item IDs + rotation hint + multi_item flag + missing_captures); byte-identical re-runs; image manipulation deliberately stays consumer-native.

  • Subject voice tier for the voice-grounding contract (#598/#604): voice.subjects[] (per-subject spoken corpus glob + optional voice_doc), independently activated from the author tier; resolve_subject_voice_docs mirror resolver; essay pilot consumer with conditional Misattribution critical flag 8 (voice-identity failures; substance verification belongs to the corpus-provenance contract).

  • Local-corpus claim provenance contract (#597/#605): top-level corpus: [paths] BRIEF field + resolve_corpus_dirs (directory-gated, project-first/consumer-fallback, missing-as-structured-entry) + anvil/lib/snippets/provenance.md — per-version claim→source map, five-way audit classification (VERIFIED / PARAPHRASE_OK / MISMATCH / NOT_FOUND / FABRICATED), five fabrication-class critical flags, <thread>.{N}.corpus-audit/ sibling naming. Skill adoption (pub/essay) tracked as follow-ons.

  • Deck business-model dimension (#550/#556, #557, #565, #566): dim 10 Business-model & unit-economics credibility (weight 5) — deck rubric /44 → /49, threshold ≥43; perspective substrate extended; deck-economics adversarial critic owns the dim; fifth critical flag for incoherent/absent business model.

  • Deck gates + imagery (#547/#585, #562/#574, #563/#571, #564/#570, #567, #568): proactive generative imagery with default_policy override + additive-ness gate; unified sizing-aware overflow/fit gate; figure legibility-at-display-size gate; JPEG/WebP → PNG transcode via optional [deck_imagegen] extra; deck-outline gate + reviser restructure authority; mmdc --scale 2 fix.

  • Memo hardening (#559/#583, #560/#573): NO-GO terminal state for honest thesis-failure; independent red-team adversarial critic sibling.

  • Rhetoric lint positional scope (#601/#608): scope: "first-line" rule attribute + no-opening-emdash default rule (28 rules); consumer density-tightening recipe (5/1000 id-collision replacement) documented.

  • Install scaffold: VOCABULARY.words.txt (#602/#607): Stage 7.9 ships the starter word list as the fourth voice-grounding pair (sibling-resolution convention); hint block documents python -m anvil.lib.vocab_reminder.

  • VALUES template schema completion (#600/#609): Substrate / Forming positions / Voice modes sections + Standing reference-scaffolding prompt — the concepts essay rubric dims 2/5 already score against.

  • version.sh release interface (#590/#591): list + bump <level> --tag subcommands per upstream Loom v0.10.4 release.md.

  • project_brief.py: audience: {primary, secondary} dict shape (#546/#588).

  • ip-uspto: audit/finalize render-gate backstop + 2pt ip-skill threshold adjustment (#572/#587).

Fixed

  • All four xelatex preambles load amsmath/amssymb — pandoc-emitted \text{} no longer fails render (#592/#594).
  • Installer auto-proceeds on non-TTY stdin (CI/agent installs) (#569).
  • Per-critic entry steps guarded against portfolio-wide staging sweeps (test armor, #593/#595).

[0.6.0] — 2026-06-13

Summary

v0.6.0 is the adoption-readiness release: the entire backlog surfaced by the v0.5.0 multi-repo adoption survey is closed (62 PRs, issues #424–#543). The arc is three-fold — (1) sphere parity: every gap that blocked the studio-sibling install (consumer git-sync hooks, report figure adapters + data-contract audit + customer-context store + audience-class switches, pub web-litsearch + interview bootstrap, deck imagegen backend, beamer→Marp theme porting, project-migrate foreign-grammar adoption); (2) house-engine absorption: anvil becomes the artifact engine for the whole repo family, adding anvil:essay (short-form voice-grounded essays/blog posts — the 11th artifact class) plus the reusable voice/persona grounding contract, deterministic numeric-consistency + rhetoric-lint pre-flight gates, and a quoted-evidence requirement for critic scores; (3) ip-uspto maturation: the new anvil:ip-uspto-provisional sibling grows a full lifecycle (pre-flight, claims-seed, figures, drawings-vision, inventorship-lite, audit, COUNSEL-READY finalize), provisional→non-provisional conversion linkage (§119(e) text, 12-month deadline, §112(a) coverage), and evidence-mined inventorship (git-mining → interview packets → determination synthesis) on the non-provisional side.

Catalog: 13 → 15 skills (anvil:essay, anvil:ip-uspto-provisional). Test count: 3956 → 5956 (+2000 across the release, 0 regressions). Three new skills (datasheet, ip-uspto-provisional, essay) now vendor worked examples.

Added

  • NEW SKILL anvil:ip-uspto-provisional (issues #433/#444 Phase 1; #502/#515/#516/#480 Phase 2). USPTO provisional applications — claims-optional, enablement-depth-first (anvil-ip-provisional-v1, 9-dim /45 ≥39, dim 1 §112(a) enablement depth weight 8, dim 9 Conversion readiness). Full lifecycle: draft/review/§112/prior-art/revise loop, a deterministic pre-flight gate (claims-optional shape, advisory §112 stub scan), an opt-in claim-seed critic (absence is never a finding), provisional-shaped figures + a drawings-vision critic (informal drawings valid; scores rubric dim 4, no 37 CFR 1.84 formality nits), an advisory inventorship-lite list check, an audit command, and a COUNSEL-READY finalize assembling the <thread>.counsel/ filing package. The conversion seed for anvil:ip-uspto.

  • anvil:ip-uspto provisional conversion linkage (issues #501/#519, #517/#523). A converts_provisional BRIEF block threads §119(e) priority-claim text into the spec/ADS at draft/finalize, a producer-side _filing.json captures the filing date, and a pure-stdlib conversion_deadline.py surfaces the 12-month bar (end-of-month-clamped, fail-loud). The ip-uspto-112 critic gains a converts_provisional-gated §112(a) disclosure-coverage check (re-runs the claim-support sweep against the provisional spec; advisory for counsel, critical-flag-eligible for an unsupported converted independent-claim limitation, dormant when absent).

  • anvil:ip-uspto evidence-mined inventorship (v1 + v2) (issues #445/#492, #493/#512, #511/#525). --evidence mines repo git history into inventorship_map.json + evidence.jsonl with Notes-only matrix citations; --interview generates per-inventor packets; --synthesize rolls completed packets into a determination FOR COUNSEL. Legal invariants held throughout — advisory attorney work product, never touches the attestation matrix, never adjudicates. inventorship_evidence.py promoted to anvil/lib/ (provisional is the second consumer).

  • anvil:ip-uspto opt-in findings-only critics (issues #434/#447, #446/#498). An adversarial critic (ip-uspto-adversary, all dims null) attacks the application via §103 combinations over supplied art + AAPA, design-arounds, and §112(a) enablement holes; an FTO triage critic (ip-uspto-fto, report-only, never flags) produces a triage-for-counsel relevance assessment (0–4 scale, NOT-AN-FTO-OPINION boilerplate, no clearance verdict).

  • Consumer-integration tier for anvil:report (issues #427/#443, #428/#449, #429/#452, #450/#455). Pluggable block-figure adapters (.anvil/config.json report.figure_adapters; shell-free subprocess contract); data-contract numerical audit (refs/data/manifest.json-activated, four-valued VERIFIED/UNVERIFIED/CONTRADICTED/NOT-IN-REFS + orthogonal STALE); cross-project customer-context store (customers/<slug>/context.yaml + append-only disclosures.jsonl, topics-to-avoid critical flag); audience-class house-style switches (commercial/defense/internal, defense DRAFT watermark, no anvil-shipped legal text).

  • Consumer entry paths for anvil:pub (issues #424/#437, #425/#451). Opt-in web_search BRIEF knob (live searches allowed, but every citation must resolve through anvil/lib/cite.py before entering refs.bib — unresolvable hits become "Web leads", never citations); interview-driven BRIEF bootstrap when <thread>/BRIEF.md is absent (--no-interview opt-out, TODO(operator) markers never fabricated).

  • anvil:deck imagegen reference backend + onboarding (issue #430/#441) and beamer→Marp brand-theme porting path (issue #431/#448): a stdlib deterministic placeholder backend exercising the real config→dispatch→journal path, an onboarding guide, and anvil/lib/marp/brand-theme-starter.css with a .sty→CSS mapping recipe.

  • anvil:project-migrate foreign-grammar adoption (issues #432/#439, #440/#457, #454/#506, #507/#513, #503/#520). --adopt-vn (vN report dirs), --adopt-family ({Project}.{Letter}.{N} letter families + declarative --tag-map), --adopt-review (honest unscored stub conversion of foreign review.md sidecars) with an operator-driven --rescore, and filename-driven enrollment of native provisional.tex threads. Scan-time refusal for leading-zero version-slot collisions (#458/#459).

  • NEW SKILL anvil:essay (issue #460). Eleventh artifact-class skill: short-form voice-grounded essays / blog posts (markdown-only <slug>.md body, 500–1500 words typical), grounded in the rjwalters.info adoption survey — its pre-anvil blog skill is strictly upgraded (monolithic review.md → critic sidecars; 6-dim /30 → 9-dim /44 anvil-essay-v1 with full #346 stamping; ≥35 general tier ≈ the consumer's 80% bar). Deliberately small command set (essay status orchestrator + essay-draft / essay-review / essay-revise; no figures, no audit, no PDF); the state machine ends at READY with a documented publish handoff contract (the report CUSTOMER-READY precedent — TSX conversion / registry / Cloudflare deploy stay consumer-native). Voice fidelity is the OWNED dim 2 at weight 7 (first heavy consumer of the #461 voice/persona grounding contract; corpus-quoted deductions mandatory; a missing voice: block is a major finding every pass, not a crash); dim 9 Rhetorical economy is load-bearing (absorbs the consumer's length-discipline dim, fed by the #463 rhetoric lint as advisory evidence). Gates: numeric_consistency --blocking (the #462 hook built for this skill — its first blocking consumer) and broken-link resolution are convergence-BLOCKING; rhetoric lint stays advisory per its #463 contract; example coherence ships as the blog-review step-2.5 LLM critical-flag prose check (detector deferred per #462 gate 1). Seven critical flags ported from the consumer's blog-review (anti-stance violation, out-of-standing claim, generic AI cadence, factual error, unattributed borrowing, example-coherence failure, numeric-consistency failure). essay registered as a skill-identity artifact_type; anvil-essay-{drafter,reviewer,reviser} agents generated.

  • Voice/persona grounding-docs contract (issue #461/#466, #468/#476). An optional top-level BRIEF voice: block (style_guide/vocabulary/values/corpus glob + rhetoric_rules), the voice_grounding.md snippet (drafter exemplar-quoting + reviewer corpus-quoted deductions), and a memo dim-8 calibration consumer. Absent = byte-identical.

  • Deterministic authoring pre-flight gates (issues #462/#465, #463/#467, #464/#474). A numeric-consistency gate (anvil/lib/numeric_consistency.py — claim-vs-claim spread/percent/multiplier validation with currency K/M/B/bn/mn/tn scale handling, advisory for memo/pub, blocking hook for essay); a rhetoric lint (anvil/lib/rhetoric_lint.py — 27 conservative AI-tell/anti-trope rules + consumer JSON rules, advisory memo_rhetoric_lint gate dimension); a quoted-evidence requirement (anvil/lib/evidence_check.py — every critic dimension justification must quote the reviewed body verbatim or mark by-absence; fabricated quote = MAJOR), piloted on memo and rolled out to all 10 main reviewers + 9 scored specialist critics.

  • anvil:ip-uspto evidence-mined inventorship v2 — interview packets (--interview) (issue #493, follow-up to #445). New opt-in --interview mode generates one structured interview packet per candidate inventor from the v1-mined artifacts (inventorship_map.json + evidence.jsonl) — the deterministic-templating half of the native consumer's proven /ip-inventorship v2 (the judgment-laden --synthesize rollup is a deferred follow-up, filed at loom:triage now that the packet markdown shape is frozen). New skill-local anvil/skills/ip-uspto/lib/inventorship_interview.py (mirrors v1's placement — no anvil/lib/ promotion; #480 is the trigger) ports the native verbatim statutory constants (STATUTORY_INTRO with the Burroughs Wellcome conception standard / MPEP §2138.04 / plain-English §115/§116/§256, the Q1–Q7 question block, CONFIDENTIAL_FOOTER, VENDORED_CODE_PROMPT), the 5-step bot-author resolution chain (operator-configurable bot pattern; the bot is never a §115 inventor), composite-label collapse (a label like 1(b)(iv-v) yields exactly ONE Q1–Q7 block), candidate-matching by email OR display-name, and a three-level sensitivity taxonomy — reusing v1's is_vendored_path / vendored_prefixes helpers rather than reimplementing. Legal invariants held byte-identical: packets are advisory-only attorney work product; they never touch the matrix, inventor columns, or TBD markers; evidence anchors are labelled memory-aids-only; there is no auto-adjudication. Packets land at <thread>/inventorship-evidence/interviews/{slug}.md; --interview consumes v1 outputs only (emits a "run --evidence first" notice when they are absent, writing nothing). Command prose extended with the I1–I6 mode section; Git-sync paragraph stages interviews/. New fixture pair + 30+ tests (packet structure, composite-label collapse, vendored/bot blocks, candidate matching, advisory-only invariants, graceful degradation, command-file structure).

  • NEW SKILL anvil:essay (issue #460). Eleventh artifact-class skill: short-form voice-grounded essays / blog posts (markdown-only <slug>.md body, 500–1500 words typical), grounded in the rjwalters.info adoption survey — its pre-anvil blog skill is strictly upgraded (monolithic review.md → critic sidecars; 6-dim /30 → 9-dim /44 anvil-essay-v1 with full #346 stamping; ≥35 general tier ≈ the consumer's 80% bar). Deliberately small command set (essay status orchestrator + essay-draft / essay-review / essay-revise; no figures, no audit, no PDF); the state machine ends at READY with a documented publish handoff contract (the report CUSTOMER-READY precedent — TSX conversion / registry / Cloudflare deploy stay consumer-native). Voice fidelity is the OWNED dim 2 at weight 7 (first heavy consumer of the #461 voice/persona grounding contract; corpus-quoted deductions mandatory; a missing voice: block is a major finding every pass, not a crash); dim 9 Rhetorical economy is load-bearing (absorbs the consumer's length-discipline dim, fed by the #463 rhetoric lint as advisory evidence). Gates: numeric_consistency --blocking (the #462 hook built for this skill — its first blocking consumer) and broken-link resolution are convergence-BLOCKING; rhetoric lint stays advisory per its #463 contract; example coherence ships as the blog-review step-2.5 LLM critical-flag prose check (detector deferred per #462 gate 1). Seven critical flags ported from the consumer's blog-review (anti-stance violation, out-of-standing claim, generic AI cadence, factual error, unattributed borrowing, example-coherence failure, numeric-consistency failure). essay registered as a skill-identity artifact_type; anvil-essay-{drafter,reviewer,reviser} agents generated. Deferred: worked example, rjwalters.info drafts/ migration, voice.rhetoric_rules wiring (#468), PDF path, example-coherence detector.

Changed

  • Lib promotion (issue #460, second-consumer trigger): hyperlink_resolver.py moves from anvil/skills/memo/lib/ to anvil/lib/ — the essay skill's blocking link gate is the second consumer of the #335 deterministic link-validation critic, firing the CLAUDE.md "wait for the second consumer before generalizing" rule (the #382/#393 promotion pattern). The memo path remains a back-compat re-export shim (both import paths and both python -m invocations keep working); tests move to tests/lib/test_hyperlink_resolver.py with shim-identity coverage. citation_coverage.py deliberately stays memo-local (assessed in its docstring: memo detects unlinked load-bearing claims; essay's coverage concern is unlinked named entities — judgment-side, carried as essay-review prose).
  • Opt-in per-phase git commit/sync hook (issues #426/#438, #436/#456, #528/#532, #537). Repo-level .anvil/config.json {"git":{"commit_per_phase","push"}} knob (default off) wired into all ~88 write-bearing commands; the verbose inline prose was later trimmed to the canonical git_sync.md short pointer while keeping each command's per-phase staging target inline.
  • anvil:memo framework-maintained .latest symlinks + runnable render-phase CLI (issues #472/#495, #473/#500). New skill-local CLIs make two previously-prose lifecycle steps actually runnable from agent-driven lifecycles (the PDF render and the .latest pointer maintenance), the latter preserving operator pins.
  • [deck.imagegen] config migrated .anvil/config.toml.anvil/config.json (issue #442/#485), consolidating on the single runtime config file introduced for the git-sync knob.
  • Installer hardening (issues #471/#489, #490/#514): scaffolds a consumer-owned starter theme so fresh memo renders aren't unstyled, and extends the hash-tracked skip-with-warning discipline to Stage 5 lib override assets (framework code always upgrades; only documented override assets are preserved).
  • anvil:rubric-rebackport KNOWN_RUBRICS catalog refresh + drift guard (issues #482/#484, #486/#509) for datasheet / ip-uspto-provisional / essay; datasheet registered in REGISTERED_ARTIFACT_TYPES.

Fixed

  • Memo --css silently dropped (issue #470/#483): restored the $for(css)$ loop in template.html (a #331 regression) so consumer theme overrides actually load.
  • Stale /40-era rubric prose aligned with the shipped /44 and /45 rubrics across ~44 files, including six behavior-affecting gate lines (issue #435/#453).
  • evidence_check machine-summary scorecards were vacuous (issue #536/#538): the JSON-only parser never matched the markdown-table scorecards the ip skills actually emit, so the quoted-evidence self-check silently checked nothing in real ip reviews — now falls back to the table parser. Plus ellipsis-elided quote acceptance + dash folding (#478/#494).
  • numeric_consistency diagnostics show the selected-ratio arithmetic and honor currency K/M/B + bn/mn/tn scale suffixes (issues #469/#488, #491/#508).
  • project-migrate leading-zero version-slot collisions now refuse at scan time in both adoption planners instead of silently collapsing or failing mid-apply (issue #458/#459).

[0.5.0] — 2026-06-11

Summary

v0.5.0 grows the catalog from 10 to 13 skills: anvil:datasheet (the ninth artifact class — customer-facing IC/component datasheets, LaTeX → PDF, 9-dim /44 rubric with mechanical pin-map/bus-width checkers), anvil:project-scout (read-only repo-wide discovery of anvil-adoptable document clusters), and anvil:project-share (per-project SHARE/ export of latest sources + refs + research). Alongside: the #295/#296 project-org nesting rollout to deck/slides/proposal, a per-skill-phase subagent registry, deck-side rubric_overrides + dimension waivers, the render-gate image-dimension lint, project-migrate enrollment growth (--enroll, starter-BRIEF synthesis, surgical BRIEF merge), three memo BRIEF knobs, and hardening fixes across the LaTeX classes, sidecar sweep, and rubric-rebackport.

Test count: 2962 → 3956 (+994 across the release, 0 regressions).

Added

  • NEW SKILL anvil:datasheet (issue #418, PR #421). Ninth artifact-class skill: customer-facing IC/component datasheets (LaTeX → PDF via xelatex). Distilled from hand-authoring two real preliminary datasheets at the studio canary (dual-SKU edge-AI part family). 9-dim /44 rubric (anvil-datasheet-v1, ≥39 customer-facing threshold, five critical flags) with dims for source-traceability, internal consistency, and measured-vs-projected provenance. Six commands; anvil-datasheet.cls template (two-column first page, provenance macros \est{}/\simval{}/\meas{}, \preliminarynotice); deterministic skill-local checkers pinmap_check.py (every pin assigned exactly once) and buswidth_check.py (N-bit field must cover its claimed range) with the canary failure cases pinned as unit tests. Audit-owned refs back-check (spec bundle in refs/ outranks the brief for numbers) and a revision-history READY-gate. 61 new tests incl. two real XeLaTeX compiles.

  • NEW SKILL anvil:project-scout (issue #407, PR #417). Strictly read-only repo-wide survey: walks a tree, classifies anvil-adoptable document clusters into an adoption taxonomy (foreign-grammar guard runs BEFORE any detect_shape delegation), reports the recommended next command per cluster.

  • NEW SKILL anvil:project-share (issue #396, PR #399). Collects each thread's .latest-resolved source + PDF + assets + per-thread refs and the shared research/ pool into one shareable, provenance-stamped SHARE/ folder. Marker-guarded blow-away rebuild; --dry-run / --zip flags.

  • anvil:project-migrate single-file enrollment --enroll (issue #406, PR #414) and starter-BRIEF synthesis for bare version-dir threads (issue #408, PR #411).

  • anvil:memo challenge/strategy genres + consumer artifact-type overlay tier (issue #394, PR #401), deterministic scorecard arithmetic validation (issue #392, PR #400), and consumer pandoc-template passthrough via BRIEF (issue #391, PR #398).

  • Per-skill-phase subagent registry (issue #377, PR #384). Generated .claude/agents registrations per skill phase via scripts/generate-anvil-agents.py; installer copies them into consumers.

  • ArtifactType extended with deck/slides/proposal + silent artifact_type default fixed (issue #386, PR #388).

  • Parallel-safe per-critic staging sweep cleanup_one_staging (issue #376, PR #381).

  • Monorepo coexistence contract test for the installer (issue #409, PR #410).

  • Render-gate image-dimension/aspect sanity check (issue #395). New advisory memo_image_dimensions dimension (check 5 of the now-six-dimension memo gate in anvil/lib/render_gate.py), catching runaway figure canvases before they ship — canary: technical-vision's silicon-ladder.png rendered at 16,622×5,652 px through two full versions (matplotlib bbox_inches="tight" inflated ~9× by a rogue artist on a transparent canvas; the framework's own anvil.mplstyle pins exactly that savefig.bbox: tight + savefig.transparent: True pair, so this is a safety net for a framework-shipped hazard). For every body-referenced image plus every PNG/JPEG under exhibits/ (union enumeration; URL/absolute refs skipped): (1) pixel ceiling — width or height > image_max_px (default 6000; per-thread overridable via the new gate(kind="memo", image_max_px=...) kwarg with the words_per_page coerce-or-silently-fallback validation, effective ceiling recorded in the message); (1b) extreme aspect — > 6:1 either orientation; (2) declared-vs-actual — actual dims diverging > 1.5× from a sibling src/<stem>.py's parseable figsize/dpi (or the PNG pHYs density) — silent skip when nothing declarative is parseable; (3) content-bbox vs canvas — content occupying < 25% of the canvas (the tight-bbox rogue-artist signature), behind a new [image_lint] optional extra (same Pillow+numpy set as [auto_shrink]; the bbox algorithm adapts the deck auto-shrink corner-sampling precedent into RGBA space so transparent canvases read as background) with graceful degradation via check_image_lint_deps_available() + IMAGE_LINT_REMEDIATION in anvil/lib/render.py, plus a per-image decode skip for over-ceiling canvases (decoding a 90-megapixel image is the hazard, not the cure). Checks 1/1b/2 are pure stdlib (struct PNG-IHDR / JPEG-SOFn header parsing — zero new base deps; the few-hundred-byte huge-IHDR fixture trick from test_imagegen.py::_make_tiny_png proves the path). All findings are warning severity — pass unaffected, no CriticalFlag, the memo_overfull_check advisory model; findings flow to _progress.json.render_gate.findings through the existing wiring. Suppression via <!-- anvil-lint-disable: memo_image_dimensions --> (suppressed hits surface as info findings). 44 new tests at tests/lib/test_render_gate_image_dims.py.

  • Deck-side per-thread rubric_overrides + dimension waivers (issue #393). The deck skill becomes the second consumer of the per-doc rubric_overrides: BRIEF contract (memo precedent #233/#265/#296), and the shared schema gains an explicit waiver shape for operator-directed content exclusions (canary: the acme-semi series-a-deck carried a no-team-content directive that structurally capped dim 6). Two halves: (1) calibrationsdim_N_calibration already parsed artifact-type-agnostically post-#382/#386; deck-review.md now loads them (new step 5e mirroring memo-review 4h) and attaches verbatim suffixes to owned-dim scoring.md justifications via the promoted helper. (2) waivers — net-new dim_N_waiver key family in anvil/lib/project_brief.py (rationale-as-value: the YAML value IS the mandatory rationale; unjustified waivers and waiver+calibration conflicts on the same dim are rejected at parse time with errors naming the keys). At verdict time a waived dim is removed from BOTH numerator and denominator: advance iff total_over_remaining >= 39 × (44 − waived_weight) / 44, compared as an exact fraction (dim 6 weight 4 waived → 390/11 ≈ 35.45/40). verdict.md states the normalized judgment explicitly and quotes each waiver rationale verbatim; _summary.md gains a top-level observational rubric_overrides block (ran / calibrations_applied / waivers / waived_weight). Boundaries: critical flags are NOT waivable (a dim-6 waiver does not suppress Fabricated team credentials); _meta.json stamping stays nominal per the #346 contract; memo-side waiver consumption is out of scope (the shared parser accepts waivers on any entry, but only deck-review consumes them in v0); specialist critics defer per the PR #363 split-init precedent — only the aggregator loads/applies overrides.

  • #295/#296 project-org nesting for anvil:deck / anvil:slides / anvil:proposal + project-migrate deck handling (issue #382). The three remaining rich-command-set artifact skills adopt the nested <project>/<slug>/<slug>.N/ artifact contract (SKILL.md trees rewritten; project-level BRIEF.md documented as the config locus per #296). The slug-echo body rename is deliberately scoped out for these skills — deck.md (Marp CLI) and proposal.tex (xelatex + anvil-proposal.cls) are consumed by external tooling; each SKILL.md carries an explicit body-filename note and the migration is directory nesting only (matching the studio hand-fix 2cf3f37). Proposal SKILL.md's four .anvil.json references are retired per #296 (iteration cap repointed at the project-BRIEF paired override; render-gate page_cap flagged TBD-on-BRIEF-grow mirroring memo's words_per_page prose). anvil:project-migrate now recognizes the nested-but-flat shape (thread root with BRIEF/refs/assets/.anvil.json as a sibling of flat version dirs), plans the <slug>.N<slug>/<slug>.N nesting move (critic siblings included), merges the deck paired max_iterations + iteration_cap_rationale override into the project BRIEF (only when it would survive the strict parser), dispatches mixed-grammar projects per thread, and stays idempotent on migrated mixed trees (the FULLY_MIGRATED body check now keys on skill-fixed bodies rather than slug-echo). New fixtures build_beacon_shaped_deck + build_mixed_memo_deck_proposal and four new mixed-shape test files.

Changed

  • Lib promotion (issue #393, second-consumer trigger): rubric_overrides_suffix.py moves from anvil/skills/memo/lib/ to anvil/lib/ — deck is the second consumer of the calibration-suffix primitive, firing the CLAUDE.md "wait for the second consumer before generalizing" rule (same shape as the #382 promotion of project_brief.py). The memo path remains a back-compat re-export shim; the memo behavioral corpus keeps running against the canonical implementation through it. The promoted module additionally grows the waiver-normalization helpers (normalized_advance_threshold / meets_normalized_threshold / waived_weight_for, exact-Fraction math).

  • Lib promotion (factoring A of issue #382): latest_resolution.py, project_discovery.py, project_brief.py, and cross_thread_refs.py move from anvil/skills/memo/lib/ to anvil/lib/ — deck/slides/proposal are the 2nd–4th consumers of the project-org primitives, firing the CLAUDE.md "wait for the second consumer before generalizing" rule. The memo paths remain as back-compat re-export shims (both anvil.skills.memo.lib.X and anvil.lib.X import paths work; rubric_overlays.py stays memo-local — overlay JSONs are memo-artifact-type-specific). anvil/lib/render_gate.py's theme-discovery tier drops its memo-lib sys.path injection in favor of plain anvil.lib sibling imports. New canonical-path test files under tests/lib/ pin shim identity + representative behavior for all four modules.

  • Docs requalification sweep: deck/slides/proposal command docs requalified to nested thread paths (issue #387, PR #389); deck filesystem-path references rewritten to Python-import form (issue #375, PR #380); memo-review step 4i aligned with post-#386 skill-identity artifact types (issue #390, PR #397).

Fixed

  • LaTeX classes: prefix-sensitive \ifx empty guards replaced with etoolbox \ifdefempty (issue #422, PR #423). Hardening across anvil-doc.cls, anvil-proposal.cls, anvil-installation.cls, anvil-datasheet.cls — the reported \long-prefix root cause does not reproduce on TeX Live 2026, but the \ifx idiom is fragile under older kernels/refactors; text-level regression tests added in all four consumers.
  • anvil:project-migrate surgical BRIEF merge for migrate-mode --apply (PR #416).
  • anvil:memo load_overlay hardened against path-separator artifact_type values (issue #403, PR #404) and memo/lib sibling imports qualified for the package-import contract (issue #379, PR #383).
  • anvil:rubric-rebackport --skill flag acts as force-set, not just filter (issue #374, PR #378).
  • Test scoping: check-2 silent-skip assertion moved past the check-3 deps breadcrumb (issue #412, PR #413).

Removed

  • Dead require_mmdc() convenience wrapper (issue #419, PR #420). Zero production callers since PR #72; check_mmdc_available() and MMDC_REMEDIATION untouched; the MMDC_REMEDIATION test coverage preserved.

[0.4.0] — 2026-06-08

Summary

v0.4.0 lands three structural primitives and a new bridge-tool skill, all driven by canary friction from the Studio's 47-thread iteration sweep. (1) Sidecar atomicity — new anvil/lib/sidecar.py primitive (staged_sidecar context manager + cleanup_stale_staging sweep) prevents partial critic-sibling writes from looking like complete sidecars to discover_critics. Piloted in memo-review (#350), then migrated across 38 critic-writing commands in 8 skills (#359). Closes the 13-partials-in-Studio canary surface. (2) Per-review rubric version stamping + /44 (and /45) migration — rubrics now write rubric_id / rubric_total / advance_threshold to _meta.json plus a top-level rubric block in _summary.md plus per-row rubric_id on _progress.json.metadata.score_history[] plus a "Rubric version transition" subsection in findings.md (#346 for memo + proposal, #357 for the remaining six skills). (3) NEW skill anvil:rubric-rebackport — operator-facing bridge tool for legacy unstamped reviews: --stamp-only infers rubric_id from a KNOWN_RUBRICS catalog; --rescore dispatches per-skill --rescore-mode <id> into a .review.rescore-<id>/ sidecar (#358, #366, #368). Skill count: 9 → 10.

Plus: memo BRIEF schema gains three new optional keys (latex_header_includes #347, recommendation_target #348, max_iterations+iteration_cap_rationale paired override #349); proposal mirrors recommendation_target: undecided on dim 8 Open decisions (#356); tier:maintenance cleanups (#360, #367, #369).

Test count: 2390 → 2962 (+572 across the release, 0 regressions).

Added

  • Sidecar directory atomic writes (issue #350). New framework primitive anvil/lib/sidecar.py ships staged_sidecar (context manager) + cleanup_stale_staging (startup sweep) + staging_path_for (pure-function path helper) + SidecarIncompleteError (manifest-violation exception) + STAGING_SUFFIX constant. The Studio canary surfaced 13 critic-sibling directories in partial state after mid-cycle interrupts: some of the expected files (verdict.md, scoring.md, comments.md, _summary.md, _meta.json, _progress.json) made it to disk, others did not — and the existing discover_critics contract in anvil/lib/critics.py treats any sibling dir with a canonical _review.json OR a complete legacy file triple as a valid critic, so partial-write shapes slipped through silently and were aggregated as under-specified sidecars. The directory-level analog of the existing file-level tmp + os.replace precedent (see anvil/lib/cite.py::_cache_write, anvil/skills/proposal/lib/synthesizer.py, anvil/skills/project-migrate/lib/apply.py, anvil/skills/deck/lib/imagegen.py) gives correctness at the directory boundary: the writer stages files into a leading-dot sibling .<slug>.<N>.<tag>.tmp/, then on clean completion the manifest is verified and the staging dir is atomically renamed to the final <slug>.<N>.<tag>/ name. On exception or missing-required-file, the staging dir is left in place for forensic inspection; the next cleanup_stale_staging sweep removes it. The leading-dot + .tmp shape is invisible to discover_critics by the existing critics.py:122-129 rejection paths (the leading-dot prefix fails the <slug>.<...> shape check; the inner tag review.tmp carries a dot which fails the no-dot-in-tag check — belt-and-suspenders rejection). API surface re-exported from anvil/lib/__init__.py so callers can from anvil.lib import staged_sidecar, cleanup_stale_staging. Pure-stdlib implementation (no new pyproject.toml deps — uses only os, pathlib, shutil, contextlib, logging). The crash-recovery contract in anvil/lib/snippets/progress.md now documents two shapes distinctly: version dir (single-canonical-output check — memo.md/deck.md/spec.tex present and parses) vs critic sidecar dir (staged-rename — final-named dir exists iff complete). Pilot consumer is anvil/skills/memo/commands/memo-review.md: step 1 invokes cleanup_stale_staging on the portfolio root before the resume check; step 3 opens the staged_sidecar context manager with the six-file memo-review manifest; step 11 closes the context after the final _progress.json write triggers the manifest verification + atomic rename. The other seven artifact skills (pub, deck, slides, report, ip-uspto, installation, proposal) plus memo-revise are a follow-on issue per CLAUDE.md §"Skill-local first, lib promotion later" — pilot on memo (11 of 13 studio partials were memo reviews) per the issue body's four-step migration plan; expand consumers in a separate PR once the primitive's behavior is canary-validated. Tests at tests/lib/test_sidecar.py (19 new tests) cover the happy path (clean rename), missing-required-file branch (no rename + staging preserved + SidecarIncompleteError), exception-in-body branch (no rename + staging preserved + exception propagates), pre-existing final dir refusal (FileExistsError on entry), pre-existing staging dir wipe-and-retry (forward-progress contract), cleanup_stale_staging discrimination (removes leading-dot *.tmp/ only; leaves final-named dirs, .git, plain files, and bare .tmp alone), idempotency, safe-on-nonexistent-parent / safe-on-file-parent paths, INFO-level logging shape, discovery isolation (a staging dir carrying a valid _review.json is NOT discovered), and a canary-replay test that synthesizes all 62 non-empty proper subsets of the six-file manifest, plants them as leading-dot staging dirs, and verifies (a) discover_critics finds zero of them, (b) cleanup_stale_staging removes all of them. Eleven additional doc-coverage guards at tests/skills/memo/test_memo_review_sidecar_atomicity_doc.py pin the lib API surface (module exists, exports the documented names, re-exported from anvil/lib/__init__.py) and the memo-review.md + progress.md prose (the wiring references staged_sidecar + cleanup_stale_staging, the six-file manifest appears verbatim, the atomic-rename contract is described, the issue number is cited for traceability, the snippet distinguishes version-dir from critic-sidecar shapes).

  • Sidecar primitive migration across 8 skills (issue #359). The staged_sidecar primitive shipped in #350 is now consumed by 38 critic-writing commands: memo siblings (6: memo-perspective, memo-figure-content, memo-hyperlinks, memo-citations, memo-image-accessibility, memo-revise --plan); pub (3); deck (7: review, audit, narrative, market, design, perspective, vision); slides (4); report (6 including report-promote's receipt-as-marker contract and report-claim-figure-grounding); ip-uspto (9, including ip-uspto-finalize's terminal .final/ package atomicity); installation (1); proposal (4 including proposal-synthesize's gaps.json half-write race closure). Each command adopts the same four-touch pattern: stale-staging sweep at the entry step, staged_sidecar(staging_path, expected_files=[...]) at the initialize step, close at the _progress.json update / commit step, Outputs-section atomicity note. Subdirectory outputs under vision sidecars (slides/, pages/, drawings/) stage inside the staging dir but are not validated by the flat required-files manifest. Opt-in --write-review commands (memo-figure-content / memo-hyperlinks / memo-citations / memo-image-accessibility / report-figure-content / report-claim-figure-grounding) gate both the sweep AND the staged_sidecar wrap on the flag. 185 new doc-coverage tests pin the migration (5 guards per command × 37 commands + ip-uspto-finalize). Backward-compatible with already-complete sidecars from prior versions.

  • NEW SKILL anvil:rubric-rebackport (issue #358). One-shot bridge tool — modeled on anvil:project-migrate (#297) — for stamping or rescoring legacy /40 reviews under the new per-review version stamping contract from #346. Skill count: 9 → 10.

    • CLI shape: anvil:rubric-rebackport <project-tree> [--legacy-rubric=<id>] [--stamp-only | --rescore] [--apply]. Dry-run is the default; mutation requires --apply.
    • --stamp-only mode: walks the project tree, finds reviews lacking rubric_id, infers the stamp from a KNOWN_RUBRICS catalog (skill, total) → id mapping, or from an operator-supplied --legacy-rubric=<id> flag. Writes the stamp into _meta.json + _progress.json score-history rows + _summary.md rubric block.
    • --rescore mode: dispatches the per-skill <skill>-review --rescore-mode <id> hook (see #368), which re-runs the reviewer into a .review.rescore-<id>/ sidecar instead of .review/, with rescore_state: "completed" + rescore_id: "<id>" in _meta.json.
    • Detect / plan / stamp / rescore / apply / verify / orchestrate split mirroring anvil:project-migrate's lib layout. Atomic per-review apply with snapshot/rollback; idempotent re-run. Dry-run-by-default invariant SHA-256-tested across all fixtures.
    • 80 unit + 16 end-to-end tests at anvil/skills/rubric-rebackport/tests/. Tests use an importlib-based unique package-name pattern in tests/_skill_lib.py to dodge cross-skill lib/ pytest cache collisions.
  • Per-review rubric version stamping for anvil:memo + anvil:proposal (issue #346). Reviewers now write rubric_id + rubric_total + advance_threshold into _meta.json; a top-level rubric block into _summary.md; per-row rubric_id onto _progress.json.metadata.score_history[]; and a ## Rubric version transition subsection into findings.md whenever the prior version was scored against a different rubric. The contract is additive — legacy reviews without stamps still pass verdict logic byte-identically; anvil/lib/critics.py and anvil/lib/convergence.py are unchanged. Snippet prose in anvil/lib/snippets/rubric.md, scorecard_kind.md, and progress.md is now genericized from hardcoded "/40" to per-skill total. New rubric IDs: anvil-memo-v2 (was implicit /40), anvil-proposal-v2. 25 doc-coverage tests at tests/skills/memo/test_memo_rubric_version_transition_doc.py + six JSON fixtures under tests/skills/memo/fixtures/rubric_version_transition/ + two new tests/lib/test_rubric.py cases pinning the /44 schema-validation contract.

  • /44 (+/45) rubric migration for the remaining 6 skills (issue #357). Skills pub, installation, slides, report, deck migrate from /40 to /44 via the addition of dim 9 Rhetorical economy (weight 4, paralleling the memo + proposal precedent). ip-uspto migrates to /45 via a skill-appropriate dim 9 Claim-spec correspondence (weight 5, preserving the flat-weight design). Customer-facing thresholds (report, deck) move to ≥39; general thresholds (pub, installation, slides) move to ≥35; ip-uspto to ≥39. Each skill's <skill>-review.md now writes the four stamping artifacts from #346 contract. New rubric IDs: anvil-pub-v2, anvil-deck-v2, anvil-slides-v2, anvil-report-v2, anvil-installation-v2, anvil-ip-uspto-v2. ip-uspto's scorecard_kind: machine-summary is preserved (distinct from the other 5 skills' human-verdict). 94 new doc-coverage tests across the 6 skills + 6 fixture directories + a /45-weight-validation lib test (test_rubric_total_45_weights_sum_to_45_validates).

  • KNOWN_RUBRICS catalog entries for /44 (+/45) skills (issue #366). Following the migration in #357, the anvil:rubric-rebackport planner's KNOWN_RUBRICS catalog gains 6 new entries pairing each skill to its post-migration (rubric_id, total, threshold) tuple. CURRENT_RUBRIC_BY_SKILL is repointed at the /44+ entries; legacy /40 rows are retained (guarded by test_legacy_40_rows_retained_for_stamp_only_inference). End-to-end auto-inference test TestPub44AutoInference exercises the full inventory → plan → resolve path: drop an unstamped /44-era pub review, run --stamp-only --apply without --legacy-rubric, verify the stamp resolves to anvil-pub-v2. SKILL.md heuristic-inference table updated with the new rows + an explanatory paragraph on /40 + /44 coexistence.

  • --rescore-mode <id> reviewer hook across all 8 skills (issue #368). Each <skill>-review.md command now accepts --rescore-mode <id> at the entry step. When set, the staged_sidecar staging path is re-derived from .review.tmp/ to .review.rescore-<id>.tmp/ (renamed to .review.rescore-<id>/), _meta.json adds rescore_state: "completed" + rescore_id: "<id>", and prior-review lookup re-targets version N (not N-1; rescore re-scores the version that's already final). Default-mode behavior is byte-identical to pre-#368 review. The anvil:rubric-rebackport --rescore --apply end-to-end flow is now functional (was a rescore_state: "scheduled" no-op placeholder until this PR). 9 commits, one per skill + test commit; 40 new doc-coverage guards (5 per skill) + a 16-subtest end-to-end test in anvil/skills/rubric-rebackport/tests/test_rubric_rebackport_rescore_end_to_end.py that walks inventory → plan → dispatch → assert rescore_state: "completed" lands across all 8 skills + legacy byte-identity assertions for the default-mode path. Deck's split-step variant (sidecar opens at step 3, _meta.json writes at step 4) handled with a back-reference to step 3's staging path. ip-uspto's scorecard_kind: machine-summary preservation verified by a dedicated test.

  • anvil:memo per-doc latex_header_includes BRIEF knob (issue #347). BriefDocument gains an optional latex_header_includes: | string field. When set on a doc whose render_engine: xelatex, the value is threaded through _progress.json.metadata.latex_header_includes_resolved into render_gate.gate(...) and on into _render_memo_source via pandoc's --include-in-header=<tempfile> flag, with tempfile cleanup in a finally block (covers both pandoc success and pandoc failure paths). Silent-with-record skip on non-xelatex engines mirrors the PR #326 precedent. The shipped memo template.tex already had the $for(header-includes)$ slot at L131-133; no template change needed. Memo-only scope; proposal / installation compile .tex directly without pandoc, report inherits memo's chain for free. 6 new schema tests + 5 new render-gate tests (including the safety-critical tempfile-cleanup test).

  • anvil:memo BRIEF recommendation_target: undecided propagation to dim 1 (issue #348). New load_recommendation_target helper in anvil/skills/memo/lib/project_brief.py resolves the structured key from the thread BRIEF (closed set: invest, pass, conditional, undecided; lenient contract — never raises, returns None on every absence path). memo-review step 4j loads the value, step 5 applies a new dim-1 calibration suffix when set to undecided (scoring decision-framework clarity instead of recommendation clarity), and step 9 writes the resolved value into a top-level _summary.md.recommendation_target_resolved audit-trail block. rubric.md gains a §"Dim 1 — recommendation_target: undecided calibration" section with a 5-point ladder (5/5 → 0/5). Calibration suffix order: base → overlay → recommendation_target: undecided → per-doc dim_1_calibration. Byte-identical verdict when the key is absent (additive contract). 25 unit tests + 14 doc-coverage tests.

  • anvil:memo iteration-cap unlock via BRIEF paired override (issue #349). BriefDocument gains paired max_iterations + iteration_cap_rationale fields (both required when override is present — the rationale is the audit trail). Cross-field validator enforces pairing; ≥4 floor; boolean rejection; whitespace-rationale normalization. Override is mirrored into _progress.json.metadata per version and into a new §"Per-document override contract" section in memo/SKILL.md. New BLOCKED notice subsection in memo-revise.md carries the override pointer (when none set) + verbatim rationale (when already set). Memo-only scope; deck retains its existing .anvil.json paired-override mechanism (already unblocking the beacon.4 studio canary). 11 schema tests + 15 doc-coverage tests + 1 worked-example regression test in BRIEF.rubric-overrides.md.example.

  • anvil:proposal BRIEF recommendation_target: undecided propagation to dim 8 (issue #356). Mirrors #348 into proposal — but calibrates dim 8 Open decisions instead of dim 1, because proposal dim 1 is "Intent / requirements clarity" (different semantic from memo dim 1's "Recommendation clarity"). New proposal-local anvil/skills/proposal/lib/project_brief.py with byte-identical load_recommendation_target helper (closed-set tuple matches memo's so a future lib promotion is mechanical when a third consumer adopts the pattern). recommendation_target recognized in proposal-draft step 3 and propagated into proposal-review step 4j load + step 5 dim-8 calibration sub-step + step 9b audit-trail block. Helper stays skill-local per "wait for the second consumer to promote" rule (guarded by test_project_brief_stays_skill_local). templates/BRIEF.md.example updated to include recommendation_target: undecided as the documented default for new proposals. 25 unit + 17 doc-coverage tests.

Changed

  • anvil:project-migrate test infrastructure adopts the unique package-name pattern (issue #367). The pytest cross-skill lib/ import-cache collision that anvil:rubric-rebackport works around via tests/_skill_lib.py is now also mitigated for anvil:project-migrate: new tests/_project_migrate_skill_lib.py loads each lib module under the unique package name project_migrate_lib.<mod> via importlib.util. Filename intentionally differs from rubric-rebackport's _skill_lib.py to avoid a secondary sys.modules['_skill_lib'] collision in combined-run pytest invocations. All 7 project-migrate test files updated to import from _project_migrate_skill_lib instead of from lib.<x> import <y>. conftest.py dropped the redundant _SKILL_ROOT sys.path insert. Combined-run AC verified in both orderings (123 tests each direction). Promotion of the unique-package-import helper to anvil/lib/ deferred until the third bridge tool arrives.

Fixed

  • anvil:memo # Body-filename helper (issue #295) banner relocated (issue #360). The banner had been visually stranded after #348 inserted the load_recommendation_target helper between it and its actual body_filename_for target. Moved to immediately above body_filename_for at L1949; the new #348 banner above load_recommendation_target is untouched. Five-line move; no behavior change; all existing tests pass byte-identically.

  • Dead _PROGRESS_HISTORY_RE regex removed from anvil:rubric-rebackport (issue #369). Defined at anvil/skills/rubric-rebackport/lib/plan.py:282-284 but never referenced anywhere in the codebase. Removed along with the now-orphan import re at L43 (no other consumer of re in plan.py). Six lines deleted; no behavior change.

[0.3.2] — 2026-06-05

Epic #328 complete + render-stack hardening + lib promotions. v0.3.2 closes the specialized-critics epic with six new critics spanning judgment enrichment, tool-evidence detection, and VLM scoring: figure + hyperlink enrichment guidance (rubric, Phase 1), hyperlink-resolver + citation-coverage detectors (Phase 2–3), and the deferred-phase trilogy figure-content (VLM, anvil/lib/), image-accessibility (memo), claim-figure-grounding (report) (Phases 4–6). Coordination conventions settled across all three Track B / Deferred critics: opt-in --write-review CLI, exit codes 0/1/2, session-lifetime content-hash VLM cache, critical-flag → BLOCK via aggregate + compute_verdict round-trip. Alongside: per-company theme primitive (#327), per-doc render_engine knob (#326), shared XeLaTeX render primitive (#312), parity_lint + marp_lint promoted to anvil/lib/, render-stack fixes (Unicode font fallback, weasyprint runtime check, lower memo words-per-page default), addressed (judgment-held) as a third convictions ledger Resolution type (#228), strongman files standardized as recognized source-of-truth materials (#330), vendored worked examples for installation + proposal (#325).

(Full bullet-by-bullet detail in the original [Unreleased] section below; moved verbatim into [0.3.2] at release time.)

Added

  • anvil:memo + anvil:report figure-content VLM critic (Epic #328 Phase 4, issue #340). New direct-lib module anvil/lib/figure_content.py ships a vision-language-model pass over every figure in a memo or report version directory, scoring three axes per figure: (1) on-brand palette match against anvil/lib/figures/palette.py (navy / muted grey / navy tint / rule grey — VLM is told the canonical hex list explicitly so it scores against the documented palette, not its prior on "what looks corporate"); (2) caption-grounding — does the caption accurately describe what the figure depicts?; (3) adjacency-grounding — does the figure support the surrounding prose claim? Direct-lib placement (NOT skill-local) per CLAUDE.md's "wait for the second consumer" rule — both memo and report consume from day one, so the two-consumer threshold is satisfied at ship. Two figure-discovery paths: (a) PDF page extraction via pdftoppm (subprocess; graceful-degrades when the tool is missing — top-level reason + zero findings, mirroring the check_*_available() family in anvil/lib/render.py); (b) <version_dir>/figures/ walk for direct PNG / JPG / WebP sources (SVG sources recorded as unverified via top-level reason — VLM consumes raster bytes). The critic composes the existing VisionCritic / VisionRubric substrate from anvil/lib/vision.py — no reimplementation. Per-figure VLM cost cap (default 1 call per figure per run, configurable via vlm_budget_per_figure=N). Session-lifetime in-process content-hash cache (sha256(figure_bytes) → VLM payload) so a 30-page customer report with a repeated header logo costs 1 VLM call total, not 30 — load-bearing for the report consumer specifically. Cache shipped inline in figure_content.py; promotion to anvil/lib/vision_cache.py is deferred until Phase 5 (image-accessibility, #341) reaches for the same shape (per the issue body coordination note). Critical-flag critical_figure_misrepresents_claim fires on VLM-detected caption-vs-figure contradiction; aggregator's compute_verdict short-circuits to Verdict.BLOCK. Per-figure scores are clamped to rubric range defensively; sub-threshold dims (score ≤ max/2) auto-emit a Finding even when the VLM omits a narrative entry (safety net for "low score, no narrative entry"). Per-figure scores roll up to the three rubric rows via per-dimension mean (rounded to nearest int via round-half-to-even) so the _review.json reads as a single 3-row scorecard, total /15. Two command docs land: anvil/skills/memo/commands/memo-figure-content.md and anvil/skills/report/commands/report-figure-content.md — both document the CLI shape (python -m anvil.lib.figure_content <version_dir> [--write-review], exit codes 0 clean / 1 findings / 2 invocation error), the three scoring axes, the severity ladder, the auto-discovery wiring, the failure-mode table, and the cross-skill consumer parity rationale (reports are figure-heavy; the cache amortizes the cost for them specifically). Auto-discovery via the existing <version_dir>.<tag>/ contract — .figure-content/ sibling is picked up by anvil/lib/critics.py::discover_critics with no aggregator changes (third consumer of the convention, after Phase 2's .hyperlinks/ and Phase 3's .citations/). Tests at tests/lib/test_figure_content.py (43 new tests) cover the full #340 AC: three-axis rubric shape (each /5, max_total=15), prompt construction (rubric dim names + critical-flag taxonomy + caption + adjacency + brand palette hex list), figure discovery (empty version dir, figures/ dir PNGs, SVG-skip, pdftoppm graceful-degrade), the three scoring axes (on-brand / caption / adjacency clean + finding-emitting cases), VLM cache deduplication (identical content → no second VLM call; distinct content → distinct calls; external cache lives across calls), critical-flag firing on contradiction + Verdict.BLOCK short-circuit, Review schema compliance (kind=vision + rendered_artifact required + round-trip through model_validate), auto-discovery + aggregate round-trip, per-figure score roll-up to rubric mean, CLI smoke (exit codes + --write-review sibling creation), top-level reasons (empty version dir + JSON shape), pdftoppm preflight, command-doc lock-in (both memo and report docs ship with the expected CLI / sibling-name / critical-flag invariants), and VLM budget cap. All VLM calls in tests are MOCKED via the callback= injection point on VisionCritic; no network access required for the test suite. The Phase 4 settle holds: zero schema changes in anvil/lib/review_schema.py, zero changes to existing skill rubrics, zero changes to existing command step structures. The Finding model lock-in test asserts the deferred experimental fields (action, target_anchor, proposed_content) have NOT crept in.
  • anvil:memo image-accessibility critic (Epic #328 Phase 5, issue #341). New tool-evidence + VLM-hybrid critic at anvil/skills/memo/lib/image_accessibility.py that scans the body markdown of a memo version dir for three classes of accessibility defect: (a) missing alt text (markdown ![](path) with empty alt, HTML <img> with no alt= attribute or alt=""), (b) inadequate alt text (literal placeholders image / figure / chart / img / picture / graphic / diagram; single-word generic prefixes without further subject — screenshot / photo / illustration / drawing / icon; sub-10-character non-descriptive alt), and (c) broken paths (image file does not exist at the resolved path; reuses memo_image_refs.lint_source for the determination — no duplicate filesystem walking per the issue body AC). Missing/inadequate-alt findings carry a VLM-generated candidate in Finding.suggested_fix when a callback is wired (via anvil/lib/vision.py); the CLI default is OFFLINE-safe and uses a deterministic template instead. Broken-path findings carry a closest-match suggestion via difflib.get_close_matches (mirrors the citation-coverage pattern from #337) when a similarly-named file exists nearby (propose_edit); otherwise emit a propose_removal template. VLM cost discipline: content-hash cache (in-process dict, session-lifetime) ensures identical image bytes hash to one VLM call; the cache shape mirrors the coordination point with Phase 4 (figure-content, #340) so a future anvil/lib/vision_cache.py promotion is a one-line import swap. No critical flags — a11y is advisory in v0 per the issue body settle; severity ladder is major (missing alt, broken path) / minor (inadequate alt). Kind decision (single sibling, single Kind): ships as ONE <version_dir>.image-accessibility/ sibling with kind=Kind.TOOL_EVIDENCE for the entire Review. Broken-path findings carry tool_calls=[] (no tool invocation); missing/inadequate-alt findings carry one ToolCall entry per finding describing the VLM invocation. The rejected alternative (two siblings — one TOOL_EVIDENCE, one VISION) was incompatible with the schema validator: Kind.VISION requires rendered_artifact set on the Review (one rendered artifact per Review), but the critic spans N images per memo (one per ref). The N-images-per-Review shape is a clean fit for TOOL_EVIDENCE and a structural mismatch for VISION. Suppression directive <!-- anvil-lint-disable: memo_image_accessibility_{missing_alt,inadequate_alt,broken_path} --> supported per-line, mirroring the existing memo_image_refs_exist discipline. New command doc anvil/skills/memo/commands/memo-image-accessibility.md documents the CLI entry-point (python -m anvil.skills.memo.lib.image_accessibility <version_dir> [--write-review]), the three classes with severity table, the kind-decision rationale, the VLM cost discipline, the auto-discovery contract, and the failure modes / re-run patterns. CLI shape mirrors the Phase 2 (hyperlink_resolver, #338) / Phase 3 (citation_coverage, #337) sibling contracts — opt-in write via --write-review, exit 0 clean / 1 on findings / 2 on invocation error. Skill-local first per CLAUDE.md — promotion to anvil/lib/ is deferred until pub / report / proposal reach for the same primitive. Zero anvil/lib/ changes, zero schema changes in anvil/lib/review_schema.py, zero changes to anvil/skills/memo/rubric.md (Phase 1 from #333 just touched dim 3 weighting), zero changes to memo-review.md step structure (4f/4g/4h/4i load-bearing from PR #332/#334). New tests at tests/skills/memo/test_image_accessibility.py (46 tests; VLM calls MOCKED) cover all eight acceptance criteria: valid alt passes, missing alt (markdown empty + HTML no-attribute + HTML empty-string) emits with VLM candidate, placeholder alts (image, figure, chart, screenshot alone vs with subject, case-insensitive) emit inadequate, sub-10-char alt emits inadequate, broken path no-nearby emits propose_removal, broken path with nearby (cp -r footgun shape + typo) emits propose_edit with closest-match, VLM cache deduplicates identical bytes (one callback invocation for two identical PNGs), the .image-accessibility/ sibling is auto-discovered by discover_critics + round-trips through load_review + aggregate without raising, the schema validator accepts every emitted Finding (all carry tool_calls), suppression directive (same-line + line-above), URL/absolute path skipping, graceful-degrade on VLM-callback-raises and missing body file, and CLI exit codes. Full test suite green — 1319 passed, 2 skipped (network smoke tests under tests/lib/).
  • anvil:report claim-figure-grounding critic (Epic #328 Track B Phase 6, issue #342). New deterministic tool_evidence-class critic at anvil/skills/report/lib/claim_figure_grounding.py that scans the body markdown of a report version dir for prose references to figures / tables / charts whose label is not present in the version directory's known-label roster. The critic recognizes three prose-detection shapes (prepositional: see Figure 3, as shown in Chart B, per Table 2, in Figure 3.1; subject-verb: Figure 3 illustrates …, Table 2 reports …, Chart 1 shows …; parenthetical: (Figure 3), (Table 2)) over three label-id forms (integer 3, dotted 3.1, single uppercase letter A). The label-validation step walks three ground-truth sources inside the version dir: LaTeX \label{<prefix>:<id>} macros in any .md / .tex / .latex file (recursive); markdown pandoc-style anchors {#<prefix>:<id>} on headings or images; and filenames in figures/ and exhibits/ subdirectories whose stem matches <prefix>[-_.]<id> (e.g. figure-3.png, fig_a.svg, table-2.md, chart-1-2.pdf). Missing references emit a major-severity Finding with a closest-match suggestion (integer distance ≤ 2 for numeric ids; difflib.get_close_matches with the 0.6 cutoff for alphabetic ids — mirroring the citation-coverage precedent from PR #337; class-restricted so suggestions never cross Figure / Table / Chart) or the "add or remove" fallback when no close candidate exists. Per the issue body's "Dedupe by (label_class, label_id)" requirement, multiple references to the same missing label produce one finding with the first reference's line + verbatim text anchoring the evidence span and an additional_references: N count in the rationale. A critical_promised_figure_missing flag fires on any missing reference (the issue body's "Critical flag on any non-existent reference" contract), short-circuiting the aggregator's verdict to Verdict.BLOCK. False-positive disciplines mirror citation-coverage: quoted material (blockquotes, fenced code, inline backticks) is silently skipped; same-line same-(class, id) duplicates collapse at detection time. New command doc anvil/skills/report/commands/report-claim-figure-grounding.md documents the CLI entry-point (python -m anvil.skills.report.lib.claim_figure_grounding <version_dir> [--write-review] [--body-filename <name>]), the three detection classes with examples, the three ground-truth roster sources, the closest-match suggestion algorithm, the dedupe contract, the auto-discovery wiring (<version_dir>.claim-figure-grounding/ sibling matches the <version_dir>.<tag>/ pattern that anvil/lib/critics.py::discover_critics recognizes without code changes), and the failure-mode / re-run patterns. The CLI shape is byte-faithful to the Phase 2 / 3 precedents (PRs #338 / #337): --write-review is opt-in; exit codes are 0 clean, 1 findings, 2 invocation error. Skill-local first per CLAUDE.md §"Skill-local first, lib promotion later" — promotion to anvil/lib/ is deferred until the sibling Phase 5 critic (#340 figure-content) lands and a figure_discovery promotion candidate emerges. Zero anvil/lib/ changes, zero schema changes in anvil/lib/review_schema.py, zero changes to other skills. New tests at tests/skills/report/test_claim_figure_grounding.py (59 tests) cover the module surface guards; positive cases for all three prose-detection shapes (prepositional see Figure N / as shown in Chart B / per Table 2 / in Figure 3.1; subject-verb Figure 3 illustrates / Table 2 reports / Chart 1 shows; parenthetical (Figure 3) / (Table 2)); positive cases for the abbreviation Fig. 3 form; grounded paths for all three ground-truth sources (LaTeX \label{fig:3} in body markdown; markdown {#fig:3} anchor on a heading; filename figure-3.png in figures/; filename fig-3.svg in exhibits/; dotted-id filename figure-3-1.png for Figure 3.1; filename chart-a.png for Chart A; filename table-2.md for Table 2); the closest-match suggestion algorithm (numeric distance 1 / 2 / 3, class-restricted, dotted-id); the dedupe contract (two references one finding; distinct labels separate findings; first-reference evidence-span anchoring); the false-positive disciplines (blockquoted / fenced-code / inline-backtick / same-line dupes); the label-roster discovery from each of the three sources (LaTeX macros; markdown anchors; figures/ filenames; exhibits/ filenames; dotted filename normalization; ignoring unrelated LaTeX prefixes \label{sec:intro}; ignoring non-label files like README.md in figures/); the critical-flag heuristic (fires on any missing; does not fire on clean; justification lists first three missing labels with +N more); the filesystem integration via scan_version_dir (with body filename override); the auto-discovery contract (verify aggregate merges findings from a .claim-figure-grounding/ sibling and the verdict short-circuits to BLOCK); the CLI entry-point shape (write-review opt-in; clean-scan-exits-zero; missing-dir-exits-two; grounded body exits zero); and a doc-coverage suite over the new command file. Full test suite green at 1332 passed, 2 skipped (network smoke tests under tests/lib/).
  • CLI entry-point convention for deferred-phase critics (Epic #328 Phase 4 + #341 + #342): python -m anvil.lib.<module> <version_dir> [--write-review]. Documented as the agreed coordination point with the parallel deferred-phase critics #341 (image-accessibility) and #342 (claim-figure-grounding) so all three share an invocation shape. Phase 4 is the first deferred-phase critic to ship and serves as the reference.
  • anvil:memo convictions ledger extended with addressed (judgment-held) Resolution type (#228). The changelog-embedded convictions ledger (the carry-forward record that lets the reviser remember why a prior reviewer's finding was declined or judgment-held across versions) gains a third Resolution type alongside the existing declined: addressed (judgment-held) — <judgment>. This covers the #226 structural defect inherited from the killed _convictions.md approach (#229) — the prior write trigger only captured declined rows, missing load-bearing convictions from canary threads where the reviser addressed a finding but the addressing judgment was structural (e.g., the $450B-as-anchor-not-TAM reframe, the 6-risks-is-enough scope limit, the bear/base/bull sufficiency call). The new type lives in changelog.md (always written regardless of verdict.advance), inheriting the #225 fix for free — no dead-letter risk. Single-file scope: anvil/skills/memo/commands/memo-revise.md — CLI invariants bullet now names both declined and addressed (judgment-held) as ledger types the scope filter must not drop; step 7 always-include carries forward both types to the next reviser; step 9 changelog example adds rows for both with a three-type vocabulary definition; the Notes / Convictions ledger section explains both write triggers and the "load-bearing framing decisions only" usage guidance. Falsifiability contract documented in commit history (3-week observation window; kill-switch A — disuse — if grep returns empty; kill-switch B — ineffective — if marker used but judgment re-raised without reviser acknowledgment).
  • anvil:memo citation-coverage critic CLI aligned with hyperlink_resolver (#338) sibling (PR #337 judge follow-up). Refactored _cli_main in anvil/skills/memo/lib/citation_coverage.py so the two Track B detectors feel interchangeable from a consumer / CI perspective: writes to <version_dir>.citations/ are opt-in via a new --write-review flag (default invocation prints the CoverageResult.to_json() payload to stdout with no filesystem side effects), and the exit code is non-zero when findings exist (0 clean, 1 on any unhooked claim or broken citation, 2 on invocation error). Matches the byte-faithful contract documented in PR #338's _cli_main. Doc update to anvil/skills/memo/commands/memo-citations.md reflects the new shape; new tests pin (a) --write-review opt-in semantics, (b) no-write default, (c) clean-scan-exits-zero, (d) missing-dir-exits-two, (e) the multi-claim-one-cite line-level-hook behavior (a line carrying two claims with only one \cite{} suppresses BOTH — documenting the line-level hook granularity design note), and (f) lock-in cases for Table 2 reports the breakdown and see page 12 for details as structural self-references that must not fire as numeric claims.
  • anvil:memo citation-coverage critic (Epic #328 Track B Phase 3, issue #336). New deterministic tool_evidence-class critic at anvil/skills/memo/lib/citation_coverage.py that scans the body markdown of a memo version dir for two failure modes: (a) load-bearing claims that are unhooked — numeric ($2.3B, 42 %, 12 ms, Q3 2024), named-author (Smith (2023), Karpathy's 2024 talk), quantitative summary (we found that…, the median was…), and date-pinned events (On March 5, 2025,); and (b) broken \cite{key} / [@key] markers where key is not present in any discovered refs source (per-thread refs/ + portfolio research/ via refs_resolver.resolve_refs_dirs, plus the version dir's own refs.bib). Broken keys carry a closest-match suggestion via difflib.get_close_matches (stdlib alternative; cite.py does not ship a Levenshtein helper). The critic emits a typed Review (kind=tool_evidence) into the <version_dir>.citations/ sibling, auto-discovered by anvil/lib/critics.py::aggregate with no aggregator change. False-positive discipline is deliberately conservative — version numbers in technical context (Python 3.12, Node.js 22.0.0), self-referencing structural markers (see Figure 3, Section 4), hedged claims (roughly 30 customers, an estimated $1B market), and quoted material (blockquotes, fenced code, inline backticks) all default to NOT-emit to preserve dim 3 reviewer headroom. A critical_unsourced_load_bearing_claim flag fires when more than 5 unhooked claims surface OR any single named-author claim is unhooked (the named-author short-circuit reflects that class as the highest-confidence positive shape — an unhooked one is a fabrication risk). New command doc anvil/skills/memo/commands/memo-citations.md documents the CLI entry-point (python -m anvil.skills.memo.lib.citation_coverage <version_dir>), the detection contract, the severity ladder, and the auto-discovery contract. The CLI shape is the documented default until Phase 2 (hyperlink_resolver, #335) lands and a sibling convention is coordinated. Skill-local first per CLAUDE.md — promotion to anvil/lib/ is deferred until pub / report / proposal reach for the same primitive. Zero anvil/lib/ changes, zero schema changes in anvil/lib/review_schema.py, zero changes to anvil/lib/cite.py. New tests at tests/skills/memo/test_citation_coverage.py (46 tests) cover all four detector classes with both positive AND false-positive cases, both citation styles (\cite{} and [@]) with both broken-key and valid-key paths, the closest-match suggestion path (\cite{karpathy204} with refs containing karpathy2024 suggests the correction), the critical-flag heuristic at both threshold and named-author short-circuits, the filesystem integration via scan_version_dir + collect_refs_keys, the auto-discovery contract (verify aggregate merges findings from a .citations/ sibling without aggregator changes), and the CLI entry-point smoke. Full test suite (1239 passed, 2 skipped — network smoke tests).
  • anvil:memo hyperlink-resolver critic (Epic #328 Track B Phase 2, issue #335). New skill-local lib module anvil/skills/memo/lib/hyperlink_resolver.py ships a deterministic link-validation pass over a memo version directory and emits a canonical _review.json (kind=tool_evidence, critic_id=hyperlinks) for broken cross-thread refs, broken markdown internal paths, broken wiki-links, and (behind the off-by-default --check-external flag) failing external HTTP/HTTPS links. Four link classes: (1) cross-thread refs [[../slug/slug.N]] validated via cross_thread_refs.resolve_cross_thread_ref (no duplicate parsing — delegates entirely to the existing typed resolver), severity blocker on failure AND raises the load-bearing critical_broken_cross_thread_anchor flag so the aggregator's verdict short-circuits to BLOCK; (2) markdown internal links [text](path/to/file) validated against the version dir (trailing #anchor / ?query stripped — anchor validity is out of scope), severity major on missing file; (3) markdown external links [text](https://...) recorded by default but NOT probed (offline-safe / CI-reproducible), probed via subprocess curl -I with a 5-second timeout when --check-external is set, severity major on 4xx / 5xx / timeout; (4) wiki-links [[document-name]] validated against the enclosing project's BRIEF.md documents: list (discovered via project_discovery.discover_thread_root + project_brief.load_project_brief), severity major on unknown slug or missing BRIEF. New command doc anvil/skills/memo/commands/memo-hyperlinks.md documents the invocation, the four link classes with severity table, the auto-discovery wiring (<version_dir>.hyperlinks/ sibling matches the <version_dir>.<tag>/ pattern that anvil/lib/critics.py::discover_critics recognizes without code changes), and the seven failure-mode entries with operator actions. Implementation reuses existing primitives: cross_thread_refs.py for the internal-ref half (~60% of the link surface), project_brief.py + project_discovery.py for the wiki-link half, review_schema.py for the canonical Review / Finding / CriticalFlag shape (no schema delta — settled at Epic #328 kickoff). Tests at tests/skills/memo/test_hyperlink_resolver.py (28 new tests) cover every issue #335 acceptance criterion plus schema-compliance regression guards (asserting the Finding model has NOT grown action / target_anchor / proposed_content fields the Phase 2 settle deferred), the auto-discovery round-trip through aggregate, the CLI entry-point exit codes (0 clean / 1 findings / 2 invocation error), and a doc-coverage suite over the new command file.
  • CLI entry-point convention for Epic #328 Track B mechanical detectors: python -m anvil.skills.memo.lib.<module> <version_dir> [--check-external] [--write-review]. Documented as the agreed coordination point with the parallel Phase 3 citation-coverage critic (#336) so both detectors share an invocation shape.
  • Output-dir naming convention for Epic #328 Track B siblings: <version_dir>.<critic-tag>/ (.hyperlinks/ here, .citations/ for #336). Both forms are picked up by anvil/lib/critics.py::discover_critics via the <version_dir>.<tag>/ pattern without code changes — the contract was already shipped; this just exercises the second consumer of the convention.
  • anvil:memo figure + hyperlink enrichment guidance for the standard reviewer (Epic #328 Track A, issue #333, documentation-only). Extends anvil/skills/memo/rubric.md §"Refs back-check (dim 3)" with a new §"Figure + hyperlink enrichment (dim 3, advisory scope: expand)" subsection that instructs the standard memo-review reviewer to emit scope: expand enrichment findings in comments.md for four classes of gap: (1) missing or off-brand figures (load-bearing claims where a chart / table / diagram would clarify; figures that visibly diverge from the brand palette in anvil/lib/figures/palette.py), (2) missing or inadequate alt text on markdown ![alt](path) and HTML <img> references (empty alt, literal image/figure/chart placeholders, sub-10-character non-descriptive alt — the existing memo_image_refs_exist lint covers existence only), (3) prose references to figures that do not exist in the version directory (see Figure N, as shown in Chart M, Table K reports patterns whose referenced anchor is missing), and (4) suggested-but-missing hyperlinks on load-bearing claims that could anchor to a known external source (advisory; lower priority than 1–3). Severity ladder reuses the existing major / minor / nit comments.md vocabulary; scope tag is scope: expand (participates in the existing §"Expand trim-candidate rule" so any major enrichment proposing ≥1 paragraph names what could be trimmed to fund the addition). Enrichment is comments.md-side only and does NOT alter the dim 3 /6 score — the existing dim 3 sub-rules (citation hooks, refs back-check, strongman back-check, cross-thread back-check) own the per-instance deduction surface; this layer is enrichment that proposes additions for the next revision to consider.
  • Authoring-surface precedent codified: enrichment guidance lives in rubric.md (per-skill, durable, ships with the skill) rather than in rubric_overrides on BRIEF.md (per-thread, ephemeral). The decision is load-bearing for future enrichment scopes (image accessibility, claim grounding, etc.): cross-thread always-on guidance ships in rubric.md; per-thread tuning lives as rubric_overrides.dim_N_calibration calibration suffixes that compose with the rubric via the existing Reader-dispatch-order contract. A per-thread override that scopes out figure enrichment (e.g., a synthesis brief where figures are intentionally minimal) attaches as a calibration suffix on dim 3's justification and the reviewer respects it inline.
  • Phase A discipline preserved: zero anvil/lib/ changes, zero schema changes in anvil/lib/review_schema.py, zero new Python detector, zero new critic-sibling type, zero new command. The change is documentation in anvil/skills/memo/rubric.md only. commands/memo-review.md step structure is unchanged (steps 4f / 4g / 4h / 4i renumbered by the just-merged PR #332 are left alone — the new enrichment guidance attaches to existing step 5's dim 3 scoring loop via the comments.md write at step 8, no new step required). The contract follows the same shape as the strongman back-check (#330 / PR #332) and the prior back-check Phase A precedents (PR #144 / PR #250 / #236).
  • Backwards compatibility: this sub-rule is always active (no on-disk substrate trigger required — unlike the strongman back-check). A memo with no figures, no image refs, and no figure-reference prose produces zero enrichment findings of classes 1–3 (and likely zero of class 4 if the citation-hook contract is fully satisfied). A reviewer running on a memo authored before this sub-rule shipped behaves byte-identically — there are no missing-figure findings to emit when no claim load-bearingly anticipates a figure.
  • Validation step (deferred to follow-on): the canary author's .enrich/ smart-actuators fixtures (offered in Epic #328's comment thread, not yet attached to #333) serve as the future Phase B regression anchor in the same way tests/fixtures/summary_detail_consistency/northwind_gen_attribution/ anchors the summary-detail Phase B detector. The rubric guidance ships first; fixture validation lands when the fixtures are attached. A fixture-request comment can be filed on #333 without blocking landing.
  • Track A canary validation for Epic #328: this is the first active phase of the reframed Epic #328 (#328 was reshaped 2026-06-05 after the studio canary demonstrated that judgment enrichment via rubric guidance captures ~80% of the value of the original five-critic plan at ~30% of the implementation surface). Phases 2 (hyperlink-resolver) and 3 (citation-coverage) are Track B mechanical detectors — they are informed by Phase 1's outcome (specifically: whether the canary's "judgment enrichment is enough" hypothesis holds against the smart-actuators fixtures, and whether loading the existing reviewer with figure/hyperlink enrichment scope dilutes its judgment quality on existing dimensions) but do not block on it.
  • anvil:memo strongman files standardized as recognized source-of-truth materials (#330, documentation-only formalization). The studio canary's strongman practice (paired adversarial briefs scoped per research question, stored alongside research inputs that feed the memo draft) is formalized as strongman-for.md / strongman-against.md recognized filenames in the resolved refs-dir list. Three studio threads actively use the pattern in production (meridian-licensing with 5 vertical pairs, smart-actuators/investment-memo with 7 vertical pairs, smart-actuators/broadcom-thesis with one pair on the Broadcom analogy); the second-consumer threshold for the pattern shape was reached months ago. The strongman files live in the research/input layer (refs/ or research/<topic>-analysis/), NOT as a critic sibling of a versioned memo dir — they are author-supplied substrate, not critique output.
  • anvil/skills/memo/SKILL.md — extended §"Source-of-truth materials" with strongman-for.md (dim 2 Thesis coherence calibration substrate) and strongman-against.md (dim 3 Evidence quality back-check substrate) as recognized filenames, plus a §"Strongman scoping convention" paragraph documenting the per-thesis scoping (one named thesis or research question, not the memo as a whole; multiple pairs may exist per thread root organized into refs/<topic>/ or research/<topic>-analysis/).
  • anvil/skills/memo/rubric.md — extended §"Refs back-check (dim 3)" with a §"Strongman back-check (dim 3)" sub-paragraph naming the three-valued classifier (ADDRESSED / PARTIALLY_ADDRESSED / NOT_ADDRESSED) and the load-bearing-aware severity ladder (load-bearing NOT_ADDRESSEDcritical + critical-flag candidate + -2 dim 3; non-load-bearing NOT_ADDRESSEDimportant + -1; PARTIALLY_ADDRESSEDimportant + -1). strongman-for.md calibrates dim 2 (Thesis coherence) — the reviewer's dim 2 justification SHOULD note whether the memo's thesis aligns with the strongest version of its own argument when strongman-for.md is present.
  • anvil/skills/memo/commands/memo-review.md — added new sub-step 4g (strongman back-check, reviewer-prose-only Phase A) implementing the per-objection classifier; renumbered the prior 4g (rubric_overrides loader) to 4h and the prior 4h (artifact-type overlay) to 4i. The new step mirrors the structural shape of step 4f (cross-thread cite back-check) with a 3-valued vocabulary instead of 4-valued. Added the strongman_back_check top-level block to the _summary.md schema (sibling to summary_detail_consistency, cross_thread_cite_consistency, scope_distribution, rubric_overrides) carrying ran, strongman_against_files_scanned, strongman_for_files_scanned, objections_enumerated, findings_count, findings_by_severity, findings, critical_flag_candidate. Added the ## Strongman back-check findings subsection to findings.md (always emitted; three shapes — findings present / skipped / clean). Extended step 7 verdict aggregation with the strongman critical flag, step 10 verdict-write with the Strongman: NOT_ADDRESSED (load-bearing) critical-flag entry and the strongman-driven top-3 revision priority.
  • anvil/skills/memo/commands/memo-draft.md — extended step 3 Read inputs with the §"Strongman drafter contract" sub-paragraph documenting that when strongman files are present the drafter reads them as authoritative load-bearing substrate and either directly addresses or explicitly scopes out the named counter-arguments inside strongman-against.md (reviewer will check at step 4g); strongman-for.md is read as load-bearing thesis-statement context for dim 2 alignment. Extended step 6 Evidence §"Source-of-truth refs as authoritative hooks" to list strongman files among the canonical source-of-truth shapes.
  • Phase A discipline preserved: zero anvil/lib/ changes, zero schema changes to anvil/lib/review_schema.py, zero new Python detector, zero new critic-sibling type, zero new command. The entire change is documentation in four files inside anvil/skills/memo/ (SKILL.md, rubric.md, commands/memo-review.md, commands/memo-draft.md). The contract follows the existing back-check Phase A precedent: ship as reviewer-prose discipline first; promote to detector if canary signal warrants.
  • Backwards compatibility: threads with no strongman files of either kind are unaffected — the strongman back-check is inactive (the reviewer emits ran: false with a reason; the drafter proceeds normally). Threads where the operator wrote strongman-for.md but no strongman-against.md get dim 2 calibration substrate but no dim 3 findings; threads with strongman-against.md but no strongman-for.md get the full dim 3 back-check but no dim 2 calibration note. The canary's three active threads (which already write both files in production) get the full contract.

Fixed

  • anvil:memo render: removed redundant <link rel="stylesheet"> from template.html (#319). The bare relative href="styles.css" was resolved by weasyprint against pandoc's CWD (the consumer repo root on canary installs), producing a stderr ERROR: Failed to load stylesheet: file:///…/styles.css while the absolute --css path passed by anvil/lib/render_gate.py separately loaded the correct file. The render succeeded and the styling landed because --css did the work, but the failed-fetch warning sailed through and would be promoted to a hard error by the --fail-if-warnings invariant already active in render_gate.py:972. One-line delete of anvil/lib/memo/template.html:36; stylesheet delivery is now solely via pandoc --css (the existing --css invariant in render_gate.py is untouched and remains correct). Consumer-visible effect: no more spurious weasyprint stderr line on anvil:memo-render runs against the canary's output/clear-signal/memo/memo.1/. New regression guard test_lib_memo_template_html_avoids_bare_stylesheet_link in tests/lib/test_memo_render_detection.py asserts the <link rel="stylesheet"> shape stays absent (modeled on test_memo_styles_page_size_doc.py::test_styles_css_page_block_avoids_bare_letter_keyword, the same inverse-shape "this must NOT appear" pattern). The xelatex fallback path (template.tex) is unaffected.

Changed

  • parity_lint promoted to anvil/lib/parity.py (#317, PR #205 follow-on #2 of 4). Per CLAUDE.md §"Skill-local first, lib promotion later", the second-consumer trigger fired on merge of PR #215 (memo-side mirror); this is the canonical one-line import-path swap. The shared module ships the byte-identical core (extractors, normalization, escape-hatch, Finding, LintResult) plus a unified lint_parity(primary_path, sibling_path, primary_kind, sibling_kind) wrapper. Both skill-local modules (anvil/skills/deck/lib/parity_lint.py, anvil/skills/memo/lib/parity_lint.py) shrink to thin re-exports preserving the public API byte-compatibly — the deck-review step 5d / memo-review step 4d invocations and the two *-review.md doc-coverage tests remain unchanged.

[0.3.0] — 2026-06-04

Project-organization model lock + canary-driven completion of the multi-thread named-document layout. v0.3.0 closes out the #283 epic (project-as-thread-root layout discovery, project-level BRIEF.md parser, cross-thread reference validation, .latest symlink resolution, rubric overlay selection from BRIEF, shared research/) and locks the model with the three-part contract change: body filename echoes doc slug (#295), BRIEF.md absorbs all per-project anvil config (#296), and a new anvil:project-migrate skill bridges existing projects (#297). Skill count: 8 → 9.

Added — Multi-thread named-document project layout (issue #283 epic, 6 sub-deliverables)

The full project-as-thread-root layout shipped across six sub-deliverables, matching the canary's smart-actuators model where one project (e.g. foundry, northwind-pitch-strategy) holds multiple named documents (investment-memo, team-thesis, execution-plan, latency-wall, technical-vision) as siblings inside <project>/<slug>/<slug>.<N>/.

  • Sub-deliverable 1 — dual-layout thread-root discovery (#284 / PR #290). anvil/skills/memo/lib/project_discovery.py walks upward from any path and recognizes the enclosing thread root. discover_thread_root() returns a typed DiscoveryResult carrying thread_root, layout, project_root, and slug. (The classic-layout dispatch ships here for v0.2.0 back-compat, then is removed in #295 — see Removed below.)
  • Sub-deliverable 2 — typed project BRIEF parser (#285 / PR #292). anvil/skills/memo/lib/project_brief.py defines pydantic models for the project BRIEF: ProjectBrief (project / audience / hard_rules / documents) and BriefDocument (slug / artifact_type / target_length). Closed-ended ArtifactType enum with five seed values (investment-memo, position-paper, tactical-plan, vision-document, descriptive-thesis) — adding a new type is a documented anvil feature request. Lenient + strict loaders; optional validate_dirs flag for slug-vs-directory divergence checking (listed-but-missing → warn, on-disk-but-unlisted → hard error).
  • Sub-deliverable 3 — rubric overlay selection from BRIEF (#286 / PR #294, absorbs closed #278). anvil/skills/memo/lib/rubric_overlays.py plus per-artifact-type overlay JSONs under anvil/skills/memo/rubric_overlays/. The reviewer resolves the active overlay by reading the project BRIEF's artifact_type for the current thread's slug and applies the matching overlay on top of the base /44 rubric. Five overlays ship at v0.3.0 matching the five registered artifact types. Closes #278 (per-thread rubric overlays for non-memo artifact types — folded into this issue's BRIEF-driven dispatch).
  • Sub-deliverable 4 — cross-thread reference validation (#287 / PR #291). anvil/skills/memo/lib/cross_thread_refs.py walks the project's siblings during reviewer back-check (dim 3) and flags broken see <slug>.<N> §<section> references: cite-target slug doesn't exist, version doesn't exist, or the cited section anchor is missing. New _review.json.cross_thread_refs block carries the audit trail.
  • Sub-deliverable 5 — canonical .latest symlink resolution (#288 / PR #293). anvil/skills/memo/lib/latest_resolution.py resolves <slug>.latest/ to the highest-numbered concrete version dir, with optional pinned-symlink override. SKILL.md documents the convention; the framework consistently respects whichever version .latest points at.
  • Project-level shared research/ (#280 / PR #281). <project>/research/ is a first-class peer of the document folders — cross-thread evidence pool that any sibling's reviewer can cite. Composes with the existing per-thread <slug>/refs/.

Added — anvil:project-migrate skill (issue #297, model-lock bridge)

NEW skill anvil/skills/project-migrate/ — one-shot bridge tool that migrates existing studio projects to the post-#295 / post-#296 canonical model (project root + BRIEF.md absorbing all anvil config + <slug>.md body filename + <project>/<slug>/<slug>.<N>/ shape). Closes the third leg of the three-part model lock (#295 + #296 + #297). Skill count: 8 → 9.

  • Commands: /anvil:project-migrate <project-dir> (dry-run, NO mutations), /anvil:project-migrate <project-dir> --apply (execute), /anvil:project-migrate <project-dir> --report (markdown report only).
  • Three recognized current shapes: pre-#283 classic (<stem>.N/ sibling version dirs, no project BRIEF), post-#283 with .anvil.json (project BRIEF + per-thread .anvil.json), fully-migrated (target shape — no-op).
  • Per-project steps: detect → plan → (optional) apply → verify. Each DocumentPlan is independently applyable; rollback is per-doc via a <project>/.anvil-migrate-rollback/<slug>/ snapshot.
  • Cross-thread reference rewriting: planner walks each body markdown for old-stem tokens (e.g., memo.7) and emits a ContentRewrite that updates them to the new slug-shaped reference (<slug>.7) after the directory renames land.
  • Git integration: git mv is preferred when the project is under git so history follows; falls back to plain shutil.move otherwise.
  • Opinionated: no back-compat flags. The skill converges existing projects onto one shape; it does not preserve the legacy shape under any option.
  • Idempotent: re-running --apply on a fully-migrated project is byte-identical zero-diff.
  • rubric.md OMITTED: migration output is mechanical; no /40 dimension to score.
  • anvil:memo-migrate carve-out: the LaTeX bootstrap path continues to write a legacy .anvil.json; project-migrate runs as the documented post-step that consolidates it into the project BRIEF.
  • Per-doc rollback fault-injection test (#301 / PR #302) exercises the _restore_doc branch via _rename monkeypatch — verifies failing doc rolls back to pre-migration shape while successfully-migrated prior docs stay migrated.
  • Touched: NEW anvil/skills/project-migrate/SKILL.md, NEW anvil/skills/project-migrate/commands/project-migrate.md, NEW anvil/skills/project-migrate/lib/ (detect.py, plan.py, apply.py, verify.py, orchestrate.py), NEW anvil/skills/project-migrate/tests/ (seven test_project_migrate_*.py files + _fixtures.py + conftest.py), MODIFIED anvil/skills/README.md and top-level README.md (skill index), MODIFIED CLAUDE.md (skill count 8 → 9).

Changed — project-organization model lock (issues #295 + #296)

The structural primitives shipped in #283 (sub-deliverables 1–5) get the contracts on top of them tightened to one shape:

  • Body filename echoes doc slug (#295 / PR #298). The default body is <slug>.md (e.g. investment-memo.md, team-thesis.md), not skill-fixed memo.md. Anvil discovery is doc-name-agnostic; macOS Spotlight, "Open Recent", and shell output all carry the document identity. body_filename_for(slug) is the single resolution helper (originally added in anvil_config.py here, then moved to project_brief.py in #296 when anvil_config.py was deleted). The override mechanism in the prior .anvil.json shape is removed because the new echo default makes it redundant.
  • BRIEF.md absorbs all per-project anvil config (#296 / PR #299). <project>/BRIEF.md is the single config locus; per-thread .anvil.json files are no longer read or written by the live pipeline. BriefDocument grows optional target_length_overrides (per-version override map) and rubric_overrides (preserves PR #265's calibration-suffix shape verbatim) under each documents: entry. anvil/skills/memo/lib/anvil_config.py is deleted entirely (711 lines); its load_rubric_overrides(thread_dir) API is replaced by project_brief.load_rubric_overrides_for_slug(project_dir, slug) preserving the empty-on-absence contract. anvil/skills/memo/templates/BRIEF.rubric-overrides.md.example ships as the new worked example (replaces the deleted .anvil.json.synthesis-brief.example and .anvil.json.feedback-memo.example).

Fixed — anvil:memo-render pandoc 3.x compat (issue #277 / PR #289)

anvil/skills/memo/lib/render_gate.py and the xelatex template gain a pandoc 3.x emission compat block. Recent pandoc emits LaTeX requiring packages the template didn't load; the fix backstops the canary's render-gate against the regression without pinning pandoc.

Removed — LAYOUT_CLASSIC dual-layout fallback (issue #295)

anvil/skills/memo/lib/project_discovery.py no longer returns LAYOUT_CLASSIC. The per-thread-BRIEF layout (with no project root) is gone — every memo thread now lives inside a project root with a project-level BRIEF.md. The LAYOUT_CLASSIC constant and its branches in _resolve_layout / discover_thread_root are deleted. Stray threads without an acknowledging project BRIEF return None from discovery. The classic-layout test cases were either deleted or converted to the project-brief shape; the classic-portfolio fixture under tests/fixtures/project_brief/ was removed.

Removed — .anvil.json per-thread runtime config (issue #296)

anvil/skills/memo/lib/anvil_config.py deleted in full (711 lines), plus its test surface (test_anvil_config.py, test_anvil_json_examples_roundtrip.py) and the .anvil.json.*.example templates. All anvil_config consumers now read from BRIEF.md. The one intentional carve-out: anvil/skills/memo/lib/migrate.py (the LaTeX bootstrap path) still writes a legacy .anvil.json as transient output that anvil:project-migrate then consolidates — documented in both commands/memo-migrate.md §".anvil.json legacy note" and project-migrate/SKILL.md §"Relationship to memo-migrate".

Maintenance

  • Post-#297 polish bundle (#301 / PR #302). Top-level README.md skill count 8 → 9 and project-migrate skill table row, project-migrate/SKILL.md line 144 test-filename drift fix, per-doc rollback fault-injection test (test_project_migrate_rollback.py, 4 assertions exercising the _restore_doc branch), and apply.py _rewrite_file count math rewritten with text.count(old_string) for clarity.
  • Convictions ledger residuals cleanup (#303 / PR #304). Removed three dead references to the killed-by-#229 _convictions.md artifact: memo-revise.md "Convictions section when _convictions.md exists (legacy)" bullet, the plan.md.template convictions block, and the test_template_documents_convictions_section test. The lightweight Resolution: declined — see prior conviction at <anchor> mechanism in the regular changelog.md is preserved untouched at memo-revise.md lines 49, 265, 366.

Acknowledged unsolved (architect placeholder)

  • #228 — cross-version conviction carry-forward remains open as a loom:architect placeholder per the curator's 2026-06-03 defer-ratification verdict. The unsolved problem (firm convictions silently re-litigated in subsequent revise passes) is real; the #142 _convictions.md primitive was the wrong shape (write-only on advance per #225; wrong trigger per #226); no concrete redesign has emerged and none of the three re-opening triggers from #228's body has fired. v0.3.0 leaves it untouched.

[0.2.0] — 2026-06-03

Canary-driven iteration since 0.1.0. Seventy-eight PRs landed in five days as the framework absorbed friction from the 2AM Logic Studio canary running multi-thread investment memos and proposals against rolling deadlines. The shape of this release: a new dim 9 Rhetorical economy rubric dimension (rubrics → /44, threshold ≥35), an anvil:proposal synthesis-sibling pipeline that consolidates cross-critic findings before revise, an anvil:memo --plan / --apply two-phase reviser, a rubric_overrides mechanism for non-investment-memo shapes (synthesis-brief, feedback-memo), a bulk memo-migrate LaTeX→markdown migration tool with 9 detector clusters, a framework-wide <thread>.{N}.perspective/ sibling role, and an installer pivoted to uv-runnable consumer layouts. See WORK_LOG.md for the chronological merge record.

Added — anvil:memo critic-side scope tagging on review comments (#242)

  • Critic-side scope: preserve | expand | reduce tagging on every <thread>.{N}.review/comments.md entry, mechanically tied to dim 9 Rhetorical economy (#244 / PR #254). Phase A reviewer-prose-only (no anvil/lib/ schema changes); composes with reviser-side severity filtering (#241).
  • anvil/skills/memo/rubric.md — extended §"Dim 9 — rhetorical economy" with a "Surfacing to comments.md" subsection codifying the dim 9 → scope: reduce echo rule; added a §"Scope tagging (comments.md)" top-level subsection defining the three-valued vocabulary, the dim 9 echo rule, the scope: expand trim-candidate rule (major→minor downgrade when no trim candidate named), the verdict.md scope: reduce first-priority rule (when dim 9 < 4/4), the _summary.md.scope_distribution block, and the backwards-compat fallback for legacy reviews.
  • anvil/skills/memo/commands/memo-review.md — extended step 5 (per-dim scoring) with the dim 9 scope: reduce echo sub-step; extended step 8 (line-level comments) with the scope label requirement, the dim 9 echo, and the scope: expand trim-candidate downgrade rule; extended step 9 (_summary.md write) with the top-level scope_distribution block (sibling to lint and render_gate); extended step 10 (verdict.md write) with the scope: reduce first-priority rule when dim 9 < 4/4.
  • anvil/skills/memo/SKILL.md — added a short "Critics → reviser: scope tagging on comments.md" framing subsection pointing at the rubric and command-spec contract.
  • tests/skills/memo/test_memo_review_scope_tagging_doc.py — new doc-AC test asserting the rubric and command spec carry the scope-tag contract surface (mirrors the existing test_memo_review_render_gate_wiring_doc.py doc-AC pattern). Distinct filename per the #58 packaging convention.

The asymmetry the canary diagnosed (critics propose adding content, never trimming): dim 9 (#244) closed the scoring-side hole; this issue closes the comments-stream-side hole. Without the echo, the reviser sees the dim 9 deduction in scoring.md but has no comments.md entry to act on. With it, every dim 9 anti-pattern instance mechanically becomes a scope: reduce comment, every scope: expand comment proposing a paragraph or subsection names a trim candidate, and _summary.md.scope_distribution carries the operator-visible signal that the critic is surfacing both directions. Memo-only this round; proposal-side mirror deferred per the precedent that #245's deck-side mirror followed (ship the rubric-side primitive on the canary-surface skill first; mirror to siblings after one consumption cycle).

Added — anvil:memo-revise plan-then-apply mode (issue #243, Phase A)

  • --plan / --apply CLI flags on memo-revise — opt-in two-phase invocation that materializes a plan.md change-set preview between scope choice and edit application. The shape mirrors terraform plan / terraform apply (or git rebase -i): memo-revise <thread> --plan writes a per-item planned-edit table at <thread>.{N+1}.plan/plan.md (a critic-sibling-shaped artifact, NOT a version dir) and exits without producing <thread>.{N+1}/memo.md; operators edit plan.md in place to decline items (three accepted shapes: same-line <!-- declined: <reason> -->, row deletion, or Priority: declined + bracketed [declined: <reason>]); memo-revise <thread> --apply then reads the (optionally edited) plan, validates freshness, and produces <thread>.{N+1}/memo.md + changelog.md per the existing reviser contract.
  • Plan validity contract. --apply refuses stale plans across five cases: no matching plan exists, source review verdict.md mtime is newer than plan.md (re-reviewed since plan written), a new critic sibling was added since plan time, plan is older than plan_max_age_days (default 7, configurable via <thread>/.anvil.json), or <thread>.{N+1}/ already exists. Each rejection points at remediation (typically: re-run --plan to refresh). Plan-sibling shape: _meta.json declares scorecard_kind: "planner"; _progress.json.metadata.critic_siblings_at_plan_time snapshots the critic set so apply-side detection of new siblings is exact.
  • Composition with --polish. memo-revise <thread> --polish "<reason>" --plan writes a polish-pass plan; the verbatim operator reason flows from the plan header through to the produced version dir's _progress.json.metadata.revise_force_reason (operator does NOT re-pass --polish on --apply — the plan IS the audit trail). New composed revision_mode values: "plan_then_apply" and "polish_plan_then_apply". Both are additive and audit-trail-only — not scored, not gating, no state-machine impact. --plan and --apply are mutually exclusive.
  • State-machine impact: none. Plan siblings (<thread>.{N+1}.plan/) do NOT advance the thread to REVISED. The state-machine derivation in SKILL.md continues to use <thread>.{N+1}/memo.md presence as the REVISED evidence; plan siblings are invisible to it. This preserves the immutability contract (a half-built version dir without a memo.md is never REVISED).
  • Default no-flag path unchanged (load-bearing regression contract). memo-revise <thread> with no flags continues to produce <thread>.{N+1}/ directly per the existing 11-step procedure. The new dispatch steps 0a (--plan) and 0b (--apply) fire FIRST in the Procedure block; absence of either falls through to the unchanged 11-step procedure. Every existing consumer (the canary today, the 8 shipped skills' integration tests, the install-script regression tests) continues to work without modification.
  • Phase A scope: reviewer-prose-only — no Python detector module. The plan-parsing logic is small enough to live inline in the command spec; extraction to anvil/skills/memo/lib/plan.py is a follow-on once a second consumer adopts the two-phase pattern (per CLAUDE.md "skill-local first, lib promotion later"). The shipped change touches anvil/skills/memo/commands/memo-revise.md (primary spec — new §"Plan-then-apply mode" + dispatch steps 0a/0b), anvil/skills/memo/SKILL.md (new §"Operator-confirmable change-set preview" sibling to §"Operator-initiated polish passes" + plan-sibling row in the artifact-contract diagram + state-machine non-gating note + command-dispatch-table flag update), anvil/skills/memo/templates/plan.md.template (canonical plan-artifact shape), anvil/skills/memo/tests/test_memo_revise_plan.py (44 tests across 8 test classes — doc-coverage + fixture-shape + AC10 inventory), and six fixtures under anvil/skills/memo/tests/fixtures/memo_revise_plan/ (clean_plan_apply, stale_verdict_rejected, declined_items_decay, target_length_exceeded, polish_plan_compose, no_flag_regression).

This addresses the studio canary friction documented in issue #243 (northwind-pitch-strategy thread, 2026-06-02): memo.3 → memo.4 produced a defensible higher-scoring version that the operator deleted and reverted on read because each addition was defensible in isolation but the aggregate drifted away from "clean and forceful presentation." A plan.md preview at the --plan step would have surfaced the per-item summaries and allowed line-level rejection before any edit was committed.

Added — anvil:proposal synthesis-sibling schema + command spec (sub-issue 1 of issue #246)

  • anvil/skills/proposal/lib/synthesis_schema.py — NEW pydantic models for the <thread>.{N}.synthesis/gaps.json contract. GapList carries schema_version: "1", for_version, optional thread, and the two cross-sibling consolidation primitives: Gap (clustered findings — at least one ContributingFinding per gap, plus root_concern / recommended_response / severity ∈ {critical, blocker, should-fix, nice-to-have} / optional rubric_dimensions) and Singleton (findings that did NOT cluster). Mirrors anvil/lib/review_schema.py shape and discipline (extra="forbid", pinned schema version, optional fields default to safe empties). Skill-local first per CLAUDE.md "Skill-local first, lib promotion later" — lives under the proposal skill until a second skill adopts synthesis.
  • anvil/skills/proposal/lib/synthesis_schema.json — companion JSON Schema document (Draft 2020-12) auto-generated from the pydantic model so non-Python callers validate gaps.json against the same contract.
  • anvil/skills/proposal/commands/proposal-synthesize.md — NEW command spec for the synthesizer lifecycle role inserted between parallel critics and the single reviser. Documents the on-disk shape (<thread>.{N}.synthesis/ with verdict.md, synthesis.md, gaps.json, _meta.json, _progress.json), the resume / crash-recovery contract, the clustering procedure (deterministic pre-filter + LLM cluster step + conservative "leave as singleton when uncertain" fallback), the severity ladder (max-across-contributors), the aggregator-skip rule (role: "synthesizer" in _meta.json — no per-dimension scores means the existing aggregator's null-fall-through handles it without code change), the state-machine integration (new SYNTHESIZED transient state between REVIEWED+AUDITED and REVISED), and the reviser-side backward-compatibility fallback (when gaps.json is absent, proposal-revise reads per-sibling findings directly — preserved as the rollout safety net).
  • anvil/skills/proposal/tests/test_synthesis_schema.py — schema-only tests: round-trip on the 12LP+ canary fixture from the issue body, default-fields safety, schema-rejects-invalid coverage (empty contributing-findings → rejected; unknown severity → rejected; missing required fields → rejected; extra fields → rejected; for_version < 1 → rejected), full severity vocabulary acceptance, JSON Schema document presence + drift detection between model and on-disk JSON, command frontmatter parse. Distinct filename per the #58 packaging convention.

This is sub-issue 1 of 4 from the curator's decomposition of issue #246. It establishes the load-bearing contract on which sub-issues 2 (reviser-side consumption), 3 (orchestrator + state-machine integration), and 4 (Studio reproducer integration test) depend. Reviser code, orchestrator wiring, and SKILL.md state-machine updates are deliberately out of scope for this PR — sub-issues 2 and 3 will land in parallel once the contract is fixed; sub-issue 4 lands last.

Changed — anvil:memo and anvil:proposal rubric shape

  • New dim 9 Rhetorical economy (weight 4) added to both anvil:memo and anvil:proposal rubrics. Both rubrics now score against 9 weighted dimensions summing to 44 (was 8 dims / 40). The advance threshold rises to ≥35/44 (was ≥32/40) — the ~80% bar is preserved (35/44 = 79.5%; 32/40 = 80%). Dim 9 polices whether every paragraph is load-bearing — could the same argument land in fewer words? Are the most important claims surfaced early? Is hedging proportional to genuine uncertainty? Could a busy reader extract the recommendation in 90 seconds? It is the countervailing pressure against the bloat failure mode the existing 8 dims structurally rewarded (every other dim rewards adding more). Six named anti-patterns (multi-paragraph hedges, oversized footnotes, redundant subsections, restating tables, reformulated open-decisions entries, restating bullet lists) make the dim actionable for the reviser. The justification MUST cite specific instances — same anchoring discipline as the existing dim 3 citation-hooks rule. Closes #244 (canary surface: 2AM Logic Studio's northwind-pitch-strategy v1→v2 produced a "less compelling" v2 despite a higher /40 score; dim 9 is the missing countervailing pressure).
  • Cross-skill divergence: the other six anvil-shipped skills (anvil:pub, anvil:report, anvil:deck, anvil:slides, anvil:ip-uspto, anvil:installation) continue on the 8-dim /40 rubric. Dim 9 ships first on the two skills where canary friction surfaced it; broader propagation is a separate decision driven by per-skill calibration evidence. The framework no longer has a single "8-dim /40" default — per-skill rubric shape is now the explicit reality.
  • Backward compatibility: existing on-disk <thread>.{N}.review/verdict.md written against the old /40 rubric remains a legal historical record and will not be retroactively re-scored. The first revise pass after upgrade produces a v{N+1} whose subsequent review scores against the new /44 rubric. No anvil/lib/ schema changes; critic siblings continue to emit the human-verdict scorecard kind via the existing LEGACY_MEMO_FILES adapter in anvil/lib/critics.py.
  • Deferred — Option C (genre-flag knob): the long-term shape is a genre: buildable-system | strategic frontmatter knob that activates dim 9 only for strategic-positioning artifacts. Option C depends on the per-genre rubric-override mechanism in #233 and ships as a companion issue once #233 lands. The dim 9 prose shipping here is reusable across both shapes.

Added — anvil:proposal synthesis pipeline (issue #246, four-PR decomposition)

  • Sub-issue 2 — reviser consumes gaps.json (#270). proposal-revise.md steps 6/7/9 updated to prefer <thread>.{N}.synthesis/gaps.json as the revision-plan source when present (validated against the pinned GapList pydantic model), with the per-sibling reading path preserved verbatim as the rollout-safety fallback. Step 7 walks gaps + singletons with critical → blocker → should-fix → nice-to-have ordering, planning one coordinated response per gap. Step 9 introduces the canonical synthesis <gap-id> (<sibling>.<ref>, ...) row format while preserving the <thread>.<N>.<sibling> (<severity>) shape on the fallback. 26 new structural tests pin the contract.
  • Sub-issue 3 — orchestrator + state-machine integration (#271). commands/proposal.md state inference (step 3) recognizes SYNTHESIZED as a transient state when <thread>.{N}.synthesis/verdict.md + gaps.json exist. Dispatch table (step 4) gains rows for REVIEWED+AUDITED → proposal-synthesize and SYNTHESIZED → proposal-revise, plus the parallel at-cap → BLOCKED rows. Anomaly detection extended for stalled-no-synthesis, crashed-synthesis, and orphan-synthesis cases. SKILL.md state-machine ASCII diagram now includes SYNTHESIZED between REVIEWED+AUDITED and REVISED; evidence table and command-dispatch table updated to match. 32 new tests across 9 classes pin the contract.
  • Sub-issue 4 — Studio reproducer integration test (#272). Fixture-and-clustering regression test for the 12LP+ FinFET mask cost canary (three siblings → one gap with three contributing findings). Ships an anvil/skills/proposal/lib/synthesizer.py clustering primitive with callback-injection seam (mirrors anvil/lib/vision.py::VisionCritic): default path raises NotImplementedError; consumers pass a callback for the LLM clustering step. Skill-local per CLAUDE.md "skill-local first, lib promotion later." 25 new tests pin clustering shape + post-processing pipeline (severity ladder defensive-override, dim-list union, schema validation).
  • Tolerant findings filename + alias contract (#255 / proposal-synthesize.md). The synthesizer's input contract documents that critic siblings may emit findings.md, Findings.md, or findings.json; the synthesizer reads whichever exists. Matches the existing audit-side tolerance pattern.

Sub-issue 1 of #246 (schema + command spec, #253) is documented above under "Added — anvil:proposal synthesis-sibling schema + command spec". With all four sub-issues landed, the EMPTY → DRAFTED → REVIEWED+AUDITED → SYNTHESIZED → REVISED → … → READY → AUDITED proposal lifecycle is fully wired end-to-end.

Added — anvil:memo rubric_overrides for non-investment-memo shapes (issue #233)

  • Sub-issue 1 — typed loader (#267 / anvil/skills/memo/lib/anvil_config.py). Pydantic loader for the rubric_overrides block in <thread>/.anvil.json. Supports per-dimension calibration strings (dim_N_calibration), optional target_length inside the override, and a memo_subtype discriminator (synthesis-brief, feedback-memo, etc.). extra="forbid" on the inner block; unknown keys surface as unknown_keys for forward-compat visibility without hard-failing the load.
  • Sub-issue 2 — reviewer integration (#273 / anvil/skills/memo/lib/rubric_overrides_suffix.py). memo-review reads rubric_overrides via the typed loader and appends \"calibration applied: <override text>\" as a verbatim suffix to each scored dimension's justification in _review.json + scoring.md. New top-level §"Reader dispatch order: .anvil.json vs BRIEF.md 'Critical reviewer guidance'" documents the precedence (structured config wins; BRIEF.md is documented Option-A fallback). Zero-impact when rubric_overrides is absent; documented in 27 tests covering suffix attached / suffix absent / per-dim dispatch / zero-impact / verbatim contract / loader-integration pipeline.
  • Sub-issue 3 — docs + worked-example templates (#274). SKILL.md gains a "Rubric overrides and non-investment-memo shapes" section with worked examples for both canary subtypes and the BRIEF.md Option-A fallback. rubric.md carries one-sentence pointers near the most-commonly-recalibrated dims (1, 5, 6, 7). Two .anvil.json example templates ship under anvil/skills/memo/templates/: .anvil.json.synthesis-brief.example (meridian-synthesis canary, [9000, 13000] words, calibrates dims 1/5/6/7) and .anvil.json.feedback-memo.example (northwind-pitch-strategy canary, [4000, 6000] words, calibrates dims 1/4/5/6/7). 19 round-trip tests pin loader compatibility + cross-template consistency. Deferred: memo-draft / memo-revise consumption of rubric_overrides.target_length (the reviewer surfaces target_length_present for audit visibility but doesn't act on the value; drafter/reviser wiring is a follow-on).

Added — anvil:memo-migrate (bulk LaTeX → markdown migration)

A new anvil:memo-migrate command and supporting library for converting a portfolio of legacy LaTeX memo threads to the markdown convention in bulk. Ships across nine PRs:

  • Command + base migration (#207). anvil:memo-migrate <source.tex> <portfolio> runs pandoc over the source, lays down the version-dir layout, and emits a structured _progress.json report.
  • Refs/ seeding from BRIEF.md Sources (#208). anvil:memo-migrate-refs extracts Sources references from BRIEF.md and seeds refs/ for later reviewer back-check consumption.
  • Detector clusters (#217–#222). Five detector clusters in anvil/skills/memo/lib/migrate.py flag layouts the canary corpus surfaces but pandoc loses: orphan figures in source figures/ (#217), packed single-cell tabularx layouts (#218), 4-column key/value metricbox tables (#219), empty figures/ directories (#221), and figure_policy classification for zero-figures intent (#222). Each emits a structured finding into the migration report.
  • Source brief ingestion (#220). Earliest-brief-wins rule: when multiple version directories carry a brief.md, the migration ingests the root-level (or oldest) brief and records provenance in _progress.json.metadata.source_brief_path.
  • Detector cluster reference (#223). commands/memo-migrate.md documents the detector cluster catalog with one-paragraph framing per cluster, anchoring the reviewer-side surface to the source-side detectors.
  • Memo parity lint mirror (#224). Memo-side <thread>.{N}.review/lint.json mirror of the existing anvil:deck parity lint shape (warning-only, Phase A). Surfaces missing-from-BRIEF and missing-from-memo discrepancies without gating advance.

Added — Framework <thread>.{N}.perspective/ sibling role (Epic #143)

A new perspective-aware critic sibling that surfaces strong-form alternatives to the artifact's central claim before the reviewer scores it. Lands across five PRs as a framework-level addition rather than a per-skill one.

  • Snippet convention + perspective discipline (#154). anvil/lib/snippets/perspective.md introduces the perspective sibling convention to the snippet-library substrate every skill reads.
  • anvil:memo perspective sibling (#183). memo-perspective command surfaces alternative candidate threads from the portfolio; output lands at <thread>.{N}.perspective/candidates.md for reviewer consumption.
  • anvil:proposal perspective sibling (#184). proposal-perspective mirrors the memo shape against the proposal lifecycle; new audit-wiring lines surface perspective evidence in the audit's findings.md.
  • Deck market perspective cross-check (#156, #157). deck-perspective command + market-side cross-check.
  • Perspective-aware dimension calibration (#194). anvil/lib/rubric.py extension that lets venue-pinned rubric overlays opt into perspective-aware calibration prose per scored dimension.

Added — anvil:memo reviewer & reviser convergence machinery

  • memo-revise --polish operator-initiated polish-pass entry point (#206). Operators can force a polish-pass revision with a verbatim free-text reason that flows into _progress.json.metadata.revise_force_reason for audit. Composes with --scope and --plan (see below).
  • --scope severity filter on revisers (#257, memo + proposal). Default important; operators may pass --scope all to fold in nice-to-have-severity findings or --scope blocker to limit to blocker-severity only. Filter is applied to per-sibling findings (or to gap severity under synthesis).
  • Per-revision directive convention (#260). Formalizes the operator-supplied <thread>.{N+1}.directives.md shape as the pre-revise directive surface — separate from BRIEF.md's per-thread directives.
  • Summary-detail consistency back-check (#245, PR #250). Phase A reviewer-prose-only back-check that surfaces verdict-summary vs per-dim-detail mismatches in <thread>.{N}.review/comments.md.
  • Cross-thread citation back-check (#236, PR #262). Phase A reviewer-prose-only back-check that flags citations referenced in a thread's body but absent from its refs.bib, walking sibling threads in the same portfolio.

Added — Rendering, layout, and ergonomics

  • anvil:memo-render command + state-machine integration (Epic #158 / PR #193). New explicit render lifecycle phase, replacing the implicit rendering that previously hid inside memo-revise. State-machine snippets updated to document the new phase.
  • anvil:memo lib substrate + renderer detection (Epic #158 Phase 1 / PR #172). New anvil/skills/memo/lib/ with renderer-detection helpers (weasyprint, pandoc, fallbacks).
  • Memo PDF render-gate (kind="memo") (#185). anvil/lib/render_gate.py gains a kind="memo" mode for the markdown→PDF pipeline; matches the LaTeX-side gate's shape (page-fit, overfull boxes, compile success, placeholder scan).
  • Render-gate findings wired into reviewer + word-count-primacy rubric prose (Epic #158 Phase 4 / PR #198). Render-gate output flows into reviewer prose for the dim 7 length-pinning calculation.
  • @page size US-Letter pinning for weasyprint (#232, PR #263). Fixes a regression where weasyprint defaulted to A4 because the @page size was unspecified.
  • Per-thread words_per_page override for memo_page_fit (#264). <thread>/.anvil.json: words_per_page lets operators tune the page-fit gate's word/page coefficient when the canary corpus drifts from the default.
  • Booktabs-class CSS for markdown tables (#238, PR #259). Brings the markdown table render closer to the LaTeX booktabs aesthetic; opt-in via class="booktabs".
  • orientation: landscape frontmatter knob for table-dense proposals (#248). Per-version landscape rendering for proposals whose table widths exceed portrait.
  • Pre-flight image-reference lint (memo_image_refs_exist) (#160). Detects markdown image links that resolve to missing files in figures/.
  • Per-version target_length overrides + provenance (#161). Operators may pin target_length on a per-version basis via <thread>.{N}/_progress.json.metadata.target_length for dim 7 anchoring.
  • refs/ source-of-truth materials + reviewer back-check (#162). Reviewers verify every body-of-memo citation against the materials in refs/; mismatches surface as dim 3 findings.
  • Configurable target_length in .anvil.json (#122). Top-level target_length in .anvil.json defines the resolved range that flows into dim 7 scoring. (Carried forward to the rubric_overrides mechanism above.)
  • Per-skill installer content-hash detection (#163). Installer records a per-skill content hash for modified-vs-pristine detection at upgrade time.
  • Surface new-skill availability on upgrade (#239, PR #261). Installer prints which skills are NEW at upgrade time, with copy-and-paste invocation lines.

Added — anvil:deck and anvil:slides extensions

  • anvil:deck-imagegen orchestration runtime + command spec (Epic #130 Phase 2 / PRs #169, #170, #171, #182, #186, #191, #192, #197). Brings generative-imagery into deck via a backend-agnostic preset library, an imagery_policy BRIEF.md frontmatter field, an imagegen orchestration runtime with prompt-journal schema and read/write primitive, three new audit findings (fabrication-attribution, generative-imagery findings gated on imagery_policy), and a consolidated imagegen_phrases.py for stock phrasing.
  • deck↔memo parity pre-flight lint (#205). Warning-only Phase A check that flags structural mismatches between a deck and its companion memo (used by the canary's deck-from-memo pipeline).
  • Deck .row + .split stock layout classes (#174). Reusable layout-only utility classes in anvil-deck.css.
  • Slides marp_lint re-export (#164, PR #173). anvil:slides pins re-export of the deck-side inline-display-style-dropped rule; the underlying primitive lives in marp_lint.
  • Inline display:grid / display:flex lint (#134). Source-side warning for inline display styles that Marp's foreignObject SVG render silently drops.
  • Deck iteration-cap rationale paired with max_iterations override (#141). When a deck override raises max_iterations, an iteration_cap_rationale line is mandatory and the orchestrator surfaces a BLOCKED notice for missing-rationale cases.

Added — Framework substrate

  • palette.json sibling for bare-python3 consumption (#126). Anvil's brand palette is now also available as a JSON sibling alongside palette.py for tooling that can't import the Python module.
  • Optional .latest symlink convention (#120, PR #123). Documented convention for the optional <thread>.latest symlink that resolves to the highest version dir.
  • Filetype-first vs project-first portfolio placement (#127). Snippet-level documentation of the two portfolio-layout shapes.
  • Drafter-side citation-hook contract (#140). Documented contract for the drafter-emitted citation-hook lines that the reviewer back-check consumes.
  • anvil:memo BRIEF.md fresh + migration templates (#139). Two new BRIEF.md templates: a fresh-thread template and a migration-target template.
  • Optional pdftotext PDF refs back-check (#175). Path A: subprocess pdftotext extracts the rendered PDF's text and back-checks refs against the source markdown.
  • Refs back-check rolled out to deck + proposal (#176). Same back-check primitive consumed by both sibling skills.
  • revise_consistency stale-token sweep + deck-revise wiring (#114). New anvil/lib/revise_consistency.py primitive sweeps for stale tokens between drafter output and reviser input; wired into deck-revise.

Changed — Dependencies

  • pyyaml>=6.0 declared as a base [project] dep (#231, PR #268). anvil/lib/rubric.py does a top-level import yaml; anvil/lib/__init__.py re-exports Rubric / load_rubric / discover_venue_rubric, so any from anvil.lib import ... (and downstream anvil.lib.render_gate) transitively requires yaml at import time. This matches the same load-bearing-for-import-chain shape as the existing pydantic base-dep exception; pyproject.toml's header comment is updated to document the second exception. Without this, a fresh uv sync produced a build that failed on first import.
  • Installer pivots to uv-runnable consumer layout (#230, PR #269). The shipped install layout is now a directly-uv sync-runnable shape; consumers no longer need a separate pip install invocation. Drift-detection note added to the installer to flag layouts that predate the new shape.

Removed

  • _convictions.md advisory contract (PR #229, retiring PR #155). The Epic #142 Phase A _convictions.md advisory primitive shipped in #155 was removed after Phase B verdict surfaced no canary signal to justify it. Kill-switch removes the snippet, command-doc references, and tests. The same surface area is being re-explored under a different (in-progress) design.
  • anvil:memo per-skill --polish BRIEF.md scaffolding (rolled into the new --polish flag landed in #206 above; pre-#206 partial scaffolding removed in the same PR).

Fixed

  • memo-migrate RenderError inline for consumer-install layout (#199, PR #204). Inlined RenderError in refs_pdf.py so consumer installs don't carry a dangling import.
  • Installer Claude shim depth-1 placement (#138). Shims must land at depth 1 of .claude/commands/ so /anvil-*:* commands actually register; the installer was previously placing them too deep.
  • anvil:deck auto-shrink tests gated on [auto_shrink] extra (#115, PR #116). Without the gate, the auto-shrink tests fail when the optional [auto_shrink] extra isn't installed.
  • matplotlib parse_math anti-pattern guidance (#125). Docs note steering away from parse_math=True in figure-side matplotlib code.
  • CLAUDE.md refresh post-0.1.0 (#112). Refresh of stale sections after the first installable release.

[0.1.0] — 2026-05-30

First installable release. Anvil moves from skeleton to a working framework with 8 shipped skills, a maturing anvil/lib/ substrate, and active use by the 2AM Logic Studio canary. 30+ PRs landed in two days of canary-driven development. See WORK_LOG.md for the chronological merge record.

Added — Skills (8)

  • anvil:memo — investment memos, internal documents (Markdown).
  • anvil:pub — research papers with venue-pinned rubrics (NeurIPS / Nature / arXiv overlays).
  • anvil:report — customer-facing technical reports (Markdown / LaTeX → PDF) with mandatory audit + CUSTOMER-READY promotion gate.
  • anvil:deck — investor pitch decks (Marp Markdown → PDF).
  • anvil:slides — talk / conference slides with speaker notes (Marp Markdown → PDF + handouts).
  • anvil:ip-uspto — USPTO non-provisional utility patent applications (LaTeX → PDF) with 9-check pre-flight including render-gate.
  • anvil:installation — experiential / installation artwork concept proposals (LaTeX → PDF).
  • anvil:proposal — buildable-system proposals (pre-contract bookend to anvil:report); collapses the "internal build spec" case via customer_kind: internal.

Each skill ships a complete draft → review → revise → (audit) → figures lifecycle, an 8-dimension /40 rubric, opinionated templates, a worked example thread, and tests.

Added — anvil/lib/ framework substrate

  • snippets/ (10 markdown files) — pure-markdown conventions every skill reads (progress, timestamp, version_layout, thread_state, state_machine, rubric, critics, scorecard_kind, audit, cite).
  • review_schema.py + .json — typed _review.json contract with kind ∈ {judgment, tool_evidence, vision} discriminator; JSON Schema export.
  • critics.py — sibling-critic discovery, aggregation, verdict computation; legacy-shape adapters for the memo prose triple and ip-uspto hybrid.
  • convergence.pycheck_stable + decide_termination; STALLED verdict for plateaued threads.
  • cite.py — DOI + arXiv resolver, BibTeX writer, idempotent refs.bib; stdlib only.
  • rubric.py + rubric_schema.json — pydantic models + venue-pinned overlay discovery (<thread>/.anvil.json: venue → optional advisory rubric).
  • render.py — Marp → PDF, PDF → PNGs, pandoc → PDF, matplotlib figure walker; check_*_available() preflight helpers for mmdc / pdfjam / auto-shrink dep set.
  • vision.pyVisionCritic + VisionRubric; injectable callback for offline/CI use.
  • render_gate.py — deterministic gate over compiled PDFs (page-fit + overfull boxes + compile success + placeholder scan); LaTeX-skill analog of marp_lint.
  • figures/palette.py + anvil.mplstyle + mermaid-theme.json — shared brand-palette substrate with 6 named tokens (navy / ink / muted / rule / bg-section / bg) + 4 semantic mermaid classDefs (anvil-accent / anvil-muted / anvil-warning / anvil-success) + per-glyph Unicode fallback for matplotlib.
  • marp/config.yml — pinned Marp config (MathJax + html; mmdc → PNG is the documented working diagram path).

Added — Per-skill rendered-artifact (vision) critics

  • deck-vision, slides-vision, pub-vision, report-vision, ip-uspto-vision — VLM critique of rendered PDFs / drawings. Each composes a skill-appropriate VisionRubric (e.g. ip-uspto-vision covers USPTO drawing requirements: reference numeral legibility, line weight / contrast, label placement, figure-number visibility, cross-reference accuracy).

Added — Deterministic source-side lint

  • marp_lint (deck + slides) with 5 named rules: source-side overflow detection (slide-content-overflow), figure-bullet stack detection, ask-slide H1+H2 detection, italic-supporting-line word-budget (figure-italic-supporting-line-too-long), and the suppression directive <!-- anvil-lint-disable: <rule> -->.

Added — Installer + ergonomics

  • scripts/install-anvil.sh with --skills= filter, --dry-run, --check-deps (covers marp, mmdc, pdfjam, pdftoppm, xelatex, pandoc); quoted-path safety (removed all 7 sh -c indirection sites); honest dry-run output.
  • pyproject.toml — Anvil's first declared Python dep file, uv-shaped. Base dep: pydantic>=2.0 (load-bearing for the schema layer). Optional extras: [auto_shrink] (Pillow + numpy for the Marp auto-shrink detector). Documented dep philosophy: subprocess-only by default; Python deps for genuinely-better-than-subprocess detection only.

Added — Repo management

  • AGENTS.md — Loom agent-archetype reference for ongoing development.
  • ROADMAP.md — mission, design philosophy, current state, near-term themes.
  • WORK_LOG.md — chronological record of merged PRs and closed issues.
  • WORK_PLAN.md — prioritized backlog generated from current label state.
  • .claude/commands/loom/release.md adapted from Loom for anvil's actual structure (2 version-bearing files, no build workflow).

Added — Tests

  • tests/lib/ — full coverage for schema, critics, convergence, cite, rubric, render, vision, render_gate, figures.
  • tests/scripts/ — install-script regression armor (quoted-path safety, dry-run honesty, --skills= validation, version drift, version.sh set round-trip).
  • Per-skill tests under anvil/skills/<skill>/tests/ — skill-local lint, vision-critic, and template-correctness tests.
  • CSS-drift guard test enforces palette tokens stay in sync between palette.py and anvil-deck.css :root.
  • Version-drift guard test enforces CLAUDE.md and pyproject.toml stay in sync.

Fixed

  • Mermaid diagram silently rendered as raw code in Marp PDFs (regression introduced by inline-mermaid-as-default design); switched to mmdc → PNG as the documented working path with proactive preflight.
  • White-on-white table rendering on _class: ask slides (Marp default-theme cell-bg leak).
  • Deck draft and narrative critic recommended conflicting slide orders.
  • Install script broke on target paths containing ' (single-quote injection via sh -c).
  • Install --dry-run emitted misleading ok: ... confirmations of actions that didn't happen.
  • Bare --skills= silently fell through to install-all-skills (argument-validation gap).
  • Phantom completed phase state introduced by a malformed Markdown table in lib/snippets/progress.md.
  • Report ack-file substring matching was too lenient (now requires a structured YAML token + sha256 verification).
  • Report auditor allowed unreachable external citations to pass (now a critical_flag).
  • Report reviewer didn't check whether report.pdf existed despite the figurer claiming Dim 7 scored its existence.
  • IP-USPTO critic phase names in _progress.json snippets used generic "review" instead of the critic's own tag (s101 / s112 / claims / priorart).
  • Cross-skill pytest filename collision (deck vs slides test_marp_*.py) — completed __init__.py package chain across all skill test directories.
  • Slide-archetypes "ONE italic supporting line" guidance was line-counting; replaced with explicit word/character budget (≤18 words / ≤108 chars) + lint detection of overlong supporting lines under figure refs.

Changed

  • release.md skill adapted for anvil's actual version-bearing files (was inherited verbatim from Loom with 5 wrong file paths).
  • scripts/version.sh now tracks both CLAUDE.md and pyproject.toml with check reporting drift.
  • README rewritten from "Alpha. Skeleton only" to a real installation + repository-layout + working-with-anvil guide.
  • Deck _class: ask slide template no longer uses H1+H2 stack (overflowed 16:9); single H2 + inline use-of-funds paragraph.
  • Deck figure-bullets idiom replaced with figure + one supporting line (Market / Traction / Financials templates updated; lint enforces word budget).
  • Marp config pinned at framework level: MathJax not KaTeX; html: true for inline content; --config-file anvil/lib/marp/config.yml.

Status

v0.1.0 is the first installable release. Anvil works on a single laptop with no GitHub account; renderer dependencies (marp / mmdc / pdfjam / pdftoppm / xelatex / pandoc) are checked by install-anvil.sh --check-deps. Active development continues against canary friction — see WORK_PLAN.md and the open-issues backlog for what's next.

Next

  • Per-skill audit-command migrations to emit typed _review.json (pre-date the #29 codification).
  • Markdown-appropriate length-proxy gate for anvil:memo if canary friction emerges (memo is markdown-first; no PDF page-fit contract).
  • Per-skill lib/ extraction to anvil/lib/ once duplication patterns are observed across skills (e.g. marp_lint, auto_shrink_detector, report's pdf_freshness).

[0.0.1] — 2026-05-28

Added

  • Initial repository skeleton.
  • Vision, design principles, and planned v0 skill catalog in README.
  • MIT license.
  • Project-level CLAUDE.md for AI session context.
  • Directory structure for anvil/{skills,lib,templates,roles} and scripts/.
  • Minimal scripts/version.sh (manages CLAUDE.md version string only; will grow as more version-bearing files appear).

Status

  • Alpha. No installable functionality. No skills yet implemented.

Next

  • Implement v0 skills per the catalog in README.
  • Extract framework lib/ from observed duplication after the first few skill implementations land.
  • Implement scripts/install-anvil.sh.