Skip to content

Every spell surface shipped React's dev build and died when left open — fixed - #106

Merged
ichabodcole merged 18 commits into
mainfrom
develop
Sep 17, 2026
Merged

ichabodcole merged 18 commits into
mainfrom
develop

Conversation

@ichabodcole

Copy link
Copy Markdown
Owner

If a Spellbook surface ever went blank on you after sitting idle, this is why, and this release fixes it. Nothing was lost when it happened — only the browser tab died; the local background process serving it, and your files on disk, were untouched. Reloading the tab brought it back.

A quick orientation for anyone reading this without the repo open: a spell is one tool in this plugin, and its surface is the local web page it opens in your browser. Every spell you can install was affectedastrolabe, bounty, digestify, glamour, grapevine, imago, magpie and scriptorium. (A ninth, mind-mapper, is built but deliberately unpublished, so it was fixed too but nobody could have hit it. Where this note says "nine", it means bundles the build produces; where it says "eight", it means spells you can actually install.)

How to get it, and how to tell

At the time of writing this is not released yet — it becomes 3.0.1 when this merges and release-please cuts it. Until then the latest published version is 3.0.0, which has the bug.

Once 3.0.1 is out, update the plugin as usual. Then restart any spell you had running. Each version installs into its own directory, so a daemon still running from the 3.0.0 install keeps serving the old bundle no matter what you have downloaded — updating alone is not enough. Reload the browser tab afterwards.

To confirm you are on the fixed build: the surface bundles are roughly a third smaller, so plugins/spellbook/skills/<spell>/dist/index-*.js inside a 3.0.1 install will be visibly smaller than the 3.0.0 one, and will not contain the string logComponentRender.


The rest of this note is for contributors. If you just wanted the fix, you have it above.

What went wrong

v3.0.0 shipped React's development build on every surface, and a user found it rather than any check of ours.

Bun.build leaves process.env.NODE_ENV unset, so React's export conditions resolve to the development entry. That build carries profiling instrumentation: logComponentRender writes a performance.measure() entry, carrying a payload the browser must clone, on every component render — into a buffer nothing ever clears.

So any surface that re-renders on a timer leaks while nobody is touching it. scriptorium re-renders every 30 seconds purely to keep a "5 min ago" label honest; the buffer fills with the human doing nothing, until the browser can no longer allocate for the clone. The tab then dies inside React's commit phase (DataCloneError: … out of memory, followed by Should not already be working).

Time-to-crash scales with how much is on screen, and that is why the bug escaped us. A one-document test session survived an hour; a real working set died in minutes. Every short test we ran looked perfectly healthy.

buildSurface() is the single shared build for all nine surfaces, so one missing option was roster-wide rather than one spell's bug.

What was delivered

The fix, and the rebuilt bundlesad995265. buildSurface() now pins process.env.NODE_ENV to "production", and all nine shipped bundles are recompiled in the same commit — so an already-installed spell gets the fix, not just the next build. Surface bundles went 13.33 → 9.18 MiB (−31%) as a side effect of dropping the development build's dead weight.

A check so it cannot recur unnoticeda4974d3c, grimoire/surface-build-mode-ward.test.ts. A ward, in this repo, is a test that guards a house rule rather than a feature. Nothing here had ever read a shipped bundle to see which build it was. This one scans every shipped surface bundle for three identifiers that exist only in React's development build — logComponentRender, logComponentEffect, and the Components ⚛ profiling track — and fails on any hit. It passes on everything in this release. Pointed at the old v3.0.0 bundle it finds all three markers and fails — so had it existed, this would never have shipped.

A documentation-tooling layer — seven commits, and by volume the largest part of this release, though no user ever sees it. The project-docs scaffold went 4.4.0 → 8.1.0, bringing a schema, a document catalog, and a pdocs CLI under scripts/pdocs/ (a2740d20). 273 documents were backfilled with a description and 25 uncatalogued pages linked up, taking pdocs report from 353 missing fields to zero (45345651, with 046dd936 carrying an exclusion it depended on). A docs lint was then composed into the repo's check script and its pre-commit hook (c2802a30), sixteen dead cross-references were repaired (33f57b9b), and only then was the lint switched from warn-only to enforcing (49b4d0b9).

A live erratum against the published v3.0.0 note55c7a566. gate is this repo's CI check — the one that runs the build, lint and tests. That note tells readers gate is not enforced on main and that a red gate will not block a merge. That is false and has been since 2026-09-01: the branch ruleset "Main Gate" requires gate today (re-verified against GitHub while writing this). The published note sits in a merge commit body and cannot be edited, so the correction lives in the tree instead. If you read the 3.0.0 note, disregard that paragraph.

Two smaller corrections73f83dbd makes the landing tool state that its cited shas: 0 means "none that I can see", since it greps only tracked markdown in this repo. 0db525b8 tracks the vendored shadcn skill as real files with a lockfile entry, where it had been a symlink into an ignored directory that existed on exactly one machine.

