User ruling 2026-09-20: minimise the amount of new code a new UI framework
needs. Centralise logic in a redux-like store so a second or third front end
is a function from state to pixels plus a wire for events, not a second
implementation of how the application behaves.
Names the pieces that already exist as what they are -- Command is the
action, reduce_view and WidgetCache::apply are the reducer, ViewState plus
the caches are the store, Sessions::apply is dispatch -- and gives the
question to ask of any front-end code: would a third front end have to write
this again?
Three things make it concrete: the measure is lines of framework-specific
code and should trend to 'paint and wire'; a derivation done twice is the
defect even when both copies are correct today (improvise-edp is the worked
example, gotcha 44 is the same defect still open); and sharing a table beats
testing two copies of it, which is why the keymap is shared rather than
mirrored. Also says what legitimately stays local: painting, event wiring,
and a semantic-value-to-framework-type lookup like AxisColor -> axis_fg.
Sections 5-7 renumbered; the §6 testing references in repo-map.md and the
code-review skill updated with them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
A git worktree does not load the flake pin: its .envrc is unallowed, so a
process there falls through to whatever bd the profile carries. Today the
two agree, which is why nothing has broken; nothing enforces that they
keep agreeing.
The failure mode is silent — a mutating bd of the wrong version empties
the dependency graph in .beads/issues.jsonl and reports success. Section 0
of the session-start hook now reads the pinned version out of flake.nix
(never a second copy, which would rot when the pin moves), compares it to
`bd version`, and prints a banner and exits 2 on any disagreement. An
unreadable pin or an unreadable version fails the same way: an assertion
that cannot answer is not a pass.
It blocks nothing — reading with a skewed bd is safe — and it runs before
the CLAUDE_CODE_REMOTE gate so local worktrees, where the problem lives,
are covered. The cloud path re-checks after installing bd and still
completes its setup before the non-zero exit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
Review of #7: `TMP="$(mktemp -d)"` was unchecked. With TMP empty,
"$TMP/$DB_NAME" is not a relative path under a temp directory — it is
`/improvise`, an absolute path at the filesystem root, which the hook
then creates and clones into. The cleanup at the end degrades to
`rm -rf ""`, so the real temp directory, if any, leaks too.
The mktemp now sits in the condition that guards the whole block, so a
failure skips the sync with a warning instead of proceeding with a broken
path, and no temp directory is created at all when there is no origin to
sync from. Putting it in the condition also avoids reindenting the body,
which keeps this diff to the thing it fixes.
Verified by stubbing mktemp to fail: the warning is printed, the sync is
skipped, and nothing appears at /improvise. The normal path still syncs
169 issues with the repo clean.
This is the fourth finding in this family on this branch — unchecked
status on an operation whose failure produces a plausible-looking wrong
value rather than an error. The others were bd init leaving an empty
database behind, the unchecked mv that nested the new tracker inside the
old one, and jq's status being swallowed by a process substitution.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyHK7L8S1XDX3EihTnSKaX
Two findings from the review of #7.
The hook piped an unpinned installer from beads' `main` straight into a
shell. That runs code nobody has seen, from a URL whose contents can
change under us, and leaves nothing to inspect when something goes wrong.
It now downloads first and runs the file, so /tmp/bd-install.sh is
readable afterwards, and pins the revision: the peeled commit of tag
v1.3.0, which is the version this work was done against. The comment
carries the ls-remote incantation for bumping it. Byte-identical to
`main` today, so the pin costs nothing now and is the whole point later.
The seeding script read its input with `mapfile < <(jq …)`, and a process
substitution discards the exit status of what ran inside it. A malformed
or unreadable flake.lock therefore produced an empty list, which the next
branch reported as "no github-sourced inputs; nothing to seed" and exited
0 — the script would claim success having seeded nothing. jq's output is
now captured with the status checked, and emptiness is a separate case
from failure.
Verified by exercising the paths rather than reading them:
malformed flake.lock -> error names the file, rc=1
valid lock, no github inputs -> "nothing to seed", rc=0
missing flake.lock -> error, rc=1
installer download -> /tmp/bd-install.sh matches f45b249 byte for byte
shellcheck -S style clean on both scripts; the real seeding run still
completes with flake.lock unmodified.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyHK7L8S1XDX3EihTnSKaX
GitHub's docs are explicit: "if you want to ensure that Copilot code
review will read and use a skill, use a review-focused skill directory
name such as code-review". They also list .claude/skills alongside
.github/skills and .agents/skills as recognised locations, so the skill
was already in a place Copilot reads -- just not under a name that
reliably signals what it is for.
Renaming it means it owns the repo's review entry point, so it should
cover the whole job rather than only conformance. It now opens with two
passes: correctness first, then whether the change fits the design. That
also removes a self-reference, since the old text told the reader to run
the generic /code-review, which is now this.
The description is rewritten to lead with "review a pull request or
diff", which is what a reviewer's relevance matching has to match on.
Note for humans: `/code-review` in a Claude Code session on this repo
will now resolve to this skill rather than the built-in one. The CI
workflow is unaffected -- it invokes the plugin explicitly as
`/code-review:code-review`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyHK7L8S1XDX3EihTnSKaX
Review of #7 found the swap could corrupt or lose the database: the move
of the live database out of the way was unchecked, so if it failed the
next move put the new database *inside* the old directory and still
reported success; the restore was unchecked too, and the `rm -rf "$TMP"`
that followed could then delete the only remaining copy.
All three are real. The root cause was doing the critical swap across
filesystems, where a move is a copy that can half-finish.
install_tracker() removes the class instead of patching the cases. The
staged copy is brought alongside the live one first, so every step after
that is a same-filesystem rename: it either happens or it does not. The
previous database is removed only once the new one is in place, and every
move is checked, with a loud message if a restore fails and leaves it
parked. The caller collapses to one condition, which also removed a
duplicated pair of warning branches.
Proved rather than asserted, since this block has now been wrong twice.
`mv` was stubbed to fail at each point in turn:
- old-move fails -> rc=1, live database intact, no nesting, no leftovers
- final rename fails -> rc=1, live database restored, no leftovers
- missing source -> rc=1, live database intact
A first attempt at the third case used chmod, which proves nothing as
root; the stub is what actually exercises the path.
The transient .incoming/.previous directories are gitignored so a crash
mid-swap cannot leave untracked noise.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyHK7L8S1XDX3EihTnSKaX
shellcheck flags the single quotes around the PATH export, but they are
the point: $PATH and $HOME must reach CLAUDE_ENV_FILE literally so they
expand when the session sources it rather than at hook time. Marked with
a disable directive and a comment so the next lint run stays clean and
the intent is not mistaken for a bug.
Both scripts now pass `shellcheck -S style`, run out of the flake's own
pinned nixpkgs via the seeding script this branch adds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyHK7L8S1XDX3EihTnSKaX
Nix's `github:` fetcher wants a tarball from
github.com/<owner>/<repo>/archive/<rev>.tar.gz, and that endpoint is not
served in a cloud session. Three things that look like fixes are not:
- Raising the environment's network access to Full, or allowlisting
codeload.github.com, changes nothing: GitHub traffic bypasses the
domain allowlist entirely and goes through a proxy that scopes API
and release-asset requests to repositories attached to the session.
An input from any other owner gets a 403.
- Attaching the dependency repos is refused -- cross-tier adds are not
supported once the session has sources from another owner.
- Nix has no setting that makes `github:` inputs use the git transport.
The lock's own integrity machinery is what works. flake.lock records a
narHash per input, and Nix looks a source up in the store and its
substituters before fetching anything. The git and tarball fetchers
produce byte-identical trees for a revision, so prefetching the locked
rev over git+https lands the input at exactly the store path the lock
names:
lock narHash: sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=
git prefetch: sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=
scripts/nix-seed-flake-inputs.sh does that for every locked input. It
reads flake.lock and never writes it. Nix still verifies each tree
against the recorded hash, so this satisfies the lock rather than working
around it -- a mismatch fails loudly. Rewriting the lock's nodes to `git`
type and carrying the old narHash across would have been the opposite:
that hash is the claim the lock exists to make.
Verified from a fresh clone with a cleared git cache: 15/15 inputs
seeded, then `nix develop` gives rustc 1.98.1, cargo and crate2nix, with
flake.nix and flake.lock unmodified.
Not wired into the session-start hook: it costs minutes and several GB
once the devShell builds, and most work only needs the preinstalled cargo
toolchain.
Corrects three places that claimed nix could not work here --
context/cloud-agents.md, the design-review skill's kb.pl check, and the
hook's environment notes -- all written before the git transport was
tested. The doc also records the two alternatives, a binary-cache
substituter and a self-hosted environment.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyHK7L8S1XDX3EihTnSKaX
This repository states what "correct shape" means in more detail than most:
design-principles.md has the architectural intent, repo-map.md carries 31
numbered gotchas, and prolog-sessions/kb.pl holds 71 invariant/doctrine/
decision facts that CI checks as code. A reviewer who has not read all of
that reviews generically, and the generic findings are the shallow ones.
The skill is an index, not a copy. It points at those four sources and
says which parts matter for which kind of diff, because restating the
rules here would let the copy drift from the files that are maintained.
What it adds on top of the pointers is the shape of the review: command
purity, effect classification (kind() has no default, but "present" is not
"correct" -- doctrine(derive_then_verify) records three effects
mis-classified), exhaustive matches over AppMode/Axis/CategoryKind, the
two_questions_one_enum rule for a variant that shares one property but not
another, crate layering straight from forbidden/2, and the invariants no
type carries -- CellKey constructors, IndexMap order, render-only
rounding, stale layout.
The testing section is the one most worth having written down: a bug fix
needs a test that failed before it, and the three doctrines that say when
a green suite is not evidence -- check_the_checker,
populate_before_roundtrip, binding_dump_diff.
Deliberately complements /code-review rather than replacing it. That one
hunts correctness bugs; this one asks whether a change fits the design.
Every kb.pl and gotcha reference was verified to resolve. Merged
origin/main first so the references to gotcha 31 and the removed
Effect::changes_mode() are accurate on this branch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyHK7L8S1XDX3EihTnSKaX
Three findings from the review, plus a bug the first one uncovered.
Sync on every session, not only when the database is missing. Cloud
containers reuse a cached workspace, so "already present" does not mean
"still current" — the two runs in testing came back 169 and then 168
issues, which is precisely the drift that makes a skipped sync serve
stale data. Installing bd and warming the build still check first, being
genuinely one-time.
Making the sync unconditional exposed a worse failure than the one it
fixed. `bd init` exits non-zero when the remote is unreachable but still
leaves a .beads/embeddeddolt behind — an empty database. The old check
tested only for that directory, so a failed sync swapped an empty
tracker over a good one and logged success. Verified against a bogus
remote: the good tracker was destroyed. The sync now checks bd's exit
status and confirms the result actually carries issues before installing
it, and the swap parks the old database in the temp directory until the
move succeeds, so a failure keeps the previous tracker rather than
leaving none. Re-verified: the 168-issue tracker survives a failed sync
and the warning says so.
An empty remote tracker is indistinguishable from a failed clone here.
Keeping what we already have is the safe way to be wrong.
The bd installer pipeline redirected only `bash`, so a curl failure (DNS,
403, TLS) left /tmp/bd-install.log empty — exactly when it is read. The
whole pipeline is redirected now.
Also ignore *.gate.lock*: bd drops .beads.gate.lock beside .beads/ on
every run, which otherwise shows up as untracked noise in every session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyHK7L8S1XDX3EihTnSKaX
A fresh Claude Code on the web container starts in a state where three
things are slow or wrong, all of which cost real work this week.
`bd` is not installed, so the existing `bd prime` SessionStart hook and
every bd command in AGENTS.md silently do nothing.
`.beads/issues.jsonl` is a stale export, and an agent with no database
reads it as if it were the tracker. It showed 121 issues against the
remote's 168, with all four P1 bugs OPEN though every one was fixed and
closed. Two beads were implemented from scratch on the strength of it;
both were already closed on the remote and already implemented on a
branch that had not yet merged.
The Rust workspace is uncompiled, so the first `cargo test` pays a cold
build before reporting anything.
The hook installs bd, syncs the Dolt database from the git remote, warms
the build, and prints the handful of environment facts that are not
discoverable from the repo.
Syncing needs care, because `bd init` in this repo rewrites AGENTS.md,
CLAUDE.md and .claude/settings.json, installs Cursor/Codex files, and
commits the lot. `--skip-agents --skip-hooks` stops all of that but it
still commits a .gitignore change, so the hook runs it in a throwaway
directory and copies only .beads/embeddeddolt back. Nothing it does
touches a tracked file, stages anything, or creates a commit.
Also adds a permissions allowlist for the read-mostly cargo, bd and git
commands an agent runs constantly, and context/cloud-agents.md for the
detail behind the hook — including the bd sharp edges worth knowing:
`bd dolt push <remote>` ignores its argument and pushes to origin, and
`bd dolt pull` into a divergent database refuses to merge.
The hook exits immediately unless CLAUDE_CODE_REMOTE=true, so local
checkouts are unaffected. It is idempotent: a second run is a no-op
except for the build warm, and re-running costs 0s.
Validated by running it against a clean clone: tracker syncs to 169
issues, .beads/config.yaml is left alone, no commit is created, and
`git status` shows nothing beyond the hook's own files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyHK7L8S1XDX3EihTnSKaX