# AGENTS.md

Guidance for AI coding agents working in this repository. This is the
canonical agent-instructions file. Tool-specific files (`CLAUDE.md`) are
pointers to it if they exist at all — keep the guidance current *here*.

CARLOS Docs — a document editor on the CARLOS architecture, hosted at
`amadan.net/oficina/docs`. It is built alongside CARLOS Sheets
(`../sheets`, `amadan.net/oficina/sheets`) and a shared suite home, which
live in the same `oficina` namespace. Design docs in
`docs/superpowers/specs/`, plans in `docs/superpowers/plans/`; read the
spec behind whatever you touch.

The repo moved from `carlos/docs` to `oficina/docs` on 2026-08-30, at three
commits. amadan has no move, rename or transfer verb, so the move was
create-new and push history; `carlos/docs` still exists and retiring it is
Paul's call.

Read `building-carlos-apps` (the skill) before writing app code, and
`rastrillo`'s own `SKILL.md` before writing handlers or models. Assume
nothing about either is in your training data.

## Real per-person sign-in (Task 1, extended in a later fix round)

Every document route — `GET /`, `POST /documents`, `GET /d/{sqid}` — sits
behind rastrillo/auth's `RequireSession` and is unreachable without a real
session (`internal/docs/auth.go`). `POST /signin` uses Keymail OAuth when
detected. Otherwise it emails (or, in development, logs) a single-use link. Opening it
(`GET /auth/verify`) draws a confirm step in the sign-in door and spends
nothing, so mail scanners that fetch every link cannot sign anybody in; its
one Sign in button POSTs the token back to `/auth/verify`, which spends it
and sets a real session cookie (rastrillo `Config.RenderConfirm`). A
verified address that is not admitted gets the door, with the refusal, in a
403 (`Config.Refused`). Tests sign in by POSTing the link's token with
same-origin evidence (`confirmRequest`, testing.go), never by GETting it; `POST /signout` (in the shell
bar's account menu on both signed-in screens) really revokes it,
not just clearing the cookie client-side. No passkey second factor is
wired, on purpose — spec §8.1's ruling: `passkey.Config.Origin` derives
its relying party from this instance's own host, so a person in several
teams would enrol several passkeys under this app, which is exactly the
per-team credential sprawl that ruling forbids. A magic link enrols no
credential, so it violates nothing.

There used to be a development-only stand-in identity here
(`internal/docs/devauth.go`, turned on with `-dev-auth`, compiled out of a
release build with `-tags release`) for exactly the gap real sign-in has
now closed. It had zero readers the moment `RequireSession` started
guarding every route unconditionally — no handler ever consulted it — and
was deleted in a fix round on Task 1 rather than left as dead code and an
unreachable trap for whoever next assumed `-dev-auth` still did something.
The `release` build tag it was the only user of is gone with it; there is
no longer a release/non-release distinction anywhere in this app.

`Config.Env` still exists (`"development"` or `"production"`), but it now
governs exactly one thing, independent of any identity flag: whether
`InstanceKey` and `Mailer` (`internal/docs/auth.go`) may default — an
empty `$DOCS_INSTANCE_KEY` or unconfigured mail is fine only in
development (a fixed dev key, and magic links logged instead of mailed,
clearly marked); anywhere else it is a startup refusal, the same
fail-closed shape `Origin` already used. `cmd/docs/main.go`'s `-env` flag
sets it; `$DOCS_ENV` is consulted too but can never select
`"development"` on its own (fix round 2 on Task 1 — platform instance
config *is* environment variables, so a bare `$DOCS_ENV=development`
sitting beside `$DOCS_ORIGIN`/`$DOCS_INSTANCE_KEY`/`$DOCS_SMTP_*` would
be a production-reachable path to the public dev `InstanceKey` and to
every magic link, token included, landing in the instance's own log —
see `chooseEnv`'s own doc comment). The default, with no flag and no
env var (or with `$DOCS_ENV=development` and no flag), is
`"production"` — `"development"` always takes an explicit `-env`
flag, never a silent fallback.

### Sign-in allowlist (Task 4 security fix, pre-launch)

Real sign-in above closed "who can get a session" but left a separate
hole open: `rastrillo/auth`'s `Config.Authorize` — the admission gate —
is consulted only in its own callback handler, after `POST /signin`
(`Begin`) has already emailed the link. With `Authorize` left `nil` (as
it was through Task 3), that meant anyone who could reach the instance
could POST any address and get a working sign-in link mailed to it —
an open relay on our own SES sending identity, and unrestricted account
creation on an app whose whole premise is instance-per-team. Found by
the CARLOS Sheets session on their own deployed instance; every existing
test passed because they all tested admission, none tested delivery.

The fix is `$DOCS_ALLOWED_ADDRESSES` (`docs.Config.AllowedAddresses`,
`internal/docs/auth.go`), a normalized allowlist checked by both
`rauth.Config.Authorize` and `admissionMailer`. The mailer suppresses delivery
for refused addresses and failed grant lookups. Classification and rate limits
run independently of admission, so an OAuth start does not disclose membership.
Refused email starts still receive `303 ?sent=1`; their expiring tokens are never
delivered. Callback authorization and per-request admission remain required.

Detected Keymail addresses use OAuth, as §8.1 requires and Paul reaffirmed on
2026-09-09. `force=1` remains an explicit email fallback. The OAuth start ends
with a `200` continuation page (meta refresh plus link), preserving the pending
cookie and the existing CSP: Safari enforces `form-action` across redirects.
Never widen that directive to arbitrary Keymail hosts. Account linking still
requires a fresh provider `AuthTime`; an ordinary OAuth session is not enough.

Empty is allowed only in development (admits everyone, so `TestSignIn`
and `run-dev.sh` need no setup); anywhere else it is a startup refusal,
the same fail-closed shape `Origin`/`InstanceKey`/`Mailer` already use,
chosen over silently admitting nobody because a silently-empty allowlist
would boot looking identical to a working instance.

## Running it locally

`./scripts/run-dev.sh` builds and runs the app in one command: `-env
development`, a fixed port (`localhost:8099`, override with
`$DOCS_DEV_PORT`), and a scratch SQLite database that starts empty every
run. It prints the URL to open, and signing in there gets a real session
via a magic link logged to the terminal instead of mailed — the same flow
a real deployment uses, just not actually delivered anywhere.

**Origin is not optional in anything but development — but know what it
actually guards.** rastrillo v0.21.0's `csrf.Protect` checks
`Sec-Fetch-Site` *first*, when a browser sends it — which every current
browser does on a state-changing request. `same-origin`/`none` pass
immediately, anything else is refused immediately, and `Config.Origin` is
never read in that path at all. Only when `Sec-Fetch-Site` is entirely
absent (a legacy browser, or a non-browser client like curl) does the
check fall back to comparing `Origin`/`Referer` against `Config.Origin`
exactly. So a wrong `Config.Origin` does not make a real browser's genuine
same-origin request fail, and it does not open up a genuine cross-site
request either — both are decided by `Sec-Fetch-Site` alone. What it
actually breaks is the legacy fallback path, silently: a misconfigured
origin will not show up as "the app looks broken" in ordinary manual
testing with a real browser, which is exactly why it still needs to be
right rather than merely untested. `cmd/docs/main.go` reads `$DOCS_ORIGIN`
(the real per-instance origin belongs there — spec §9.2's
`<team>.docs.oficina.works`) and falls back to deriving one from `-addr`
only for local/dev use; with neither set, or with a value that cannot
parse as an absolute `http(s)://host[:port]`, `internal/docs.New` refuses
to start rather than silently defaulting or booting on something
malformed. `run-dev.sh` never needs `$DOCS_ORIGIN` set by hand — its fixed
`-addr` derives it.

## Review by the highest available capability model (Paul's rule, 2026-08-30)

**Specs and whole branches are reviewed by the highest capability model
available at the time.** Today that is **Fable** (`claude-fable-5`). The
rule is "the highest available", not "Fable" — when a more capable model
ships, the rule follows it up the ladder without an edit here.

Two things get that review, and they are the two where a mistake is
expensive to unwind:

- **A spec, before it becomes a plan.** A spec is the document every
  later decision inherits from. Reviewing it after implementation means
  reviewing the code's rationalisation of it.
- **A branch, before it squash-merges to `main`.** Read as a single
  diff, not commit by commit — that is the one reading that catches a
  branch which is individually-sensible commits adding up to the wrong
  thing.

Name the model explicitly when dispatching a review. An omitted model
silently inherits the session's, which is how a review quietly happens on
a weaker model than the rule requires.

The rest of the delegation ladder, unchanged from the family's policy:

- **Haiku** — transcription-grade work: the plan contains the exact code,
  single-file mechanical fixes, doc edits.
- **Sonnet** — implementation from a precise brief: 1–3 files, clear
  interfaces, established conventions to follow.
- **Opus** — design judgment, multi-file integration, anything security-
  or production-touching.
- **Fable** — spec review, whole-branch review, and implementation whose
  correctness rests on several interacting invariants at once. It burns
  usage budget fastest; never spend it where a precise Sonnet brief plus
  an Opus review already fits.

A precise brief beats a capable model. Exact values, file paths and
interfaces go in the brief.

## Shipping cadence (Paul's rule, 2026-08-30)

**Work happens on a worktree, on a branch. Never on `main`.**

1. **Worktree per piece of work.** `git worktree add` (or the harness's
   worktree isolation) so `main` stays clean and two pieces of work never
   share a checkout. The branch is created with the worktree, not after.
2. **Push regular commits to the branch.** Commit as you go and push
   often — small commits, pushed, so the work is visible, recoverable,
   and reviewable as a sequence. Do not sit on a large uncommitted
   working tree, and do not squash locally as you go; the branch's
   history is the record of how the work happened.
3. **Squash merge to `main` when, and only when, all three are true:**
   - the work is **done** — not "done except", not "done pending a
     follow-up commit";
   - it is **approved** — the branch has had its highest-capability-model
     review above, and Paul has approved anything that was his call;
   - **CI is green** — `amadan ci status -branch <n>` exits 0. Exit 2 is
     pending, not green. Exit 3 means nothing has run, which is not green
     either.

Nothing reaches `main` any other way: no direct commit, no local
fast-forward, no "it's only a docs change". That includes doc-only edits,
one-line fixes, and reverts.

The reason is timing rather than ceremony. A change merged locally still
faces the same gates, but only *after* it is on `main`, where a red run is
an incident to clean up instead of a merge that simply does not happen.

**The review record lives on amadan.** The pull-request equivalent is a
branch discussion — see below. **CI is `.amadan/ci.d/10-gate`**, triggered
by a push to the remote, and it is the same script you can run locally. It
covers the Rust crate, the wasm artifact, Yjs wire compatibility in both
directions, and the Go build and tests. `amadan ci status -branch <n>`
exit 0 is the merge gate; exit 2 is pending and exit 3 means nothing ran,
and neither is green.

**Finishing a branch is always the same two steps, in this order** (Paul's
rule, 2026-08-30): **push and open the branch discussion, then squash-merge
to `main`.** Never merge without the discussion existing first, and never
merge locally instead of pushing.

The order is the point rather than the ceremony. A squash merge deliberately
flattens the commit history that carried the branch's reasoning — the
rulings, the reversals, the things that turned out to be wrong twice. The
discussion is where that survives. If it does not exist before the merge,
the reasoning is not scattered, it is gone.

## Working through amadan

This repo is hosted on an amadan hub (`AMADAN_HUB=https://amadan.net`),
not GitHub. There is no `gh`, no pull request, and no GitHub Actions. The
full house rules are installed as a skill at
`.claude/skills/amadan/SKILL.md` — read it. What follows is what this repo
does with them.

**Post as the agent account, never as Paul.** An agent account is
`<name>~<operator keymail>`; everything it writes is badged and
attributable. The CLI detects a harness and refuses a human credential.
`-as-human` exists for a person genuinely typing, and it warns every time
— an agent reaching for it is the antipattern the badge exists to prevent.

**The prompt ledger is on, at `prompts` level.** `amadan ledger init` has
installed the post-commit hook, the Claude Code hooks in
`.claude/settings.json`, and the ledger refspecs, so prompts record to
`refs/amadan/ledger/<branch>` with nothing to do per commit. Two
consequences worth knowing:

- **A bare `git push` now pushes HEAD *and* the ledger refs**, because
  `remote.origin.push` is set and `push.default` no longer applies.
- **Never edit ledger refs by hand.** `amadan ledger redact` and
  `amadan ledger withhold` are the only sanctioned removals, and both
  leave a visible mark. That is the point of them.

**Every branch gets a discussion, and it is the review record.**
`amadan discuss new <ns>/docs -branch <n>` opens it; that thread is where
the branch is read as one piece, where the highest-capability-model review
is posted, and where approval is recorded. One thread per topic — reply on
it rather than opening a sibling. Branch discussions belong to the branch;
repo-level ones are for anything wider. Close what you opened.

**Tasks are a checklist, not a log.** `amadan task add -branch <n>` for
work someone will do, `amadan task advance` when it is done. Commentary
belongs in the discussion, not in task titles. A branch with real work on
it should have tasks that a reader can scan to see what is left.

**Rebuilding the editor bundle is not hand-mergeable** (branch
convention, agreed 2026-08-31 between the three sessions then working in
this repo). `internal/docs/static/editor.<hash>.js` is a single
committed artifact whose name is the hash of its own contents, and
`assets.go` panics at init if it finds more or fewer than one. Two
branches that both ran `fixtures/build-editor.mjs` therefore conflict
mechanically even when no source line overlaps. Whoever squash-merges
second discards their own bundle from the merge and re-runs the build on
top of `main`. Never reconcile the minified file by hand.

**When something is refused, say so in the discussion rather than working
around it.** An `agent` grant is writer-equivalent within one namespace:
push, post, manage tasks, nothing beyond. Refusals are usually a missing
grant or a genuine boundary, and both are worth surfacing rather than
routing around.

## Working with the sibling sessions

Docs is one of three moving parts and the other two are live:

- **`../sheets`** (`amadan.net/oficina/sheets`) — shares the suite home,
  the design-system primitives, and several rulings. Its spec is
  `docs/superpowers/specs/2026-08-30-sheets-design.md`. Docs is a
  declared consumer of the home contract in its §9.
- **The design system** — `rastrillo.org/design-system`, spec at
  `docs/superpowers/specs/2026-08-28-design-system-design.md` in the
  rastrillo repo. Vendor tokens through `ui.TokensCSS()` and
  `ui.ThemeCSS(name)` rather than copying files by hand.

When a decision affects one of them, tell that session rather than
writing around it. Several of this project's better decisions came from
one session's question exposing another's bug.

## Conventions that are already settled

- **Markup uses the bare `rst-*` attribute grammar** (`<div rst-inspector>`),
  variants in the value matched with `~=`. Unparked by Paul 2026-08-31, once
  both of his conditions were met and verified: `rastrillo v0.21.0` styles
  **both** spellings (`ui/tokens.css` carries 343 bare-attribute selectors
  alongside 398 class ones), and the design-system guidance exists.
  Two constraints come with it. **Adopt per screen, not per element** —
  mixed authoring inside one component matches neither spelling by design.
  And **pin `v0.21.0` or later**, because on anything earlier attributes
  render unstyled.
  *(This reversed twice before landing here, both times by applying a
  conclusion drawn about one repo to a different one without re-deriving it.
  The reasoning is in spec §10.1.1.)*
- **Anything Docs contributes to estilo takes an `estilo-*` prefix, never
  `rst-*`.** That namespace is the framework's and a future version may claim
  a name we took — rastrillo's own migration found `rst-form__foot` and
  `rst-form-foot` both live inside `ui`.

- **A spec ruling, code on `main`, and code in the latest release are three
  different things.** Reading one tells you nothing about the other two.
  `v0.19.0` (2026-08-24) is 18 commits behind rastrillo's `main` and lacks
  the themes, shells and locales the design-system spec describes, so
  `go get` alone will not give you them. Say which of the three you mean
  whenever you cite the framework, and stamp any commit count with the sha
  and date you took it at — a bare number ages into a wrong fact.

### Watch for tools that answer wrongly instead of failing

A tool that errors teaches you something. A tool that returns a confident
wrong answer does not, and both of the ones below cost this project real
time in a single afternoon:

- **Citing your working tree instead of `origin/main`.** A checkout parked
  on a feature branch gives you *true statements about the wrong tree*,
  complete with real line numbers that verify. Cite `origin/main` after a
  fetch, or a tag, and say which. Never your checkout.
- **`git branch --merged` against a squash-merging repo.** It returns an
  empty list that reads as "never merged" while `main` carries every line,
  because a squashed branch is not an ancestor. Compare content, not
  ancestry.
- **`carlos channels` after a promote.** It reports which release a
  channel *points at*, which is not the same question as which release
  the box is *serving*. Adoption runs on an hourly timer, so the two can
  disagree for up to an hour after `carlos promote`: observed
  2026-09-01, `edge` reading the new version while the live host served
  the previous one for five minutes straight, closed within seconds by
  `carlos restart`. Neither command fails and nothing says "not live
  yet". Trust `x-carlos-version` on the real host, or use `carlos
  deploy`, which ships, promotes and then watches the URL until it
  actually serves the new version. `carlos channels` cannot distinguish
  "serving it" from "pointed at it".
  *(Recorded from the live-sync deploy; not re-verified since, so the
  timer's exact behaviour is one observation rather than a measurement.)*

- **A library function that ignores its own "dry run" argument.**
  `y-prosemirror`'s exported `undo` is `state => ...undoManager?.undo()`
  — it takes no `dispatch` and undoes immediately. ProseMirror's command
  contract says a null `dispatch` performs nothing and only reports
  whether it could, which is how any toolbar decides whether to grey a
  button out — on every state update, because that is when the answer
  changes. So the formatting bar asked "could I undo?" after every
  change and the library answered by undoing it: typing did nothing,
  every command did nothing, no error anywhere, Go suite green.
  (`undoCommand`/`redoCommand` are the dispatch-aware pair.) The general
  form: **a convention is not a guarantee**, and the functions that break
  one are the ones that look most like they honour it. Found 2026-09-01
  on the formatting-and-export branch, by a browser drive, after an
  evening of looking at the wrong file.

The shape to watch for is a verification that cannot distinguish "no" from
"I can't see it". When you catch one, write it here rather than resolving
to be more careful — care is not a mechanism.

**A new test must be seen to fail.** Corollary of the above, and cheap:
after writing one, break the thing it covers and watch it go red, then
put it back. `scripts/browser-drive.sh` was kept only after it was
confirmed to fail on the build whose bug it had just found — without
that step a green run means "passed" and "could not see" equally.

**And it must fail for the right reason.** Watching it go red is not
enough — read *why* it went red. Three of these were found in one
evening on the `list-thumbnails` branch, in three different shapes:

- A test that could not fail at all. `TestPageOneDoesNotDisturbPDFExport`
  exported a document, ran the thumbnail layout, exported again and
  compared bytes — but the two calls share no state, so only a
  package-level global could ever have reddened it. It stayed green
  while a deliberate `l.cursor += 100` inside the code it claimed to
  guard reddened a different test. Two comments and a commit message
  cited it as proof. Deleted.
- A mutation that reddened the right test for the wrong reason. Proving
  a canary test could fail, the injected leak used a template variable
  that was not in scope, so the page 500ed and the test failed on its
  status check without the canary ever firing — a red test proving
  nothing. Redone with a leak that actually renders. **If your mutation
  changes the response code, you have tested the mutation, not the
  test.**
- A hit test that measured something else. Recorded in `list.html`:
  restated declarations "fixed" row clicking, and the probe was hitting
  rastrillo's own overlay both before and after.

The generalisation, and the question worth asking of any gate: **what
would make this go red, and have I actually done it?** A CSS rule keyed
on an attribute nothing writes is the same defect in another costume —
it matches no element, so it is invisible to every test and every
screenshot, and any bug inside it is unreachable. The Sheets session
found one of those the same night, hiding a second defect behind it.
Worth a grep when touching a stylesheet; not worth a permanent gate,
because it has enough false positives (comments quoting old selectors,
framework-emitted hooks) that people would learn to skim it — and a
gate nobody reads is another probe that answers the same in both
worlds.

**Do not pass on somebody else's finding as your own verification.** A
report from another session is evidence about what they saw, not a fact
you have checked, and relaying it flatly launders it into one. Say where
it came from, or check it — and note that "two of us noticed
independently" is not corroboration when the second of you is the one
who mentioned it first. (Written after exactly that: a merge commit on
`main` was reported here as a breach of the shipping cadence and passed
on to Paul as one, when `amadan branch merge` produces a merge commit
itself and the branch had landed correctly. One minute with the tool
would have caught it.)
- **rastrillo is `amadan.net/rastrillo/rastrillo`.** The old path,
  `github.com/carlosframework/rastrillo`, stops at v0.23.0: code importing
  it gets no fix, and its own checks cannot tell (Docs zoomed iOS on every
  tap for a month that way). `TestNoPreMoveRastrillo` (internal/lint)
  allows one import of it, native.go's bridge to rastrillo/native's
  server, until that server moves. The CLI is pinned as a `tool` in
  go.mod: run it as `go tool rastrillo …`, never a `rastrillo` from PATH.
  `scripts/build-release.sh` stamps `amadan.net/rastrillo/rastrillo.BuildVersion`;
  the old symbol leaves `/api/version` answering "dev"
  (`TestReleaseBinaryReportsItsVersion`).
- **Docs serves rastrillo's and estilo's assets from memory**
  (render.go's `vendoredStaticFS`), so `go tool rastrillo doctor` finds
  nothing vendored to check. If a future `doctor --fix` ever writes a
  rastrillo.js, busy.js or tokens.css to disk, delete it: doctor's shim
  carries the old busy rule, which fights estilo's busy.js.
  `TestBusyRuleIsLinkedAndServed` fails if one is left behind.
- **iOS zooms into any text control under 16px.** tokens.css lifts
  controls to 16px under `(pointer: coarse)`; a Docs rule that sets a
  control's font with a heavier selector must restate
  `font-size: max(1rem, 1em)` there too. `TestNoZoomOnTap` (browser tag:
  `go test -tags browser -run OnTap ./internal/docs/`, needs a Chromium via
  `RASTRILLO_CHROME` or a Playwright cache) measures every field.
- **Re-copy `tokens.css` deliberately on every rastrillo upgrade.** It is
  frozen into `static/` at scaffold time while the partials upgrade with the
  module. `rastrillo doctor` will make this mechanical; until it lands it is
  a manual step in the upgrade runbook.
- **Every user-visible string is a catalog key**, resolved at render time.
  Twelve base locales; a hardcoded English string is a bug.
- **WCAG 2.2 AA, no exemptions**: 24×24 targets, 320px reflow, and colour
  never carries meaning alone.
- **An ARIA role is a promise about behaviour, and an unimplemented one
  is worse than none.** `role="toolbar"` says "this is a single tab stop
  and the arrow keys move within it". Written without the roving
  tabindex behind it — as the formatting bar was, on this branch, until
  it was caught in review — it tells a screen-reader user that arrow
  keys are the way in, so those are what they try first, and the silence
  that follows reads as their own mistake rather than as a missing
  feature. Omitting the role would have been more honest and less
  harmful. Whenever you write a role, implement its keyboard contract in
  the same commit or do not write it.
  Two things that pattern hides, both found the same way:
  **a roving tab stop parked on a `disabled` control makes the whole
  group unreachable**, not merely awkward, and disabled is often the
  *default* state (undo and redo start that way on every page load) — so
  the "which control is the tab stop" decision has to re-run whenever
  anything is enabled or disabled, not on load. And **a closed native
  `<select>` changes its own value on Left/Right**, so a toolbar
  containing one must consume those keys and leave Up/Down alone.
  Neither is in the ARIA practices; both are unmissable in a browser and
  invisible in a unit test, which is what `scripts/browser-drive.sh` is
  for.

## Work in a worktree, and catch main up first

Take a worktree for the work. Do not work in the shared checkout.

    git -C <repo> fetch origin
    git -C <repo> worktree add ../<repo>-<task> -b <branch> origin/main

Both halves matter, for different reasons.

**The worktree**, because several sessions share this machine and some
share this repository, and a checkout has one HEAD, one index and one
working tree between all of them. A branch switch under somebody else's
uncommitted work destroys it with nothing on screen to say so, and two
sessions editing the same paths disagree quietly instead of conflicting
loudly. A worktree gives the work its own HEAD and its own files, and
costs a directory.

**From `origin/main`, after a fetch** — not from whatever the checkout
happens to be sitting on. A branch cut from a stale main is a merge
conflict scheduled for later, against files somebody else has renamed in
the meantime. A branch cut from another task's branch is worse: it
carries that task's commits into yours, where they are reviewed as
though you wrote them, and the two can only be separated by hand.

Remove it when the work has landed — `git worktree remove` — so that
`git worktree list` keeps saying something true.

Written down because of 2026-09-17, in `meet`. The shared checkout was
sitting on one task's branch while holding a *different* task's feature
uncommitted on disk — some four hundred lines, a new migration among
them — put there by a session nobody else knew was working in that tree.
Two other sessions were meanwhile coordinating over the same checkout by
asking each other not to touch it, which is the arrangement a worktree
makes unnecessary. Nothing was lost. Nothing about the arrangement would
have said so if it had been, and by then the same feature existed twice,
once on each side of a rename, because the two sessions had branched
from different starting points.