Decisions worth knowing

  • define only — minification is still deliberately off. Minifying would strip the same instrumentation and shrink the bundles further, but it is deferred by a standing decision (entry D7 of docs/architecture/house-conformance-register.md, the repo's log of open architectural rulings) behind a precondition this fix does not meet: several checks read compiled output as text, and none has ever been calibrated against renamed identifiers. The two options were measured and proved separable — pinning NODE_ENV removes the instrumentation while leaving the bundle's text shape untouched — so the crash fix ships in full and the deferred decision stays deferred, unbreached. a574e22e records that.
  • ⚠ The two bundle-size figures are not comparable. D7 previously carried 13.21 → 7.25 MiB as the minification estimate; this release measured 13.33 → 9.18 MiB from pinning NODE_ENV. Different levers, and the tree has gained a spell since. The old pair has been retired rather than re-quoted.
  • The docs lint was switched on in two steps on purpose. It was wired while still warn-only, and only flipped to enforcing once the sixteen dead references were clean — so anyone bisecting can tell "the references were fixed" from "the lint started biting".
  • ⛔ Turning the lint on pushed CI red, and a green local run was why. 49b4d0b9 passed locally and failed in CI: three markdown links pointed outside the repository — two of them absolute paths into a home directory — so they resolved on their author's machine and on no clone anywhere. The repair is eb012b7f, verified in a clean clone, which is the only place such a check means anything. Seven similar-looking links that escape docs/ into .anthill/ are correct and were left alone, because that directory is tracked. CI is green on this branch.

Known limits of what shipped

These are properties of the work in this release, not work left undone.

  • The new ward matches identifiers, not behaviour. If React renames those three internals, the ward goes green without noticing anything. It also reads surface bundles only, and says nothing about the daemons.
  • The ward proves the instrumentation is absent, not that a tab survives. The idle crash is fixed and independently confirmed by leaving a rebuilt surface open; the ward's own guarantee is narrower than that.
  • The docs lint carries one exclusion, docs/projects/scriptorium/SKILL.draft.md, which holds a different kind of frontmatter.

⚠ One unreviewed risk

Tracking the shadcn skill puts vendored third-party content into the repository. Its published installer assessment reads Gen Safe and Snyk Low Risk but carries one Socket alert (https://skills.sh/shadcn-ui/ui). Nobody has reviewed that alert. It is surfaced here rather than quietly accepted, and it wants a decision.

What this deliberately does not reach

  • No new spells and no new features. The same eight installable surfaces, built correctly.
  • Minification is still off (D7). The 31% the bundles did lose came from dropping the development build; minifying on top of that would take more, and remains deferred.
  • No dependency upgrades. Eleven root packages and two per-spell ones are behind — including two majors, the cn package (unrelated to the repo's own src/kit/lib/cn.ts of the same name) and digestify's marked — filed as one measured sweep rather than done piecemeal (889de410, docs/backlog/2026-09-15-dependency-upgrade-sweep.md).
  • CI still runs gate twice per push, which is pre-existing and unchanged here. Filed rather than fixed, because the obvious fix is a trap: dropping the pull_request trigger would hang every PR into main on a required check that never reports.

Version: the conventional-commit types in this range are one fix(build) and one fix(docs), with the rest docs(, chore( and test( — so release-please will cut this as a patch, 3.0.1.

🤖 Generated with Claude Code

ichabodcole and others added 18 commits September 14, 2026 20:08
…said otherwise

Ruleset "Main Gate" (`22059514`) was created 2026-09-01T18:39:36-07:00 — the day
after the backlog item asking for it was filed — is `enforcement: active` on
`~DEFAULT_BRANCH`, and requires the status check `gate`. It ran and passed on
both PRs that went into `main` for the 3.0.0 release (#104 4m38s, #105 4m25s).
Nobody closed the item, so for two weeks the tree asserted the opposite.

⛔ AND IT REACHED A PUBLISHED RELEASE NOTE. The 3.0.0 note lists as a standing
limit that "`gate` has to be marked required in GitHub's settings, and no agent
can do that — until a human does, a red `gate` does not block a merge", and adds
that a check which caught two real defects on its first outings "is not yet
allowed to block a merge". Both are false, and they are frozen in the
`develop`→`main` merge commit body. The note cites the backlog item by filename
for the detail, so closing that item with the evidence is the only correction
that reaches a reader once the merge commit is published.

⚠ THE REASONING ERROR, WHICH IS THE PART WORTH KEEPING. The note said of the
workflow header's assertion: "nothing in this tree can confirm it — treat that
sentence as an instruction to a human, not a statement of fact." The first half
was true; the conclusion was not. `gh api repos/<owner>/<repo>/rulesets` reads
repo configuration directly, and the same session used `gh` freely for PRs, runs
and logs the whole time. **"Not verifiable from the tree" was silently upgraded
to "not verifiable"**, and a stale backlog item was then taken as current state
because nothing in the tree contradicted it.

The investigation's clause that seeded it ("not something an agent can land or
verify") is corrected in place rather than rewritten: landing is Cole's,
verifying is one API call. `.github/workflows/ci.yml`'s header was right all
along and is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiZGj5ZTDSZi1mB8YtuRcx
…tall, and the doubled gate

Filed on Cole's ruling that the `@base-ui` bump is not its own ticket: "maybe we
just do a general dependencies check — like, should we be upgrading the other
dependencies aside from that? If so, we can do that all as one scope of work."

`2026-09-15-dependency-upgrade-sweep.md` — measured rather than proposed.
Eleven root packages and two per-spell ones are behind, and the gaps are not
uniform: `lucide-react` is 29 minors back, `cn` (0.2 → 0.3) and digestify's
`marked` (12 → 18) are majors, and `@base-ui/react` 1.6 → 1.8 is the one that
already has a reason. Written with the three traps that make this non-trivial:
every surface hash moves so `dist-check` wants a rebuild commit across nine
spells; `bun` is pinned in two places that must agree and moves bundler output
repo-wide with no source change, so the toolchain is its own step or a real
regression is indistinguishable from a bundler diff; and Biome 2.4 → 2.5 plus
Prettier 3.8 → 3.9 can red the gate and reformat committed markdown.

`2026-09-14-nested-node_modules-shadows-the-root-install.md` — renamed from
`2026-09-14-base-ui-1-8-bump-and-the-nested-node_modules-footgun.md`, which
carried two items in one file. The upgrade half moved to the sweep; what stays
is the mechanism. ⚠ Commit `c3ede8ae` cites the old filename, so the split is
announced in the file's own header rather than left for a reader to work out.

`2026-09-15-ci-runs-gate-twice-on-every-push.md` — `ci.yml` triggers on both
`pull_request` and `push`, so a push to `develop` with the release PR open
starts two identical ~4m30s runs. Measured across all three pushes of this
release. ⛔ The item leads with the constraint rather than the fix, because the
obvious fix is the dangerous one: `pull_request` is what makes the REQUIRED
check report, and dropping it hangs every PR into `main` forever on "waiting for
status to be reported". It also records that the conventional `concurrency` block
keyed on `github.ref` does NOT deduplicate the pair — `push` and `pull_request`
carry different refs for the same commit — so the paste-ready answer is wrong
here. No fix chosen, and doing nothing is named as defensible if the minutes are
not actually scarce.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiZGj5ZTDSZi1mB8YtuRcx
…n 4.4.0 → 8.1.0

Ran `update-project-docs`'s v2.6→v2.7 migration (project-docs 3.12.0). Committed
on its own, before the backfill, so this reads as "the layer was installed"
rather than that plus 273 hand-written descriptions.

WHAT ARRIVED: `docs/SCHEMA.md` (the contract), `docs/index.md` (the catalog
skeleton, ours from now on), the `pdocs` CLI under `scripts/pdocs/`, a new
`docs/cycles/` type, frontmatter on 272 documents, 19 templates refreshed, and
`.project-docs.json`. Both version markers moved 4.4.0 → 8.1.0 together.

⛔ THE GATE IS DELIBERATELY OFF — `lint.adopting: true`. The lint wants
`description` on every document and the codemod does not write one, so a fresh
migration cannot pass. `pdocs report`: 353 missing fields across 273 of 290
documents, 272 of them `description`. Turning it on is a later commit.

⚠ `docs/releases/` HAD TO BE DECLARED, and the stop that forced it is the script
working. An undeclared folder is read as LIBRARY — the strictest tier — with
every page of unknown type, so the preflight refuses rather than let that happen
quietly. Declared `workbench` with `types.releases: "release"`, matching
backlog/ and reports/: a release note is a dated artifact, not library reference.

⚠ `scripts/pdocs/` IS EXCLUDED FROM BIOME AND PRETTIER, and that is not
cosmetic. `docs/SCHEMA.md` classes it OWNED — "overwrites, every time" — and
Biome had already reformatted 16 of its 17 files. The consequence is a standing
fight: every migration's `refreshOwned()` copies the scaffold's bytes back over
them, and the formatter then wants to redo all 16 on the next commit, forever.
Biome also rejects the upstream `(tags[tag] ??= []).push(...)` outright. Same
reason `.prettierignore` already pins `.claude/skills/acc/`.

⛔ AN EARLIER VERSION OF THE PARAGRAPH ABOVE STATED THE MECHANISM BACKWARDS, and
the correction came from project-docs upstream on the `spellbook-upgrade`
channel. It claimed the refresh would read the formatted files as locally edited
and KEEP our formatting, silently skipping the upstream fixes. That is the
SEEDED reconcile — templates, compared by hash against `docs/.pdocs-seed.json`.
Owned files have no such branch: `migrate-v2.8-to-v2.9.ts:415` is a bare
`cpSync(..., { recursive: true })`, and `migrate-v2.9-to-v2.10.ts` compares
bytes only to REPORT "already identical" versus "replaced". Verified in both
scripts rather than accepted. Fixes are never skipped; formatting is always
clobbered. Recorded because the wrong version was the more alarming one, and an
alarming claim that is false costs more than a dull one that is true.

⛔ AND THE LAYER DID NOT TYPECHECK HERE UNTIL UPSTREAM FIXED IT. Seven errors in
`docs-lint/index.ts`, all one class — an indexed or destructured value widened
to `T | undefined` — because this repo runs `noUncheckedIndexedAccess` and the
scaffold's own tsconfig did not. Filed as
ichabodcole/project-docs-scaffold-template#176 with an isolation table (7 errors
with the flag, 0 without); fixed upstream at `cce845e` and copied in, which is
byte-for-byte what an Owned refresh does.

⚠ NOT TAKEN ON TRUST. The upstream agent named three files; `diff -rq` over the
whole directory confirms exactly those three differ and nothing else, the diffs
are only the narrowing (`?.[1]`, `charAt`, a destructure default, three
statements, one dropped import), and the claim "zero errors under your tsconfig"
was re-run here rather than believed. The repo's own ward now reports 0 errors
across 35/35 areas and 661/661 files.

⚠ THE OBVIOUS SHORTCUT WAS TRIED AND IS NOT AVAILABLE. Excluding
`scripts/pdocs` from `tsc` reds `type-check-ward`'s coverage cell (D42) —
644 examined of 661 — which is exactly what that ward exists to catch. The
justification comment written for it was also false: gate-honesty scans
`skills + src`, not `scripts/`, so the blindness would have been invisible
rather than printed. Reverted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiZGj5ZTDSZi1mB8YtuRcx
… its 25 pages

Step 2 and 3 of the v2.6→v2.7 guide's "After the script". `pdocs report` goes
from 353 missing fields across 273 documents to ZERO, and `pdocs orphans` from
25 to none. The gate stays off (`lint.adopting: true`) — turning it on is the
next commit, deliberately separate.

WRITTEN BY SEVEN SUBAGENTS, ON COLE'S CALL that descriptions do not need heavy
reasoning: haiku for the formulaic bulk (backlog, project artifacts, sessions,
investigations, fragments, reports) and sonnet for the library tier
(architecture, playbooks, memories, the root pages) where a wrong description
misleads future recall rather than merely reading flatly. Sharded into disjoint
file sets so nothing raced.

⛔ THEIR REPORTS WERE NOT TAKEN AT FACE VALUE, AND THAT FOUND SIX REAL DEFECTS.
Every `lifecycle` VALUE CHANGE — as opposed to a filled-in blank — was checked
individually against the repository, because a wrong one is silent: it reads as
a considered judgement forever.

- **Four shipped projects were marked `draft`** by the projects-a agent, whose
  stated reasoning was that "none were marked implemented, indicating ongoing or
  preparatory work" — inferring status from the ABSENCE of a marker rather than
  from the tree. glamour-acc-l0, glamour-conversion and grapevine-conversion all
  merged (`389d088f`, `e3d80dc5`, `8fe85c12`) and shipped in 3.0.0. Corrected to
  `implemented`, and glamour-conversion's plan to `completed`.
- **Two fragments were marked `open`** with the reasoning "no `promoted` or
  `dropped` needed", when `mind-mapper-spell-concept` became
  `docs/projects/mind-mapper` and `spellbook-aesthetic-and-rebrand` became
  `docs/projects/spellbook-rebrand`. Both are `promoted`. The other five were
  checked the same way and are correctly `open` — no successor exists for any.

⚠ AND TWO OF MY OWN CLAIMS WERE WRONG, WHICH IS WHY THE CHECKS RAN TWICE.
I first attributed eight lifecycle overwrites to the backlog-a agent and said it
had exceeded its brief; it had not. The edits were backlog-b's, still in flight
when I sampled the diff, and its seven `open`→`done` plus one `open`→`promoted`
are all correct and sha-cited. Separately, a `lifecycle: open # …` I flagged as
a leaked template comment appears only in `TEMPLATE.md` files, where the inline
vocabulary is deliberate.

⛔ THE CATALOG BUG WAS MINE AND THE LINT CAUGHT IT IMMEDIATELY. Inserting
entries after the last line starting with `- [` SPLIT an existing wrapped entry:
`PROJECT_MANIFESTO`'s hook truncated to seven words and its orphaned tail was
absorbed onto the end of the line I had just added. `STALE HOOK` named both
within seconds — which is exactly the drift that check exists for, since nobody
re-reads a catalog. Fixed by rewriting the section rather than inserting into
it, and every later section was written wholesale for the same reason.

Gate 2544 pass / 0 fail, `dist-check` 0, `pdocs check` clean across both tiers.

⚠ WHAT THIS DELIBERATELY DOES NOT FIX: twelve pre-existing dead cross-references
the new lint surfaces for the first time (`MISSING FILE`). Five point at
projects that merely moved into `_archive/`, three at a
`spell-hardening/plan.md` that appears never to have existed at that path, four
at genuinely deleted files. Separate work from this upgrade, and worth a backlog
item rather than a silent widening of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiZGj5ZTDSZi1mB8YtuRcx
…carry

`45345651` states the docs lint is clean, and that is only true with this
exclusion present — `git add -A -- docs` does not reach a repo-root file, so it
was left in the working tree. Checked out alone, that commit reds on
`docs/projects/scriptorium/SKILL.draft.md`.

⚠ A FOLLOW-UP RATHER THAN AN AMEND, ON PURPOSE. `45345651` has already been
cited to the project-docs upstream agent on the `spellbook-upgrade` channel, and
amending would leave that citation pointing at nothing — the same failure that
cost a correction earlier today when a backlog item named the sha of the commit
it was folded into.

WHY THE FILE IS EXCLUDED. `SKILL.draft.md` is a superseded draft of a SKILL.md,
so it carries SKILL frontmatter (`name:`, `description:`) rather than
project-docs frontmatter. `scripts/pdocs/lint/rules.ts` enforces UNKNOWN FIELD
against a closed key set, so `name` is rejected outright — and giving it
project-docs fields would not help, because the offending key is the one that
makes it a skill draft. Excluding it keeps the history intact and the gate
honest; the alternative was to strip the field that gives the file its meaning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiZGj5ZTDSZi1mB8YtuRcx
…t arrived

`pdocs check`'s `MISSING FILE` rule found twelve markdown links in `docs/` that
point at files which do not exist — on its first run, in under a second.

⛔ THE FINDING IS NOT THE TWELVE LINKS. It is that nothing in this repository
had ever checked one. They accumulated over months of moving projects into
`_archive/` and renaming folders, and every one survived every review of the
page it sits on, because a reader who does not click does not notice.

Classified rather than listed, because each class wants a different repair:
five targets merely MOVED into `_archive/` and can be re-pointed; three name
`projects/spell-hardening/plan.md`, a path that appears never to have existed
(that project has `sprints/<n>/plan.md`), so the fix has to decide which sprint
was meant or delete the sentence; four are genuinely gone, one of them
`.anthill/retro.md`, which is outside the docs root and gitignored — there the
honest fix may be to stop linking it at all.

⚠ Filed rather than fixed, deliberately. It arrived inside the project-docs
upgrade; folding twelve judgement calls across four folders into a 273-file
commit would have made that commit unreviewable, and nine of the twelve sat in
folders subagents were editing at the time.

⚠ One correction carried into the item itself: an earlier pass of the survey
classified `./2026-08-05-cli-stdout-truncation-on-pipe.md` as archived, because
the classifier tested the `.` in `./…` as a directory name. It is gone, not
archived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiZGj5ZTDSZi1mB8YtuRcx
…ot undo #176

Ran `update-project-docs`'s v2.8→v2.9 migration. Net change on disk is one file:
`docs/.pdocs-seed.json`, recording the sha256 of 21 templates. Phase 5 is
explicit that it has to be committed — "a manifest that never reaches the
repository cannot be read next time" — and it is what lets a later migration
update a template only while nobody has edited it.

⛔ RUN WITH `--scaffold-dir`, BECAUSE THE OBVIOUS INVOCATION WOULD HAVE REVERTED
UPSTREAM'S FIX. Phase 3 (`refreshOwned`) copies the scaffold's `scripts/pdocs/`
over ours UNCONDITIONALLY — no comparison, by design. But it generates that
scaffold from the PUBLISHED template, and the fix for
ichabodcole/project-docs-scaffold-template#176 is on upstream's `develop`,
unreleased. Verified rather than assumed:

    cce845e reachable from origin/main     NO
    cce845e reachable from origin/develop  YES
    main's docs-lint/index.ts              1 × `if (m) s.add(slug(m[1]))`
    develop's, and ours                    0 ×

So a plain run today copies the PRE-FIX file over the fixed one and re-breaks
the type-check ward — and would report `✓ scripts/pdocs/ refreshed` while doing
it, because that phase's post-check is presence, not content. Generated a
scaffold from upstream's `develop` with cookiecutter, confirmed it carried the
fix and that its `scripts/pdocs` was byte-identical to ours, then passed
`--scaffold-dir`. Reported upstream as a guide gap: `--scaffold-dir` is
documented as a way to skip the network, not as a way to avoid a downgrade.

VERIFIED WITH THIS REPO'S OWN INSTRUMENTS, not the migration's self-report,
because the owned files had just been replaced: type-check ward **0 errors, 35
of 35 areas, 661 of 661 files examined**; gate **2544 pass / 0 fail**;
`dist-check` 0; `pdocs check` clean; `pdocs report` 0 missing fields across 0 of
290 documents.

⚠ TWO PHASE-REPORT WORDINGS WORTH KNOWING, both reported upstream. Phase 3 says
`✓ docs/SCHEMA.md refreshed (both new sections present)` — ours diffed ZERO
lines, so nothing was refreshed; the check is presence. And phase 4 formatted 21
templates while 29 files here are template-shaped by name: the exact-name rule
selects 21, which is the rule working rather than a gap.

v2.9→v2.10 does not apply — `isSeeded` is already in `scripts/pdocs/lint/rules.ts`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiZGj5ZTDSZi1mB8YtuRcx
…he same way the links were

The item filed an hour ago claimed twelve dead cross-references. It is sixteen:
twelve `MISSING FILE` plus **four `MISSING ANCHOR`** — links to a `#section`
that is not a heading. The first survey grepped only for `MISSING FILE`, so the
anchors were never in the population being counted.

⚠ THE MISCOUNT HAS THE SAME SHAPE AS THE DEFECT IT DESCRIBES. A survey that
greps for one problem kind reports one problem kind, and reads as complete. The
authoritative population is what the tool prints, not what a reader thought to
look for — which is the argument for the lint in the first place.

The four are one cluster in `sprints/03-what-close-takes-with-it/` (`#a5`, `#a7`,
`#b` in `decisions.md`, and `./decisions.md#b` from `plan.md`), reading like
headings renamed after the links were written.

AND THE ITEM NOW CARRIES WHAT IT BLOCKS, MEASURED. Flipping `lint.adopting` to
false locally and reverting:

    lint.adopting: false  →  pdocs check exits 9, 16 problem(s)

So these sixteen are exactly what stands between this repository and a docs gate
that can fail. `adopting: true` is the only thing holding the gate at 0 today —
the project-docs upstream agent's warning, confirmed here rather than taken —
which means the wired check runs without teeth until they land. Deliberate, and
the guide's own advice, but not to be mistaken for the gate being live.

Renamed the file to match the count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiZGj5ZTDSZi1mB8YtuRcx
…y line

Step 4 of the v2.6→v2.7 guide's "After the script", and the last commit of the
upgrade. Approved by Cole, and read against its own guidance by the project-docs
upstream agent on the `spellbook-upgrade` channel ("approved as-is, all three
parts").

    "check":      biome check --error-on-warnings . && bun run docs:check
    "docs:check": bun scripts/pdocs/cli.ts check --format text

⛔ COMPOSED, NEVER REPLACED — which is upstream's own issue #170. A `docs:check`
that REPLACES `check` silently drops biome from every gate run, and `gate` is
`build && check && test`, so it inherits the docs check through that line with
nothing to edit. CI runs `bun run gate`, so no workflow change either.

⛔ THE HUSKY LINE IS ITS OWN, NOT ROUTED THROUGH lint-staged. `pdocs check`
takes no path arguments, so lint-staged would append the staged paths as argv
and it would lint the whole docs root regardless — the paths are noise at best.
⚠ And the guide's snippet does not fit this repo: it probes with
`grep -qx 'npm run check'`, while this hook was the single line `bunx
lint-staged`, so following it would have added nothing at all, silently.
Reported upstream; the probe is being made conditional.

⚠ THE GATE IS WIRED BUT CANNOT YET FAIL, AND THAT IS NOT AN OVERSIGHT.
`lint.adopting: true` holds `pdocs check` at exit 0 while sixteen dead
references stand. Measured by flipping it locally and reverting:

    lint.adopting: false  →  pdocs check exits 9, 16 problem(s)

So wiring it now makes the output a progress meter, which is the guide's advice,
and the sixteen are filed as their own work
(`docs/backlog/2026-09-15-the-docs-lint-found-sixteen-dead-references.md`).
**Flipping `adopting` is a separate commit that must not land until that is
clean** — with them open it would red every commit from that one on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiZGj5ZTDSZi1mB8YtuRcx
Cole's call to fix them inside the upgrade rather than defer. Before:
`pdocs check` exit 9, 16 problems. After: exit 0, "OK — no problems."

    4  anchors pointing at non-headings  → the lint's OWN slugs, read out of
       `headingSlugsOf` rather than guessed from GitHub's algorithm
    6  targets moved into `_archive/`    → path re-pointed
    3  the split-up monolithic plan      → `spell-hardening/roadmap.md`, the
       overview that survived the split into `sprints/<n>/plan.md`
    2  genuinely deleted                 → unlinked, wording kept
    2  illustrative examples in a block quote → unlinked; they were teaching a
       convention, not referencing a file, so inventing a target would be worse

⚠ THE COUNT WAS WRONG TWICE, BOTH TIMES TOO LOW, BOTH FOR THE SAME REASON.
"Twelve" came from grepping `MISSING FILE` and never seeing 4 `MISSING ANCHOR`.
"Five moved to `_archive/`" came from assuming
`2026-08-05-cli-stdout-truncation-on-pipe.md` was deleted when it sat in
`backlog/_archive/` — six. Both were corrected by asking the tool for its own
population instead of a regex, which is the lesson the item was filed to make.

⛔ AND FIXING THEM BROKE THE GATE ONCE, THROUGH THE FORMATTER. Normalising the
edited markdown with a blanket `bunx prettier --write docs` reformatted 19 files
that are NOT markdown — four `.report.json`, four `.ts`, eleven `.html` mockups —
every one of which Biome owns in this repo, which is the hazard
`grimoire/house-style.md` already records as "format with biome, not prettier".
Eight Biome errors and a red gate, on files this change never meant to touch.
Collateral reverted, the thirteen real edits kept, `biome check` back to exit 0.
Reported upstream as a guide gap: `docs/` is not a markdown-only directory, and
the migration's own phase 4 is scoped to templates for precisely this reason.

⚠ One claim I sent upstream about this was itself wrong and is corrected on the
channel: I reported that the lint's problem rows use inconsistent path origins
because `DEV_KICKOFF.md` printed without a `docs/` prefix. Every row is
repo-root-relative; that file simply IS at the repo root. What actually cost me
two wrong edits was two tracked files sharing a basename — the other is
`docs/projects/_archive/bounty-agent-usable/DEV_KICKOFF.md` — and my assuming
the reported path was docs-root-relative.

Gate 2544 pass / 0 fail, `dist-check` 0, `biome check` 0, `pdocs check` 0 — the
last two run sequentially after a concurrent run produced a false red (the build
rewrites the same `dist/` paths `dist-check` diffs).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiZGj5ZTDSZi1mB8YtuRcx
The last commit of the upgrade. `lint.adopting: true` was installed by the
v2.6→v2.7 migration so a freshly-migrated tree could not fail its own gate; with
273 descriptions written, the catalog complete and all sixteen dead references
fixed, it has no remaining job.

    before   pdocs check → exit 9, 16 problem(s)
    after    pdocs check → exit 0, "OK — no problems."

⛔ THIS IS THE COMMIT THAT MAKES THE WIRING MEAN SOMETHING. `c2802a30` composed
`docs:check` into `check`, and `gate` inherits it — but under `adopting: true`
that check could only ever exit 0, so it reported without enforcing. From here a
dead link or a missing `description` reds `bun run check`, `bun run gate`, the
pre-commit hook and CI.

Committed separately from the fixes on the project-docs upstream agent's
instruction — "test the flip locally, and only commit the flip when that is
clean" — so a bisect can tell "the references were repaired" from "the gate
started enforcing".

Gate 2544 pass / 0 fail · dist-check 0 · biome 0 · pdocs check 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiZGj5ZTDSZi1mB8YtuRcx
…d red

CI failed on `49b4d0b9` — the push that turned the docs gate on. `docs:check`
exit 9, three problems, all of them links that can only ever resolve here:

    docs/investigations/2026-07-06-astryx-…: ../../../dreamwood/media-buffet/…/proposal.md
    docs/investigations/2026-07-14-agent-co-presence-…: /Users/colereed/Projects/dreamwood/dream-flute
    docs/investigations/2026-07-14-agent-co-presence-…: /Users/colereed/Projects/dreamwood/story-loom

Two are ABSOLUTE paths into a home directory, written as markdown links. All
three point outside the repository, so no checkout anywhere can satisfy them.
Unlinked, with the text naming the external project instead.

⛔ I PUSHED A RED GATE, AND THE LOCAL GREEN WAS WORTHLESS FOR EXACTLY THESE
LINKS. `pdocs check` passed here before the push and passes here now, because
`~/Projects/dreamwood/{dream-flute,story-loom,media-buffet}` exist on this
machine. That is the same defect class as `type-sentinel-probe.ts`'s hardcoded
`typescript` path, caught by CI this morning — and the third "green locally, red
in CI" of the day. Twice now I had the means to check and did not.

VERIFIED THE ONLY WAY THAT COUNTS: a clean clone with no sibling `dreamwood/`,
the fixes applied, `bun install --frozen-lockfile`, then `pdocs check` — exit 0,
`docs-lint: clean`, zero machine-bound problems. A local pass is not evidence
for a link whose target lives outside the repo.

⚠ AND SEVEN SIMILAR-LOOKING LINKS ARE CORRECTLY LEFT ALONE.
`docs/projects/glamour-conversion/plan/*.md` point at `../../../../.anthill/dev/`,
which escapes `docs/` but NOT the repository: `.anthill/` is tracked
(`git ls-files` lists `README.md`, `config.json`, `dev/README.md`), so those
targets exist in CI's checkout too — confirmed present in the clone. Escaping
the docs root is fine; escaping the repository is not. That is the discriminator,
and churning the seven would have been noise.

⛔ THE LINT EARNED ITS KEEP TWICE IN ONE DAY. It is the first instrument in this
repo that ever read `docs/`, so it inherited sixteen dead references (fixed in
`33f57b9b`) and these three machine-bound ones — none of which anything had
checked, all of which survived every review of the pages they sit on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiZGj5ZTDSZi1mB8YtuRcx
`cited shas 0` read as "nothing cites a branch sha". It means "nothing I can
see": the check greps TRACKED MARKDOWN IN THIS REPO, so a sha cited on a
grapevine channel, in another repository's record, in a GitHub issue, or in a
COMMIT MESSAGE on the branch itself is structurally invisible to it.

Measured 2026-09-15 landing `chore/project-docs-upgrade`: it printed
`cited shas 0` and returned ✅ SQUASH-SAFE while four of that branch's SHAs had
been cited to another agent on a channel, and `046dd936`'s own message explained
itself by reference to `45345651`. The verdict was correct about what it could
see and wrong about the branch — a squash would have stranded every one of those
references. `--ff-only` was chosen on that reasoning rather than on the verdict.

⛔ THE SKILL ALREADY SAID THIS AND THE SCRIPT DID NOT. `.claude/skills/land/SKILL.md`
carries the caveat at §1 ("It greps THIS REPO ONLY… structurally invisible"), but
the script is what an agent reads mid-landing, and it printed a bare `0` with no
hint that the number had a boundary. The warning now sits beside the number,
matching the shape of the existing author-count caveat, and fires only when the
count is zero — where the misreading is possible. Suggested by the project-docs
upstream agent on the `spellbook-upgrade` channel, which is itself one of the
places the check cannot see.

Verified both directions: fires on a zero-citation range; silent on `main..develop`,
which has 187 citations and needs no such warning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiZGj5ZTDSZi1mB8YtuRcx
…estroyed

The shadcn skill from skills.sh (`shadcn-ui/ui`) is now tracked: its 15 files
under `.claude/skills/shadcn/` and its entry in `skills-lock.json`.

⛔ THE LOCK ENTRY NEEDED RESTORING BECAUSE I DELETED IT. Cleaning up a probe
commit, I ran `git reset --hard origin/develop` on a tree holding Cole's
uncommitted six-line shadcn entry — a file the standing instruction says not to
touch. `--hard` discards unstaged work with no reflog and no dangling blob; it
was not recoverable, and I checked all 55 dangling objects before saying so. The
right cleanup was to undo the one commit I had made, not to reset the tree.

RE-ADDED WITH THE TOOL RATHER THAN HAND-WRITTEN, because the lock records a
`computedHash` the CLI owns and a fabricated one is worse than a missing entry:

    bunx skills add shadcn-ui/ui --skill shadcn -a claude-code -y

Scoped deliberately: `--skill shadcn` leaves the package's second skill
(`migrate-radix-to-base`) out, and `-a claude-code` matches the actual footprint
— `.claude` is the only agent directory in this repo. ⚠ `skills list` reporting
"Antigravity, Codex, Cursor, Gemini CLI +3 more" is the agents it SUPPORTS, not
places it had installed; I misread that as an eight-agent footprint at first.

⚠ AND THE CONTENT IS UNCHANGED, so "it's fine if it's newer" turned out moot.
`SKILL.md` hashes `deba6c51…` before and after — upstream has not moved since the
2026-09-05 install.

THE LAYOUT CHANGED, AND FOR THE BETTER. It was a symlink into `.agents/skills/`,
which is gitignored (`.gitignore:65`), so the skill existed only on this machine
and a fresh clone would have had a dangling link — the same machine-bound class
that reddened CI twice today. It is a real directory now, as `acc` is, and
survives a clone.

⚠ SUPPLY-CHAIN NOTE, surfaced rather than swallowed: the installer's assessment
reads Gen **Safe**, Snyk **Low Risk**, Socket **1 alert**
(https://skills.sh/shadcn-ui/ui). Tracking it puts that content in the
repository; the alert is unreviewed here and is Cole's call to weigh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiZGj5ZTDSZi1mB8YtuRcx
…e idle

Unset, React resolves to its DEVELOPMENT build, and the development build
carries the Performance Track instrumentation: `logComponentRender` writes a
`performance.measure()` entry, with a structured-cloneable `detail`, on every
component render — into a buffer nothing ever clears.

That turns any surface with an interval into a slow leak with no user in it.
scriptorium's `useNow()` re-renders the DocumentPane subtree every 30s so
"5 min ago" stays true, so the buffer fills while the human does NOTHING, until
the clone cannot allocate: `DataCloneError: ... out of memory`, thrown inside
commitPassiveMountOnFiber, which abandons React's commit phase mid-flight and
leaves the tab dead behind `Should not already be working`.

Shipped this way in 3.0.0 on all nine surfaces — `buildSurface()` is the one
shared build, so the omission was roster-wide rather than scriptorium's — and
found by a consumer whose tab died after a few minutes idle, not by us. Time
to crash scales with subtree size: a one-file test session survived an hour
while a real document set died in minutes.

`define` ONLY, not `minify`, though minify also strips it. Minify is deferred
by Cole under register D7 behind a precondition this fix does not meet — the
instruments that read emitted artifacts AS TEXT have never seen renamed
identifiers. The two are separable and measured: `define` leaves the text
shape intact (scriptorium, 57 module-boundary comments and `src/kit/lib/cn.ts`
present before AND after; 0 of each after minify), so no ward needed
calibrating. Surface bundles 13.33 → 9.18 MiB raw (-31%) as a side effect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The React dev build and the minify lever are separable, and collapsing them
would read as D7 moving when it has not. So the row now leads with the fix —
NODE_ENV pinned, all nine surfaces, the idle crash gone — and then says
plainly that the minify deferral is untouched and its ward-calibration
precondition still unmet, because `define` needed none of it.

It also keeps the near-miss. `_archive/backend-convergence/phase-1b-journal.md`
and `phase-2-journal.md` both wrote down "the unminified DEV React graph" in
September. The dev build was observed twice, recorded as a size fact, and read
by nobody as a correctness defect — the evidence was in the register's own
lineage and the bug still reached a consumer.

And it retires a number before it misleads: D7's `13.21 → 7.25 MiB` was the
surfaces on 2026-09-09, the tree has since gained scriptorium, and `define`
alone has already taken them to 9.18 MiB. Minify's remaining headroom is
whatever it buys on top of that, so the old pair must be re-measured rather
than re-quoted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fix stops it happening again; this stops it happening again UNNOTICED.
Nothing in the tree read a surface bundle for its build mode, which is why
v3.0.0 shipped nine development builds and a consumer found it rather than an
instrument. The ward greps every shipped `dist/index-<hash>.js` for the dev
build's Performance Track writers and fails on any hit — run against the
v3.0.0 artifact it reports all three markers, so it would have been red.

It carries its own blind set, as the sibling wards do: markers not semantics
(a renamed React internal passes), surfaces only, and — deliberately — it is
NOT a minify check. `minify` strips the same markers, but minify is deferred
under register D7, so a ward that conflated them would quietly become a minify
gate the day D7 is decided. It must stay green under both.

The scenario records the judgment that shaped the fix: Cole asked what the
change would SUPERSEDE before it landed, which found D7's deferral and split
one move into two — `define` shipped, `minify` stayed deferred. A small diff
is not a small decision, and the one-line-ness is what stops anyone looking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s the gotcha

`carry-bun-gotchas-forward` is a TEMPORAL rule whose last reinforcement was its
own seed, 2026-05-28 — the state in which a rule about carrying gotchas forward
has stopped carrying any. The row now holds the one that cost us a release.

The gotcha is not the flag, it is the DEFAULT: `Bun.build` leaves
`process.env.NODE_ENV` unset, so React resolves to its development build, and
an unset default emits an artifact that passes every short test and fails only
on a clock. That is the shape to carry — Bun's bundler defaults differ from the
ecosystem's, and the failures they produce are time-dependent rather than
immediate.

Also records the half that did not ship: `minify` strips the same markers and
was deliberately left off under D7's deferral.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ichabodcole
ichabodcole merged commit 9c1294d into main Sep 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant