Agents have been working in the operator's checkout and clobbering each other. Nothing in the tree told them not to: worktrees appeared in CLAUDE.md only as §3's gate workarounds, and decisions/README.md described worktree isolation as something agents already do rather than as a rule. New CLAUDE.md §0 states the rule, with the commands and an explicit do-not list for the shared checkout. Placed before §1 so every existing §-reference in the repo keeps its meaning. Measured in the shared checkout on 2026-10-05, each row a distinct failure mode: 107 `checkout: moving` reflog entries; sitting on feat/pm-agent-issue-to-pr, not main; two other runs' untracked trees left in it; local main two commits behind origin/main; 42 registered worktrees of which 31 are on already-merged branches; 9 gate scripts carrying a `.claude/` exclusion because an in-tree worktree made find-based discovery count the same manifest up to 18 times. Corrects a claim carried in earlier briefs: `git worktree remove` is NOT blocked by the agent sandbox. Provoked on a throwaway worktree — rc 0. §3 reframed as the normal path and grown from two breakages to three: spec-immutability's skip is avoidable, not merely tolerated, with the spec repo mounted read-only in the same ENGINE shim plus -e SPEC_REPO=/home-network-k3s. CLAUDE.md is a sensitive path, so this PR needs a non-author review or an administrator /approve-override posted as a PR review. Not posted by me: §1 forbids an agent clearing that on its own PR. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
103 lines
4.2 KiB
Markdown
103 lines
4.2 KiB
Markdown
# `decisions/` — the handoff log, one file per entry
|
|
|
|
This directory **is** the decision log. `../DECISIONS-LOG.md` is the frozen archive of
|
|
everything written before 2026-09-29 and is never edited again (see "The archive is
|
|
frozen" below).
|
|
|
|
## Why this exists
|
|
|
|
`DECISIONS-LOG.md` was append-only in a single file, and every agent appended to its
|
|
**tail**. Two branches that both append hit the same last hunk, so git cannot
|
|
auto-merge them: the second one to be reviewed conflicts, and so does the third, and
|
|
the fourth. Measured on 2026-09-29: **four of the five open PRs were unmergeable, and
|
|
`DECISIONS-LOG.md` was the only conflicting file in all four**:
|
|
|
|
#22 fix/crafty-oauth2-proxy-signin CONFLICT (content): DECISIONS-LOG.md
|
|
#20 feat/conformance-job-template CONFLICT (content): DECISIONS-LOG.md
|
|
#19 feat/conformance-trigger-kubeconfig CONFLICT (content): DECISIONS-LOG.md
|
|
#18 feat/conformance-runner-image CONFLICT (content): DECISIONS-LOG.md
|
|
#17 feat/ci-is-the-approver clean (it touched no log entry)
|
|
|
|
Nothing was wrong with any of those changes. The log's *storage format* serialised work
|
|
that was otherwise parallel, and it got worse with every concurrent branch: N branches
|
|
produce N-1 rebases, each of which invalidates any gate run that preceded it.
|
|
|
|
The fix is the obvious one for concurrent appenders: **stop sharing a file**. Each
|
|
handoff is its own file. Two agents writing handoffs touch disjoint paths, so there is
|
|
nothing for git to merge and nothing to conflict. This scales to any number of parallel
|
|
branches with zero coordination — which is the point, since the agents are deliberately
|
|
isolated from each other in separate worktrees (required by `CLAUDE.md` §0) and cannot
|
|
coordinate.
|
|
|
|
There is deliberately **no index file** and no counter. An index would be a shared
|
|
mutable file — exactly the thing that just failed — and a sequence number would have
|
|
two agents racing for `0043`. The directory listing is the index.
|
|
|
|
## Writing an entry
|
|
|
|
One file per handoff, named:
|
|
|
|
decisions/YYYY-MM-DD-<slug>.md
|
|
|
|
`<slug>` is lowercase `[a-z0-9-]`. **Use your branch name's last segment** — it is
|
|
already unique per unit of work, which is precisely the property the filename needs.
|
|
A branch `fix/crafty-oauth2-proxy-signin` handed off on 2026-09-29 writes:
|
|
|
|
decisions/2026-09-29-crafty-oauth2-proxy-signin.md
|
|
|
|
The file is YAML frontmatter followed by three required sections:
|
|
|
|
```markdown
|
|
---
|
|
date: 2026-09-29
|
|
branch: fix/crafty-oauth2-proxy-signin
|
|
title: The Keycloak gate works — Crafty's own login was the wall
|
|
---
|
|
|
|
## What changed
|
|
|
|
Prose. What you actually did, and why.
|
|
|
|
## What I verified
|
|
|
|
The measurements, with their numbers. Quote the field you read, not the prose you
|
|
believed. Name the gate result and its exit code.
|
|
|
|
## What I did NOT verify
|
|
|
|
Everything else. An honest "UNVERIFIED" is worth more than an optimistic "green" —
|
|
this repo has been burned by the difference more than once.
|
|
```
|
|
|
|
All three headings are **required and enforced** (`ci/decisions-log.sh`). They are
|
|
CLAUDE.md §7's contract; making them a checked structure is what stops an entry
|
|
degrading into "done ✅".
|
|
|
|
## Reading the log
|
|
|
|
The whole log, oldest first:
|
|
|
|
ls decisions/[0-9]*.md | sort
|
|
|
|
The current PM handoff — the newest entry. Ties within a day are broken by what landed
|
|
on `main` last, which is the honest answer, so ask git rather than the filename:
|
|
|
|
git log --diff-filter=A --format='%H' --name-only -- 'decisions/[0-9]*.md' \
|
|
| grep '^decisions/' | head -1
|
|
|
|
For a quick skim of who said what:
|
|
|
|
grep -H '^title:' decisions/[0-9]*.md | sort
|
|
|
|
## The archive is frozen
|
|
|
|
`DECISIONS-LOG.md` is **read-only history**. Its SHA-256 is pinned in
|
|
`decisions/ARCHIVE.sha256` and `ci/decisions-log.sh` fails the merge gate if the file
|
|
changes. That is not ceremony: without it, the next agent to read `runbooks/` or an old
|
|
CLAUDE.md habit appends to the tail again and re-creates the conflict for everybody
|
|
else, silently, one PR at a time.
|
|
|
|
If the archive genuinely must change (a leaked credential to redact, say), that is a
|
|
deliberate, reviewed act: change it, run `sha256sum DECISIONS-LOG.md > decisions/ARCHIVE.sha256`,
|
|
and say in your handoff entry why. Re-pinning is visible in the diff. Appending is not.
|