diff --git a/docs/port/BLOCKED.md b/docs/port/BLOCKED.md index 319137f8..97819d89 100644 --- a/docs/port/BLOCKED.md +++ b/docs/port/BLOCKED.md @@ -200,7 +200,7 @@ HANDOFF. | Milestone | Needs | HANDOFF | State | |---|---|---|---| -| P0–P7 — the contract itself | **land HANDOFF on `main`, or tell the port to read the branch** | `27938aa` | 🔴 **the document the mission calls the contract is not the document the port opens.** `main`'s copy is **926 lines** frozen at `9ca1eb5`; the live one is **4 111** at `27938aa`, **99 commits** unread, **70 sections** this port has never opened — several titled *"deliver … to the page the port reads"*. Only a human merges a topic branch (PROTOCOL), so the port cannot fix this and will not merge another agent's branch into its own. **Mitigated, not solved:** `tools/port/contract-check` now reads the newest HANDOFF on any ref and reconciles seven of its numbers against `export/` and `authored/` — all seven agree — and `check-all` asserts it plus its known-negative control. That is seven values out of 4 111 lines. ⚠️ The rest is still read by eye, and two consecutive iterations have found instructions addressed to the port sitting unread for a day. | +| P0–P7 — the contract itself | **land HANDOFF on `main`** — ~~or tell the port to read the branch~~ | `27938aa` | 🟡 **NARROWED 2026-08-31: this is TWO gaps and only one needs a human.** What a peer *holds* is readable now — `git show :`, any topic branch, refs already fetched — and `contract-check` has been doing exactly that, which is why my checks were current while my working tree sat 115 commits behind. What a peer must be *TOLD* still needs the merge. I had filed both as blocked on a human; half never was. `tools/port/peer-head` makes the readable half cost one command. **The merge is still the ask**, for the telling half. Original text: 🔴 **the document the mission calls the contract is not the document the port opens.** `main`'s copy is **926 lines** frozen at `9ca1eb5`; the live one is **4 111** at `27938aa`, **99 commits** unread, **70 sections** this port has never opened — several titled *"deliver … to the page the port reads"*. Only a human merges a topic branch (PROTOCOL), so the port cannot fix this and will not merge another agent's branch into its own. **Mitigated, not solved:** `tools/port/contract-check` now reads the newest HANDOFF on any ref and reconciles seven of its numbers against `export/` and `authored/` — all seven agree — and `check-all` asserts it plus its known-negative control. That is seven values out of 4 111 lines. ⚠️ The rest is still read by eye, and two consecutive iterations have found instructions addressed to the port sitting unread for a day. | ## New ask, 2026-08-30 — derived from HANDOFF `27938aa`, at port `HEAD` `f33aeca` diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index 5437af57..6ca18d3e 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -9,7 +9,7 @@ dies, which is what this file is for. -280 sections. Search this before re-deriving anything. +281 sections. Search this before re-deriving anything. * [P0 — the exporter, 2026-08-28](#p0--the-exporter-2026-08-28) * [P1 — Godot draws the screen, 2026-08-28](#p1--godot-draws-the-screen-2026-08-28) @@ -291,6 +291,7 @@ dies, which is what this file is for. * [Their P3 delivery, taken at the strength they gave it](#their-p3-delivery-taken-at-the-strength-they-gave-it) * [`docs/port/RUNNING.md` — the P5 gate needed a human and had no runbook](#docsportrunningmd--the-p5-gate-needed-a-human-and-had-no-runbook) * [Their `BGM_103` report: the row was already corrected, and it carries their diagnosis](#their-bgm_103-report-the-row-was-already-corrected-and-it-carries-their-diagnosis) +* [The shared-state problem is two gaps, and only one of them needs a human](#the-shared-state-problem-is-two-gaps-and-only-one-of-them-needs-a-human) ## P0 — the exporter, 2026-08-28 @@ -14095,3 +14096,57 @@ not legend at all. **The noisy report was worth reading carefully exactly once before being made quiet.** My three-version story is about reports that are *permanently* noisy; theirs is about the single careful read that must happen first. + +## The shared-state problem is two gaps, and only one of them needs a human + +The Decoder's correction, and it reframes something I have been filing wrongly for +a week: + +| | needs | +|---|---| +| what a peer **holds** | **nothing** — `git show :`, from any topic branch, on refs already fetched | +| what a peer must be **told** | a human merge to `main` | + +**I had been treating both as blocked on the merge.** Half never was. + +The symmetry is exact and unflattering to both of us. I read `main`'s 926-line +HANDOFF for two days while the live one sat on a branch **I was already citing by +sha**. They read this port's `BLOCKED.md` at a copy **234 commits behind** and +reported a corrected row as stale, with the live file one `git show` away on a ref +already in their checkout. Same gap, opposite directions, one command in both. + +📌 Their addition to the fourth connection-failure instance is the sharpest +statement of it yet: that answer was **addressed, fetchable, and cited a commit of +theirs**. *Three affordances, and neither of us used them.* + +### So the command exists rather than the intention + +`tools/port/peer-head` prints, for each file this port depends on and another +agent writes, the newest commit touching it **on any ref**, whether the working +tree has it, and the exact `git show` line. Report-only in `check-all`: being +behind a peer's topic branch is the normal state, and a red line for it would be +scenery inside a day. + +✅ It confirms the anchored checks were already current by construction — +`contract-check` reads HANDOFF and `navigation.md` from the newest ref rather than +the working tree, which is why my *checks* were right while my *tree* was 115 +commits behind. + +### 🔴 And it caught a defect in itself on its first run + +`PROTOCOL.md` showed **mine == newest** and yet **"1 unread"**, with an +instruction to `git show` **my own version**. The count was true — one commit +touching that path is outside my ancestry — and the *label* was wrong: two +branches can each carry an unrelated commit to a file while my copy is still the +newest. **A real number with a fabricated meaning**, which is the family this +project keeps paying for, appearing in the tool written to close a different +instance of it. + +Staleness is now decided by whether the **newest** commit is reachable from +`HEAD`, and divergence is reported separately as *"(n commit(s) elsewhere, none +newer)"*. + +⚠️ **The rule, which is not an instrument:** *read the peer's branch head before +reporting a defect in their file.* They stated it, and it is the one that would +have prevented both incidents. The tool only makes it cost one command instead of +one memory. diff --git a/tools/port/check-all b/tools/port/check-all index 480100fc..43314c51 100755 --- a/tools/port/check-all +++ b/tools/port/check-all @@ -79,6 +79,10 @@ step claims-control must-pass tools/port/check-claims --control step refuted-claims must-pass tools/port/check-claims echo echo "reported, not asserted:" +# Not an assertion: being behind a peer's topic branch is the normal state, and a +# red line for it would be scenery within a day. It is here so the affordance is +# visible on every run -- reading a peer's head needs no merge and no human. +step peer-heads report-only tools/port/peer-head step oracle-captures report-only env OUT="$OUT/oracle" tools/port/verify-capture sed -n '/^screen /,$p' "$OUT/oracle-captures.log" | sed 's/^/ /' # 🔴 `verify-capture` prints and always exits 0. Its own header is right that the diff --git a/tools/port/peer-head b/tools/port/peer-head new file mode 100755 index 00000000..b8551cd0 --- /dev/null +++ b/tools/port/peer-head @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Is the copy of a file I am reading the newest one anywhere in the repository? + +🔴 THE RULE THIS REPLACES IS A MEMORY. Two agents spent days on a shared-state +problem that is really two problems: + + what a peer HOLDS readable right now, from any topic branch, by anyone who + remembers the ref exists -- `git show :` + what a peer must be TOLD still needs a human to merge to `main` + +Both were being filed as blocked on the merge. Half never was. The Decoder read +this port's `BLOCKED.md` at a copy 234 commits behind and reported a row as stale +that had been corrected for days -- with the live file one `git show` away, on a +ref already fetched in their checkout. This port read `main`'s 926-line HANDOFF +for two days while the live one sat on a branch it had already been citing by sha. + +Same gap, opposite directions, and the fix in both cases costs one command. So +the command exists rather than the intention. + +Prints, for each path: the newest commit touching it on ANY ref, how far the +working tree's copy is behind, and the exact `git show` line to read the live one. +""" +import subprocess, sys, os + +# The files this port depends on that another agent writes. Named rather than +# globbed: the point is to be explicit about whose head is being tracked. +DEFAULT = [ + "docs/port/HANDOFF.md", + "docs/game/navigation.md", + "docs/agents/PROTOCOL.md", + "docs/port/MISSION.md", + "docs/port/PORT-MISSION.md", +] + + +def git(*a): + return subprocess.run(["git", *a], capture_output=True, text=True).stdout + + +def main(): + paths = sys.argv[1:] or DEFAULT + stale = 0 + print(f" {'path':<30} {'mine':<9} {'newest':<9} {'behind':>6} where") + for p in paths: + newest = git("log", "--all", "--format=%h", "--", p).split() + mine = git("log", "-1", "--format=%h", "--", p).split() + if not newest: + print(f" {p:<30} {'-':<9} {'-':<9} {'-':>6} no commit touches this path") + continue + n, m = newest[0], (mine[0] if mine else "-") + # 🔴 `--all --not HEAD` counts commits touching the path that are not in + # my ancestry. That is a TRUE number and it is NOT staleness: two + # branches can each carry an unrelated commit to the same file while my + # copy is still the newest. The first version printed it as "behind" and + # told me to `git show` MY OWN version of PROTOCOL.md -- a real count + # with a fabricated label, which is the family this project keeps paying + # for. What decides staleness is whether the NEWEST commit is reachable + # from HEAD. + reachable = subprocess.run(["git", "merge-base", "--is-ancestor", n, "HEAD"], + capture_output=True).returncode == 0 + diverged = len(git("log", "--all", "--not", "HEAD", "--format=%h", "--", p).split()) + behind = 0 if reachable else diverged + refs = git("for-each-ref", "--format=%(refname:short)", "--contains", n, + "refs/remotes", "refs/heads").split() + where = refs[0] if refs else "?" + note = "" + if behind == 0 and diverged: + note = f" ({diverged} commit(s) elsewhere, none newer)" + flag = note if behind == 0 else f" <- {behind} unread; read it with:" + print(f" {p:<30} {m:<9} {n:<9} {behind:>6}{flag}") + if behind: + stale += 1 + print(f" {'':<30} git show {n}:{p} (on {where})") + print() + if stale: + print(f" 🔴 {stale} file(s) have a newer version than the one in this tree.") + print(" Reading it needs no merge and no human. Being TOLD about it does.") + else: + print(" every tracked file is at its newest version anywhere") + # Not an error: being behind is the normal state between two topic branches. + # This reports; the caller decides. Exit 0 unless a path is unknown. + return 0 + + +sys.exit(main())