DECISIONS-LOG.md was append-only in ONE file and every agent appended to its
tail, so any two concurrent branches collided on the same last hunk. Measured
against the live Gitea today: four of the five open PRs were mergeable=false,
and `git merge-tree` named DECISIONS-LOG.md as the ONLY conflicting path in
all four.
#22 fix/crafty-oauth2-proxy-signin CONFLICT: DECISIONS-LOG.md
#20 feat/conformance-job-template CONFLICT: DECISIONS-LOG.md
#19 feat/conformance-trigger-kubeconfig CONFLICT: DECISIONS-LOG.md
#18 feat/conformance-runner-image CONFLICT: DECISIONS-LOG.md
#17 feat/ci-is-the-approver clean — it wrote no handoff
Nothing was wrong with any of those changes. The log's storage format
serialised work that was otherwise parallel, and the cost grew with every
concurrent branch (N branches -> N-1 rebases, each invalidating the gate run
before it). The agents work in isolated worktrees precisely so they do not
block each other; this one file undid that.
So: stop sharing a file. Handoffs are now one-per-file under decisions/,
named YYYY-MM-DD-<slug>.md. Disjoint paths have nothing to merge. There is
deliberately no index file (a shared mutable file is the thing that just
failed) and no counter (two agents would race for the same number) — the
directory listing is the index.
DECISIONS-LOG.md is frozen as the pre-2026-09-29 archive, its SHA-256 pinned
in decisions/ARCHIVE.sha256, and a new gate stage rejects any change to it.
That is a guard and not a paragraph because the old habit is written into the
runbooks and into every agent that has read the log, and getting it wrong does
not fail: it produces a clean-looking commit that re-creates the conflict on
OTHER branches, later, misattributed.
ci/decisions-log.sh new stage: archive pinned; every entry named with
a real date, carrying date/branch/title frontmatter
that agrees with its filename, and all three of
CLAUDE.md §7's sections with a real body under each.
Exit 0 or 1 only — no skip path by construction, so
no fail-open surface on a guard that closes one.
ci/gate.sh runs it; fail and (impossible) skip both BLOCK.
No REQUIRE_DECISIONS_LOG knob: a guard with an off
switch is not a guard.
ci/tests/decisions-log.test.sh 22 assertions, incl. the one that matters —
an append to the archive is rejected, and the
message names the file to write instead.
ci/tests/gate-decision.test.sh +3 assertions for the new wiring, AND a
pre-existing bug: the stub list never included
gitea-settings, so run_stage saw exit 127 and
recorded a FAILED stage in every case. 15 of 47
assertions failed on main for that reason alone —
the suite was measuring its own missing stub.
Now 50/50.
CLAUDE.md §7 rewritten; §2b widened to the new path.
ci/Makefile, ci/README.md, .gitea/workflows/README.md — stage named where
the other stages are named.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
102 lines
4.2 KiB
Markdown
102 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 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.
|