CLAUDE.md · git:20260917.fdcbb8c · 2026-09-17 · sha256 ed2955d77ad964da
CLAUDE.md git:20260917.fdcbb8cA
Immutable. This exact content is served forever at /api/v1/blob/ed2955d77ad964da.
# Grove — Agent Guide
Grove lets you tend multiple git worktrees like branches in a forest: spin up, switch between, and tear down isolated coding sessions without losing your place. See `README.md` for the product brief.
## Handling a handed-off issue (the autonomous workspace loop)
When you own a tracker issue end-to-end, drive it to a merged PR on this loop. Bias to action — never over-plan, over-spec, ask trivial questions, or invent fallbacks nobody requested.
1. **Read the issue, map the components, implement.** Read the nearest owning `CLAUDE.md`(s) on the path you'll touch *before* editing.
2. **The PR BODY must end with one `Closes #<n>` line per issue it resolves; the PR TITLE carries no issue number.** Gitea only auto-closes on the literal closing keyword (`close`/`closes`/`fixes`) followed by `#<n>` — a body that merely mentions the number, or a comment on the issue naming the PR, closes nothing, and that gap left 100+ issues open behind merged PRs. **The split is by artifact, and the honest reason is narrower than it looks:** the squash title inherits the PR title and mirrors to the public GitHub remote, where a bare `#<n>` resolves against a *different* tracker, so the title stays a clean gitmoji/conventional summary. The body is NOT Gitea-only — measured 2026-09-13, Gitea's squash commit carries the PR body verbatim, `Reviewed-on:` URL and issue-ops footer included, so it reaches the mirror too. That is fine (those host URLs are already on every existing commit and a `Closes` line adds nothing new), but do not justify this rule with "the body never leaves Gitea", because the next person will check.
3. **Check the open PR for conflicts with the remote target and resolve them** — rebase onto the freshly-fetched target, re-apply onto the *current* structure (don't clobber what landed meanwhile), re-run the gates, force-push.
4. **Reply on the issue naming the PR.** Summarize the design decision, the components touched, and the gate results. Say plainly anything that did not work, anything left out, and any acceptance criterion that turned out to be unsatisfiable — a ticket whose own premise was wrong is the most valuable thing you can leave behind, and the next session cannot recover it from the diff. Do not close the issue by hand — the keyword closes it at merge, which is the right moment — but after the merge, verify it actually closed rather than assuming the keyword did its job; an open ticket with a linked PR is honest, a closed one with unmerged work is not.
5. **After implementing, fold the durable insights into the nearest owning `CLAUDE.md`** — the invariant, the trade-off, the reference you'd otherwise re-derive; never copyable snippets or restatements of code.
**Parallelize** — fan a fleet of sub-agents out over disjoint work per the parallel-agent pattern below.
## Core operating rules
- Separate facts from assumptions and keep updating both; prefer direct evidence (run the code, read the real output) over inference from a description. Ask the user about ambiguous assumptions instead of guessing silently.
- Use absolute `YYYY-MM-DD` dates in findings you report to a human, never relative ones ("last week").
- Do not push to a remote unless explicitly asked.
- **Plans live in the issue tracker; design knowledge lives in this memory tree.** Session-local working memory — scratch plans, specs, agent reports — is never committed. Don't dump design detail into issues, or plans into the tree. An unlabelled issue is invisible to the board, so label every issue you file.
- **Never leak host-private details into tracked files.** No absolute home paths, real private repo names, personal profile names, or host/infra specifics in committed docs or code — this repo mirrors to a public remote. Keep examples generic and fictional. Real host and profile configuration lives in your own config (`~/.config/grove/`, the gitignored `.grove/config.local.json`) and user-global `~/.claude/`, never in the committed tree.
## Project structure
| Path | Purpose | Guide |
|---|---|---|
| `src/grove/core/` | engine: lifecycle, manager, config cascade, store, status, registry, activity, CLI | [core/CLAUDE.md](src/grove/core/CLAUDE.md) |
| `src/grove/core/contracts/` | wire-level Pydantic shapes that cross clients | [contracts/CLAUDE.md](src/grove/core/contracts/CLAUDE.md) |
| `src/grove/core/agents/` | tool-agnostic agent introspection (adapters) | [agents/CLAUDE.md](src/grove/core/agents/CLAUDE.md) |
| `src/grove/core/tickets/` | branch-aware ticket providers (Gitea/GitHub/Linear) | [tickets/CLAUDE.md](src/grove/core/tickets/CLAUDE.md) |
| `src/grove/core/issueops/` | issue-comment events → workspace actions (command grammar + routing) | [issueops/CLAUDE.md](src/grove/core/issueops/CLAUDE.md) |
| `src/grove/core/telemetry/` | OpenTelemetry gateway: one vocabulary, three tiers, OTLP ingest | [telemetry/CLAUDE.md](src/grove/core/telemetry/CLAUDE.md) |
| `src/grove/core/notifications/` | push notifications on workspace edges (broker + channels) | [notifications/CLAUDE.md](src/grove/core/notifications/CLAUDE.md) |
| `src/grove/core/usage/` | historical audit: tokens, time, cost, quota (derived SQLite cache) | [usage/CLAUDE.md](src/grove/core/usage/CLAUDE.md) |
| `src/grove/daemon/` | loopback FastAPI daemon (multi-repo, SSE) | [daemon/CLAUDE.md](src/grove/daemon/CLAUDE.md) |
| `src/grove/client/` | transport-agnostic attach (local PTY / SSH) | [client/CLAUDE.md](src/grove/client/CLAUDE.md) |
| `src/grove/mcp/` | MCP server (stdio tools over the client SDK) | [mcp/CLAUDE.md](src/grove/mcp/CLAUDE.md) |
| `src/grove/tui/` | Textual terminal UI | [tui/CLAUDE.md](src/grove/tui/CLAUDE.md) + the **TUI** visual contract [docs/design-system.md](docs/design-system.md) |
| `webapp/` | assistant-ui-native web dashboard (fleet, workspace transcript + work panel, usage, host-wide session catalog); owns no bespoke components | [webapp/CLAUDE.md](webapp/CLAUDE.md) + the **web** visual contract [webapp/design-system.md](webapp/design-system.md) |
| `docs/` | published mkdocs site | [docs/CLAUDE.md](docs/CLAUDE.md) |
| `packaging/` | systemd-user service units + clean-install smoke | [packaging/CLAUDE.md](packaging/CLAUDE.md) |
| `tests/` | pytest suite + CI/lint gotchas | [tests/CLAUDE.md](tests/CLAUDE.md) |
## The distributed CLAUDE.md tree
**Read the deepest file that applies before editing there, and write each lesson in the file that owns it** — promote one up a level only when it becomes genuinely cross-cutting. This root carries only cross-cutting principles, structure, and process lessons; it never restates a nested file's content. Every nested file opens with a `> ↑ parent · root` backlink and every parent lists its children — maintain both links whenever you add, move, or rename a file.
**The two `design-system.md` files are nodes in this tree, not documentation beside it.** Each is the *prescriptive* half of the surface guide above it — the guide says how the code is built, the design system says what it may look like — so a visual decision is folded there and nowhere else, and each is reachable by backlink from its owner. **They are PRISTINE by policy: reconcile an existing rule before adding one, and a decision that does not generalize past its one call site belongs in a code comment instead.** The two are never merged: a terminal and a browser share vocabulary (status, agent state, runtime) but not a single token, and the shared vocabulary lives in `core/contracts/` where a drift test can hold it.
```
/CLAUDE.md (this file — principles · structure · process lessons)
├─ src/grove/core/CLAUDE.md
│ ├─ src/grove/core/contracts/CLAUDE.md
│ ├─ src/grove/core/agents/CLAUDE.md
│ ├─ src/grove/core/tickets/CLAUDE.md
│ ├─ src/grove/core/issueops/CLAUDE.md
│ ├─ src/grove/core/telemetry/CLAUDE.md
│ ├─ src/grove/core/notifications/CLAUDE.md
│ └─ src/grove/core/usage/CLAUDE.md
│ └─ src/grove/core/usage/quota/CLAUDE.md
├─ src/grove/daemon/CLAUDE.md
├─ src/grove/client/CLAUDE.md
├─ src/grove/mcp/CLAUDE.md
├─ src/grove/tui/CLAUDE.md (+ docs/design-system.md = the TUI's visual contract)
├─ webapp/CLAUDE.md (+ webapp/design-system.md = the web visual contract)
├─ docs/CLAUDE.md
├─ packaging/CLAUDE.md
└─ tests/CLAUDE.md
```
## Engineering principles
- **Modules align with concerns, not technical layers.** Each module answers one question, nameable in a sentence; if you can't name it, split it or fold it into its real owner. Split by concern (`contracts/` for wire shapes, `git.py` / `tmux.py` for side effects, `manager.py` for orchestration) — never generic `helpers/` / `utils/` / `models/` / `services/` buckets. When one file accumulates two answers to two questions, split it.
- **Public surface is small and explicit.** A package's `__init__.py` re-export list is its contract; every module under it is internal, and a leading underscore says so. The smaller the public surface, the cheaper internal refactors become.
- **Dependencies flow inward.** Orchestration imports utilities; the reverse is a smell, and most circular-import pain traces back to it.
- **Boring code beats clever code.** Reuse the established pattern; if you must deviate, name the reason inline.
- **Build only what the current requirement demands (YAGNI, KISS, DRY).** Three similar lines is fine; a new helper, class, or subpackage costs review surface for years. Bias toward less code, and search for an existing library or an existing utility before writing anything custom. A rule reused by more than one caller lives in exactly one place.
- **Fix at the smallest generic seam; never overfit, never hard-code policy.** Before adding a typed field, a new parameter threaded through layers, or a special case, check whether an existing mechanism already covers it — the config cascade, an `env`-style map, an existing scan/union, a field already on the model. **A fix that balloons across many files or duplicates an existing capability is the signal to stop and find the seam.** A specific variable name, profile, path, or provider quirk belongs in config, never in a default constant or a branch — code supplies the *mechanism* that acts on whatever config names.
- **Mechanism, not policy — configuration cascades at the consumption surface.** Any value a developer might reasonably change is reachable from outside the code; defaults are sensible but held lightly. Config layers by specificity: built-in → project → team → machine → user → invocation. This is only sustainable because the code underneath stays strict on DRY and KISS — one boring implementation, the override cascade resolving on top.
- **Strong types where they catch bugs.** Narrow literal types for string sets that drive branching, structured types for conditional payloads, explicit return types everywhere. Escape-hatch types only for genuinely heterogeneous external data, narrowed at the boundary. Introduce a protocol only when more than one real implementation exists.
- **Pydantic at public-contract boundaries; plain `@dataclass(slots=True)` for in-process state.** The test: would a non-Python client ever construct or receive this? Yes → Pydantic; no → dataclass. Wire shapes live in [`core/contracts/`](src/grove/core/contracts/CLAUDE.md).
- **Class-encapsulated atomic state.** State and the methods over it live together as one class (instance / `@classmethod` / `@staticmethod`, inheritance + DI). A free helper in module scope is usually a missing class.
- **Side effects at the edges, pure logic in the middle.** I/O, network, and time-of-day belong at the boundary (handlers, fetchers, drivers); the decision logic between them stays testable without them. Best-effort side effects isolate their failures: bounded timeout, structured log per outcome, never re-raise into the caller's retry path.
- **Tests pin contracts, not implementation.** When a test patches a private symbol, that path becomes an implicit contract — moving it silently no-ops the patch while the test still passes. Surface the seam publicly or update the test in the same commit. Tests prefer real code paths and stub only I/O boundaries; cover full orchestration loops with in-memory fakes.
- **Comments and docstrings explain WHY, not WHAT.** Names and types document the what; prose carries the constraint, the trade-off, the surprising invariant. Write the docstring's first line for the engineer deciding whether to call this.
- **Code validates itself at the point of definition** — schema validators, strict configs that forbid unknown fields.
- **Keep published contracts stable:** interface names, method signatures, and field names don't move under consumers without coordination.
- Gitmoji plus Conventional Commits (e.g. `✨ feat(scope): ...`).
### Concurrency
Grove is one event loop (daemon), one UI thread (TUI), and a lot of slow blocking I/O — git, tmux, `docker exec`, `devcontainer up`. These rules are ordered: satisfy the earlier ones first.
- **Nothing blocking runs on the loop or the UI thread.** A `devcontainer up` is minutes, and on the loop those minutes freeze every SSE stream, every other repo's dashboard and the activity poll; on Textual's single thread they freeze every timer, keypress and repaint. Off-thread is `asyncio.to_thread` — hand-rolling `get_running_loop()` + `run_in_executor(None, …)` is the same call with more lines and no contextvars.
- **Work whose duration is unbounded gets its own bounded pool.** The default executor is the render path; sharing it with lifecycle verbs trades a loop stall for pool starvation, which looks identical to the user. Bound the pool so a fleet can never become N simultaneous image builds, name its threads, and never let shutdown wait on side-effecting work already in flight.
- **Introducing concurrency deletes an invariant. Restore it explicitly.** A single-threaded loop is an implicit mutex over everything it runs; the moment work moves to a pool that mutex is gone and nobody notices, because the code that relied on it never mentioned it. Re-establish exclusion at the **narrowest key that preserves the point of the change** — per workspace, not global, or the pool bought nothing. Atomic *storage* is not the same guarantee: `JsonWorkspaceStore` locks its read-modify-write, but a verb is `read → mutate git/tmux/container → save`, and the exposure is that middle span.
- **Results cross a thread boundary as messages, never as direct writes**, and a verb dispatched per keypress needs de-duplication or one impatient double-press is two kills.
- **Prefer not having a loop at all.** In order: (1) subscribe to an edge that already exists; (2) if you must poll, gate it on a live consumer — publishing into an empty room is pure waste; (3) suspend the tick when its surface isn't visible, but **measure which branch actually leaks before gating**, and never gate on window focus (an unfocused Grove is a legitimate live surface); (4) only then, a timer, with its body gated so an idle fleet costs nothing.
### The provider boundary (LLMs and agents)
- **Patch the provider boundary, never model behavior.** Treat LLMs and agents as non-deterministic black-box APIs; avoid anthropomorphic language. Write code only for provider and model *parameter and protocol* differences — how an invocation is launched, how tool calls are passed, how responses (and their varied content-block types) are received, normalized and presented. Never add code to correct, second-guess, or work around what a model *does*: an adapter normalizes *shape*, not *semantics*. The concrete adapter layer is [`core/agents/`](src/grove/core/agents/CLAUDE.md).
## Running, testing, linting
- Install (editable, all surfaces): `uv tool install --reinstall --force --editable '.[all]'` from the repo root, then `systemctl --user restart grove-daemon`. `[all]` = daemon+client+mcp; `'.[daemon]'` is the lean daemon-only variant that intentionally omits the MCP SDK and leaves `grove-mcp` non-functional (`ModuleNotFoundError: No module named 'mcp'`).
- Tests: `uv run pytest`. Lint: `make lint` is the full gate — `ruff check`, `ruff format --check`, `mypy src/grove`, `lint-imports` in sequence. Always run the full target before pushing, never just `ruff check`.
- CI is Linux-only; cross-platform defenses are unverified by CI. See [tests/CLAUDE.md](tests/CLAUDE.md) for the Windows/macOS gotchas to reason about by hand.
## Cross-cutting process lessons (no single component owner)
> Workflow and tooling lessons with no component home. Component-specific learnings live in the nested files mapped above — don't re-log them here.
- **A checkout means an editable install, and an update refreshes three independent long-lived surfaces.** `~/.local/bin/grove` resolves to whatever was last `uv tool install`ed, and a vanilla install pulls the published wheel, not your checkout — symptom: a new endpoint or method is green in `pytest` but the running daemon serves 404 / `No such command`. Refresh the CLI/TUI (relaunch), the daemon (restart) and the webapp (rebuild `.next` + restart) separately. The opt-in `grove-mcp` server needs no refresh: its client respawns it per connection, so an editable pull is live on the next launch; only a changed `mcp` extra needs a reinstall. See the `reinstalling-grove` skill.
- **A SERVICE'S `WorkingDirectory` CAN NAME A WORKTREE, AND THEN NOTHING YOU DO IN THE MAIN CHECKOUT DEPLOYS.** `grove-webapp.service` was pinned to `…/.claude/worktrees/native-stream-recovery/webapp`, whose `.next` predated a merged fix by two hours. Every rebuild, reinstall and restart kept serving that tree, so a landed feature read as never-landed — measured against the live app: 15 standalone file-edit cards and duplicated labels that the merged source cannot emit, while the main checkout's own built chunks contained the fix. **The rule above says which three surfaces to refresh; it does not say that one of them can be pointed somewhere else entirely, and a restart then looks exactly like a deploy.** Before concluding a shipped change did not work, check `systemctl --user show <unit> -p WorkingDirectory` against the checkout you built in — one command, ahead of any code reading. The general shape: *a stale surface is a deployment question before it is a code question*, and the tell is source and screen disagreeing while the gates are green.
- **Never run a Grove entrypoint as root — it poisons the editable install, and uv's error never says so.** The `uv tool` venv and (editable install ⇒) the checkout's `src/grove/**/__pycache__` are user-owned; one root-run entrypoint — classically a root shell where `claude` spawns `grove-mcp` from `.mcp.json` — makes CPython write uid-0 bytecode into both. `uv tool install --reinstall` must empty `site-packages`, cannot unlink a root-owned `__pycache__`, and dies with a bare `Permission denied` naming whatever dependency it hit first; nothing points at root. **Diagnose by ownership, not by the message:** `find "$(uv tool dir)/grove" ! -user "$(id -un)"` — and the *set* of root-owned `.pyc` names the guilty entrypoint (only `grove-mcp` pulls in `mcp/**`). **Recover without sudo** by renaming the venv aside (`mv "$(uv tool dir)/grove" …` — rename needs write on the *parent*, not on the root-owned contents) and reinstalling; the stale tree still needs one `sudo rm -rf`. `reinstall.sh` preflights this and prints the `chown` remedy.
- **A Grove workspace is a git worktree, so it sees only committed files.** Uncommitted working-tree edits never propagate into a worktree — commit shared config (e.g. `.mcp.json`, whose tokens are env refs, not literals) for worktrees to inherit it. Config layers: user `~/.config/grove/config.json`, committed project `.grove/config.json`, gitignored `.grove/config.local.json`.
- **The tracked `.mcp.json` is PUBLISHED, so it may only name servers a stranger could reach.** It is committed for the reason above, which makes it the one config file where "worktrees need this" and "this mirrors to a public remote" pull in opposite directions — and the env-var *reference* being safe says nothing about the URL beside it or the variable's own name. A server on a private host, or a token variable named after a person or an internal deployment, belongs at **user scope** (`claude mcp add --scope user`), which every project and every worktree already inherits without git carrying it. The tell that this drifted is a hostname in `.mcp.json` that only resolves on one LAN.
- **Judging whether a hostname is real needs evidence other than DNS.** A LAN with a wildcard record answers for every name under the domain, so `getent`/`dig` returns an address for an invented host exactly as it does for a live one — a "does it resolve" probe reads as confirmation while proving nothing. Confirm from something that names the host on purpose: a git remote URL, a live MCP server's own tool names, a service config. Then genericize with the repo's established fictional org rather than inventing a new placeholder vocabulary per file.
- **Parallel-agent build pattern: build the shared foundation solo and verify it, then fan out one agent per *disjoint* directory.** Agents only consume the foundation, never edit it → zero conflicts. This scales to a whole epic when each story owns its own new module and is restricted to small, delimited insertions in the shared files it must report exactly; merge the engine/integration stories last. **When a redesign instead forces many agents into *shared* atoms, freeze the public contract first** — props, testids, first-child structure — so each agent restyles internals freely and the integrations compose. The contract, not the directory, is the conflict boundary.
- **A delegated agent optimizes exactly what you made MEASURABLE, so a format rule plus an impression of size produces format compliance and nothing else.** Four agents told "strict bullets, at most one lead paragraph per section" and "roughly half the length" delivered the paragraph rule perfectly and cut prose by 9%: the bullets were the same sentences with dashes in front. Re-nudging did not help, because the second instruction was no more measurable than the first. **Give a per-unit budget with the invariant beside it** — "this section is 496 words, bring it under 250, here is the fact list that must survive" — and verify with a diff of the tokens that had to survive rather than by reading. The integrator's own hand-pass then closed the gap in one turn, which is the tell that the work was never the hard part.
- **Give each concurrent agent its own worktree AND a file-ownership list.** One checkout cannot hold six branches — a fleet told to `git checkout -b` in a shared tree produces interleaved commits on whichever branch happened to be current. Isolation alone is not enough: name the files each agent owns, forbid the rest, and require it to REPORT any unavoidable touch outside its list with exact lines. When a shared file does conflict, the resolution is usually the UNION of both intents, not either side whole — taking one side silently reverts the other's fix.
- **NEVER mutation-test in the shared checkout while a fleet is running — the mutation IS the tree every other agent reads.** Proving a guard bites by breaking the thing it guards is the right instinct and the only way to know a test is not vacuous; doing it in place is not. One `auth_dep` was deleted from `GET /workspaces` for ~30 seconds to confirm a route-census test failed. In that window a concurrent test-writing agent read `app.py`, correctly diagnosed an unauthenticated workspace listing, and filed it with a precise reproduction and a line number — a **false vulnerability report costing a full verification round trip**, and one that would have been extremely convincing in a handoff nobody re-checked. The window is short, which is exactly why it feels safe and why nothing in the process catches it. **Mutate in a throwaway `git worktree`, or serially with no fleet running.** The same isolation answers "is this failure mine or pre-existing": add a detached worktree at `origin/main`, `cp -al` the `node_modules` into it, and run the suite there — two identical failures on both sides settles it in one run without touching your branch.
- **A GUARD TESTED ONLY THROUGH A PATH THAT FAILS ANYWAY IS VACUOUS, AND IT READS AS COVERAGE. Mutate every guard you add — in a throwaway worktree — and expect roughly half of them to survive the first attempt.** Four did in one session, each for a different reason, and none was visible by reading the test. A route's opt-in gate was exercised with an agent kind that has no such capability at all, so an earlier `return None` answered first and deleting the gate changed nothing. A card's `supported` check was fed a refusal carrying no values, so the *next* guard caught it too. A "last frame wins" fold was asserted on a field the fixture did not vary. A list-shaped count was only ever given a list. **The common shape: the test reached the right outcome through the wrong branch** — so write the fixture that can ONLY fail if this guard is absent (a refusal that carries values, a second frame that changes the field, a parametrized both-answers case), and confirm by deleting the guard. Budget this as part of writing the test, not as a later audit; the worktree costs one command (`git worktree add`, `\cp -f` the files, mutate, run with `PYTHONPATH` against the main venv) and never touches the shared tree — see the mutation-isolation rule above for why that matters with a fleet running.
- **A BUG REPORT NAMES THE CAUSE ITS AUTHOR ALREADY KNEW ABOUT — check the proposed fix is absent before building it.** Issue #789 asked for a cwd-ambiguity refusal in `grove phase`, with a precise reproduction and a real corrupted ticket behind it. The refusal had shipped six weeks earlier, and the actual cause was the *sibling* ticket filed the same day: a leaked `XDG_STATE_HOME` pointed the CLI at a pytest fixture's workspace store, in which exactly ONE stranger matched the cwd — so inference was unambiguous and wrong, and no tie-break could ever have fired. **Two failures that share a symptom get reported as the one the reader has a name for**, and "ambiguity" is the name everyone has. The counterfactual is what settles it and it is cheap: replay the reported command against the *actual* state at the time (here, the leaked store was still on disk), and confirm the current code reproduces the reported outcome before writing a line. Both tickets were one incident; fixing the one the reporter thought was important would have left the real one live.
- **THE GUARD MOST LIKELY TO SURVIVE MUTATION IS THE ONE THAT REFUSES INSTEAD OF FALLING BACK**, because every other test still reaches a sensible answer through the fallback. Of eight guards mutated here, seven died immediately and the survivor was `raise` → *silently resolve from the cwd instead* — which is precisely the original bug, restored, with the whole suite green. A fallback path is invisible to any test that does not specifically construct the state where falling back is wrong, and that state is usually the incident itself. **Write the test from the incident's own inputs, not from the code's branches.**
- **A SUBAGENT INHERITS `GROVE_PHASE_FILE`, SO IT CAN PUBLISH A PHASE CLAIM IN YOUR NAME — onto the attached ticket.** Measured this session: a delegated agent wrote `{"phase":"scoping"}` over the orchestrator's file mid-run, and a later one wrote `"done"` while three surfaces were still unverified — a false completion claim visible to anyone reading the issue, from a process that had no idea the workspace existed. The variable is per-workspace-and-slot by design and the fleet is invisible to it. **Re-assert your phase after a fan-out returns, and never read the file back as your own state** (it may be a stranger's last write). Telling agents not to report is not the fix — they cannot tell that the file they were handed is someone else's.
- **A REBUILD UNDER A LIVE `next start` SERVES A MANIFEST NAMING CHUNKS THAT NO LONGER EXIST, and a CSS change then reads as a specificity problem.** Measured this session: after `npm run build` while the server ran, the page requested three stylesheets, one of which was absent from disk entirely, while the chunk actually holding the new rules was never linked. The new CSS was present in `.next`, correct, and unreachable — so the browser reported the OLD computed values and the obvious readings ("my selector lost", "the class beats me", "stale browser cache") are all wrong and all cost a round trip. **Two cheap discriminators, in this order:** diff the `<link>` hrefs against `ls .next/static/chunks/*.css` — a requested file that does not exist settles it instantly — and grep the built CSS for your rule, remembering the minifier strips the quotes from `[data-slot="x"]`, so grep the unquoted form or you will "confirm" it is missing. **Restart the server after every rebuild; never rebuild under one you are measuring.** webapp/CLAUDE.md states the failure mode; what is new here is that it presents as a *cascade* bug rather than a deployment one.
- **A green source census plus a green typecheck is not a rendered control: ask what the vendored component does with what you passed it.** A create dialog composed its "Agent default" row by hand and passed only the catalog to the selector's `models` prop — every assertion passed, and the trigger rendered the vendor's `Select model` placeholder, i.e. a control claiming nothing was chosen while its request carried the resolved default. The census could not see it because the composition is genuinely correct; what was wrong lived inside a file the gate forbids editing and the tests never render. **A `renderToStaticMarkup` of the VENDORED element alone, with no app around it, is the cheap discriminator** — it needs no dialog, no query client and no browser, and it is how this was caught after the delegated agent's own gates went green.
- **A delegated agent can FINISH the work and stall on reporting — read its artifacts before you nudge it.** Nudging costs a full context replay and changes nothing; `git -C <worktree> log/status/diff` answers in one call. Under a saturated fleet a slow suite is the HOST, not a hang — and a run killed by memory pressure exits 144, which is not a test failure and must not be reported as one. Corollary for the integrator: **gate the branch yourself rather than waiting to be told it is green.**
- **The working tree can advance under you mid-session.** If the Edit "modified since read" guard fires, re-read the file and re-derive the edit against current content — never force it.
- **A green gate on every agent's territory says nothing about the seam between them — look at the assembled surface.** Four agents each shipped a passing `typecheck`/`lint`/`test` on disjoint directories, and the composed page still had one page positioning itself `absolute inset-0` against a `relative` ancestor that belonged to *another* agent's shell, painting the page title over the sidebar brand. No unit test can see that, because the bug is in a relationship neither agent owns. The same pass caught a requirement I had scoped too narrowly — one agent flattened the list I named and left the identical grouping on the surface I forgot to name. **Budget a pass through the real, built, deployed UI as its own step**, and treat "the a11y tree's bounding boxes disagree with the layout you intended" as the cheapest way to find it: `complementary [box=0,0,260,…]` next to content `[box=0,0,1600,…]` is the whole diagnosis.
- **`src/grove/skills/` has ONE copy, and publishing it is still a second action.** The marketplace repo vendors nothing: its `grove` entry is a `git-subdir` source pointing at this repo's `src/grove`, so editing a skill here IS editing the published skill. What the marketplace still owns is the **version**, and an install pins a commit — so a skill improved here reaches nobody until that entry's `version` is bumped, silently and indefinitely. **The failure mode survived the fix that removed the duplication**, which is why it is worth restating rather than deleting: the old rule ("edit both copies, `diff` them") is now wrong in its remedy and would send you hunting for a file that does not exist. Check the `source` block before believing either version of this rule — that is the fact both readings hang on. A skill's frontmatter `name` must equal its `skills/<name>/` directory name.
- **A skill or guide that documents a tool surface goes stale the moment the surface grows, and nothing fails when it does.** No test, gate or type checker reads prose, so the only detector is a periodic audit against the real definitions — the Typer commands and MCP registration tuples themselves, never a README. **Never write a count you would have to maintain**; point at the enumeration that is the census instead.
- **A DERIVED store's disposability is a licence that expires the moment one column cannot be recomputed — and the tell is a field whose source is DELETED by an ordinary verb.** `usage.sqlite3` earns its schema-bump-drops-the-file and retention-prune properties by being wholly re-derivable from transcripts; a workspace title is re-derivable only while the workspace exists, and `kill` removing that record is the NORMAL end of a task, so a column there would have discarded exactly the fact nothing could rebuild (measured: 2 of 10 stored ids already named workspaces that were gone). The durable half goes in its own file and the cache ATTACHes it — **one-directional, so "deleting the cache is always safe" stays true.** `paths.quota_state_path` and `session_turns_path` are the same call already made twice; before adding a column to a derived store, ask what deletes its source.
- **Attaching a database puts its tables in the HOST's namespace.** SQLite resolves an unqualified name across main and every attached schema, so a second `meta` in the attached file SHADOWED the cache's own on a fresh cache — which read version 1, concluded a mismatch, and dropped and rebuilt itself on every startup. Name a shared file's tables as if the host's were their siblings.
- **`ATTACH` on a missing path silently CREATES an empty database**, so a lazily-connecting store must materialize its schema before anything attaches it, or the join fails at query time where nothing points at the cause (24 tests, `no such table`).
- **A SQLite PRIMARY KEY treats NULL as DISTINCT, so one nullable key column defeats a whole dedupe — and the common case is the one that has no value.** Deduping a ~1 Hz capture on its content (never on a timestamp, which appends forever) is right, and `note` being NULL for a bare `grove phase implementing` made **500 identical ticks write 500 rows**, ~86,400/day/workspace. Every key column needs `NOT NULL DEFAULT ''` with the translation at the boundary. Its twin: **a whole-second timestamp is not a unique key for a human action** — `PRIMARY KEY (id, recorded_at)` discarded a legitimate rename, because create-then-rename is routinely sub-second, and the read then needs a `rowid` tiebreak or a tie returns insertion order, i.e. oldest-first against a newest-first contract.
- **A route inherits its neighbour's gate, and that is wrong the moment its subject OUTLIVES the record.** `GET /workspaces/{id}/history` copied `/todo`'s `_manager_for` and therefore 404'd for a killed workspace — the only case a durable store exists to answer. Its six tests all passed, because every one read a LIVE workspace; the missing one was create → record → kill → read. **When adding a read whose data survives its subject, write the test that deletes the subject first.**
- **A frontend change is approved as a PICTURE, and the picture's home is the tracker issue, not the chat.** The `mocking-up-in-grove` skill is the loop: draw under the workspace's untracked `.grove/attachments/`, open it managed, read the browser preview, stop at approval, then upload the render as an issue asset and embed it in the body before any code exists. Three things the first run paid for: the draw.io PNG export positions HTML labels unreliably, so mockup text is plain `text;` cells with `whiteSpace=wrap`; a ` ` in a `.drawio` value is an undefined XML entity and the managed read refuses the whole file; and `grove_stop_diagram` needs the revision the *browser* last acknowledged, which moves after the editor loads, so read once more before stopping.