--- date: 2026-09-29 branch: chore/decisions-log-conflict-free title: The handoff log was serialising every parallel branch — it is a directory now --- ## What changed `DECISIONS-LOG.md` is frozen and the handoff log is now a directory, `decisions/`, with one file per entry (`YYYY-MM-DD-.md`). Concurrent branches write disjoint paths, so there is nothing for git to merge. The problem was not any particular change. CLAUDE.md §7 told every agent to append a handoff, and appending always landed on the *same last hunk* of one 5,228-line file. Two branches that both did it could not auto-merge; N branches cost N-1 rebases, and each rebase invalidated the gate run before it. The agents work in isolated worktrees precisely so they don't block each other, and this one file undid that. - `decisions/README.md` — the convention, the template, and how to read the log. No index file and no sequence number, deliberately: an index is a shared mutable file (the thing that just failed) and a counter has two agents racing for the same number. The directory listing is the index. - `DECISIONS-LOG.md` — a "FROZEN ARCHIVE" header prepended; content otherwise untouched. Its SHA-256 is pinned in `decisions/ARCHIVE.sha256`. - `ci/decisions-log.sh` — a new gate stage. Asserts the archive matches its pin, and that every entry is named `YYYY-MM-DD-.md` with a real date, carries `date`/`branch`/`title` frontmatter agreeing with its filename, and has all three of §7's sections with a real body under each. Exit 0 or 1 only; it has no skip path by construction, because it reads only files in the tree under test and needs no tool and no cluster. - `ci/gate.sh` — runs the stage; both `fail` and (impossible) `skip` set `BLOCK`. There is no `REQUIRE_DECISIONS_LOG` knob: a guard with an off switch is not a guard. - `ci/tests/decisions-log.test.sh` — 22 assertions, new. - `ci/tests/gate-decision.test.sh` — 3 new assertions for the stage's wiring, **plus a pre-existing bug fixed**: its stub list never included `gitea-settings`, so that stage's script was absent from the stub CI dir, `run_stage` recorded exit 127 as a FAILED stage in every case, and 15 of 47 assertions failed on `main`. The suite was measuring its own missing stub rather than the decision logic. - `CLAUDE.md` §7 rewritten; §1's "stale runbook wording" caveat extended to the log. - `ci/Makefile`, `ci/README.md`, `.gitea/workflows/README.md` — the stage named where stages are named. A guard rather than 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 people's* branches, later, misattributed. ## What I verified Measured against the live Gitea (`GET /api/v1/repos/Home-Network/home-network-k3s-impl/pulls`, 2026-09-29), not inferred from the tree: #22 fix/crafty-oauth2-proxy-signin mergeable=False #20 feat/conformance-job-template mergeable=False #19 feat/conformance-trigger-kubeconfig mergeable=False #18 feat/conformance-runner-image mergeable=False #17 feat/ci-is-the-approver mergeable=True `git merge-tree --write-tree origin/main origin/` for each named `CONFLICT (content): DECISIONS-LOG.md` and **no other conflicting path** in all four. #17 is clean and touched no log entry. So the log's storage format was the whole of the blockage. Tests: `ci/tests/decisions-log.test.sh` **PASS (22 assertions)**, including the one that matters — an append to `DECISIONS-LOG.md` exits non-zero and the message names both the frozen archive and the file to write instead. `ci/tests/gate-decision.test.sh` **PASS (50 assertions)**, up from **FAIL (15 failed, 32 passed)** on `main` before the stub fix. Gate: see the run quoted in the PR description. ## What I did NOT verify - **Nothing was applied and nothing was merged.** No `kubectl`, no `flux reconcile`. This change touches only CI and documentation; it has no cluster-visible effect at all. - **L2/L3 conformance did not run** (`CLUSTER_AVAILABLE` unset), as always. Irrelevant to this change's content but stated so the gate result is not over-read. - **I have not watched a Gitea PR check run this stage.** The stage is proven by its own unit test and by the gate locally; that the `incluster-dind` runner executes it is unobserved here. A skipped Gitea job still reports `success`, so read the job log, not the tick. - **The four hung PRs are not yet fixed by this commit.** They still carry their `DECISIONS-LOG.md` appends and will still conflict until each is rebased onto this change with its append converted to a `decisions/` entry. That is follow-up work, in flight separately. - **`runbooks/` and older entries still say "append to DECISIONS-LOG.md"** in places. I did not sweep them. The gate now rejects the action, so the stale wording misleads but cannot cause the damage; it joins the existing tracked work on stale `git push origin main` wording.