docs: handoff for resuming on another machine

Written to be read cold by a session with no context: what is established
versus what is someone's claim, which of the four actors the gate actually
binds, and what is in flight but unpushed.

The transferable part is §7 -- one failure shape recurred five times in two
days across two agents and the assistant: a property inferred from something
ADJACENT to it rather than tested directly. Protection from a settings page,
reachability from a DNS record, a label count from a grep that matched the
function definition, Gitea's host from a DNS lookup, and a clippy version from
rustfmt output parity. The last is the sharpest: rustfmt 1.8.0 and 1.9.0, nine
months apart, both emit 774 hunks here, so formatting parity carries no
information about which clippy ran -- and the conclusion drawn from it (that
CI's green was cached or ungated) was wrong in a way that would have sent
someone chasing a phantom.
This commit is contained in:
MechaCat02
2026-09-06 14:01:39 +02:00
parent e0bcef1ec2
commit 08b589fb70

View File

@@ -0,0 +1,247 @@
# Handoff — 2026-09-06
**For a fresh session on a different machine.** Written to be read cold: it
assumes you know nothing about what happened, and it says what is *established*
versus what is *someone's claim*.
---
## 1. What this project is
Two things, and they are easy to confuse:
* **The long game** — *Project Sylpheed: Arc of Deception — Reborn*, a clean-room
native port of an Xbox 360 game in Rust + Bevy. Values and behaviour come from
the original by **observation and static RE only** — never copied decompiled
code. The oracle is **the real game running in Xenia Canary**, never any
renderer of ours.
* **The work of 2026-09-04/06, which is what this document is about** — moving
the project's *working surface* onto a self-hosted Gitea, and getting CI to
produce an answer for the first time.
If you only read one other file, read [`PROTOCOL.md`](PROTOCOL.md).
## 2. The actors — four, and only two are constrained
| | what it is | identity | constrained by the gate? |
|---|---|---|---|
| **the human** | directs everything; the only approver and merger | `fabi` | n/a — is the gate |
| **the Pi agent** | supervisor, runs on the Pi beside Gitea | `sylph-pi <pi@sylpheed.local>`, **no Gitea account** | ❌ has `gitea admin`; can mint tokens, edit rules |
| **you** (this session) | runs on the x86_64 desktop, holds the push credential | commits as the human ⚠️ | ❌ same carve-out, different mechanism |
| **the two loop agents** | Decoder (disc→meaning) and Port (disc→playable) | `sylph-decoder`, `sylph-port`, Write not Admin | ✅ |
⚠️ **The looping agents are stopped and must stay stopped** until their images
carry the Gitea MCP and their issues are ready. Starting them early gives them a
brief telling them to read notifications and open issues with no tool that can.
📌 The two *supervising* agents are the ones the gate does not bind. That is
written into `GITEA-SETUP.md` Phase 2 deliberately. Neither has a distinct Gitea
identity; both operate through the human's credential or an unlinked git author.
**That is a known, unresolved wart**, not an oversight.
## 3. The machines
| | | |
|---|---|---|
| **desktop** `fabi-Hyrican-PC` | x86_64, 12 core, 15 GB | agent containers, the repo clone, **the only push credential** |
| **the Pi** `raspberrypi.fritz.box` | aarch64, on the LAN | **runs Gitea** (published through a VPS), the CI runner, and the supervising agent |
| **Gitea** `git.mc02.dev` | 1.25.5 | resolves to a hosted address — that says nothing about the origin, which is the Pi |
**File transfer between them is by hand.** The Pi agent has no push credential,
so its work arrives as `git bundle` over `scp`, which the human runs. This is the
weakest link in the setup: four round trips on 2026-09-04, each needing a
password twice, and once a guessed filename that was wrong. A `write:repository`
token on the Pi scoped to `pi/*` branches would remove it — **a deliberate
decision, deliberately not taken yet.**
⚠️ **`CARGO_BUILD_JOBS=4`** and limited `-j`. A full-parallel build has
OOM-crashed the desktop. One emulator process at a time; Canary runs muted.
## 4. Where things stand
### Merged to `main`
Nothing since `59649824`. **`main` is protected** — `enable_push=false`, 1
approval required, merge and approvals both whitelisted to `fabi` only. Verified
behaviourally: a real push was refused with `pre-receive hook declined`, as the
repository owner.
### Open pull requests
| | head | what | why not merged |
|---|---|---|---|
| **#10** | `agents/gitea-mcp` @ `a3d99ada` | the Gitea surface: MCP wiring, `gitea-protect`, the runbook, two PROTOCOL rules | waiting on the human. Merge-on-merits: docs and tooling, no `.rs` |
| **#14** | `fix/clippy-lints` @ `d8807c4f` | 73 clippy lints → 0 across four crates, `Closes #13` | same |
🔴 **Both need the admin override to merge.** `fabi` authored them and is the
only whitelisted approver, and Gitea bars self-approval — so they can never reach
one approval. `block_admin_merge_override` is `false` *deliberately* so that door
stays open. Do not tick it.
### Open issues
Nine work items, all `state/approved`, awaiting the loop agents:
```
#1 F1 measure (decoder) ←blocks— #2 F1 implement (port)
#3 F2 disc gain (decoder) ←blocks— #4 F2 playback gains (port)
#5 F3 title cue (decoder)
#6 OPTIONS re-propose (port)
#8 F5/F6 findings (decoder) ←blocks— #7 F5/F6 port work (port)
#9 f6-out-of-sample residue (decoder)
```
Four infrastructure issues:
* **#11** `state/proposed` — WASM. **Work exists past its shape; see §6.**
* **#12** `state/proposed` — rustfmt: 774 hunks, deliberately deferred.
* **#13** `state/approved` — clippy; `#14` closes it.
* **#15** `state/proposed` — the lint gate floats `@stable`.
### Unmerged branches
```
agents/gitea-mcp 11 → PR #10
fix/clippy-lints 16 → PR #14
auto/frame-blend-draw-path 495 the Decoder's corpus — returns via #8
auto/port-p6-audio 366 the Port's work — returns via #7
```
The last two are the reason `#7` depends on `#8`: `port/scripts/boot.gd` cites
`docs/re/` pages that exist on **neither** its own branch nor `main`.
`tools/port/check-citations --for-merge` counts **19** such citations.
## 5. CI — it produced its first answer on 2026-09-05
Before that: **23 runs cancelled, 2 waiting, zero successes.** The workflow
described GitHub's hosted fleet (`windows-latest`, `macos-latest`, and a
`--target x86_64` cross-compile) on a one-runner aarch64 instance, so the run
never reached a terminal state — the checks were *unfinished*, not red.
Now, three jobs, all terminal:
| job | state | |
|---|---|---|
| **Native — linux** | **green**, three runs running | `check` `build` `test` `clippy` all pass on aarch64; **207 passed, 0 failed, 14 ignored, 30 suites** |
| WASM — Web | red | **#11** |
| Formatting | red | **#12** — 774 hunks, identical on `main` |
The workspace being portable to ARM was unknown before this and is now
established. The disk exhaustion that broke the test link is retired: 46 GB
reclaimed, `/` at 55%.
⚠️ **`/var/lib/docker` is still on the Pi's 117 GB SD card while a 916 GB SSD sits
at 16%.** This will recur. The fix is `data-root` in `daemon.json` plus a Docker
restart, and it wants a moment when every container going down is fine.
## 6. 🔴 What is in flight and NOT pushed
**The WASM work.** The Pi agent has `ba6c5da`, bundle at
`/tmp/sylph-wasm-compile.bundle` **on the Pi**, base `d8807c4`. It makes
`Check WASM compile` exit 0. #11 turned out to be **three stacked blockers**,
each invisible until the previous was gone:
1. `getrandom` needs `--cfg getrandom_backend="wasm_js"` **and** the feature —
its own error says either alone is insufficient;
2. `sylpheed-formats` declared `tokio` as a **normal** dependency it never used,
dragging `tokio/full``net``mio`, which does not build for wasm32;
3. `bevy_egui` needs `--cfg web_sys_unstable_apis`.
**Two things the human must decide before it lands:**
* **#11 is `state/proposed` and this goes past its stated shape.** It was written
around the getrandom error alone. The tokio removal is a change to another
crate, not CI config — look at that one specifically.
* **It will not turn the job green.** It unblocks two steps that have never run:
`jetli/trunk-action@v0.5.0` and `trunk build --release`. The action's bundled
`dist/index.js` names `x86_64-unknown-linux-gnu` once and `aarch64` **never**,
so it will fetch an x86_64 binary onto the aarch64 runner. Upstream *does*
publish `trunk-aarch64-unknown-linux-gnu.tar.gz` — so this is "the action
cannot find it", not "trunk is unavailable on arm". Replacing the install step
means picking a version and a method: a decision, not a slip-in.
## 7. The method lessons — the most transferable part
One failure shape recurred **five times in two days**, across two agents and the
human's assistant. Every instance is: **a property inferred from something
adjacent to it, rather than tested directly.**
| what was inferred | from what | how it failed |
|---|---|---|
| `main` is protected | the settings page | merging ignores the push whitelist; both agents could have approved each other |
| the desktop can't reach Gitea | `curl` being refused | that was a *permission prompt*, not the network |
| the tool creates 12 labels | `grep -c '^mklabel'` | one match was the function **definition** |
| Gitea is not on the Pi | a DNS lookup | it is, published through a VPS |
| CI's clippy == mine | **identical rustfmt output** | rustfmt is output-stable by design; clippy moves lints between groups |
The last one is the sharpest. `rustfmt 1.8.0` and `1.9.0`, nine months apart,
both produce **774** hunks on this tree — so formatting parity carries *no*
information about which clippy ran. From it I concluded CI's green must be cached
or ungated ("the frozen splash again"). It was neither: `collapsible_else_if` is
`warn` on 1.92.0 and **`allow`** on 1.98.1, which is what the runner has.
> **The check that worked, every time, was counting the same thing with the same
> tool against a baseline.**
Three rules now in `PROTOCOL.md`, each earned:
* **A finding reaches `main` before the code that cites it.**
* **A check may only soften against a condition it can test** — *can this branch
tell the difference between "not yet" and "no longer"?*
* **If you are writing the softening in the same commit as the check, the thing
you want is an issue, not a flag.**
And the ordering constraint that is not obvious: **the cheap-looking fix for #12
is the expensive one.** A whole-tree reformat before #7 and #8 return would put a
conflict in every file of 861 commits and make the reviews those items exist to
enable unreadable. Measured: 133 files carrying 81% of the debt cannot collide;
the real blocker is **21 files**. Land #7/#8 first, then sweep once.
## 8. Resuming — concrete first steps
```bash
git fetch origin
git switch fix/clippy-lints # PR #14's head, d8807c4f
python3 tools/gitea-protect --verify # expect: protection holds. rc=0
```
Then, in order of what is actually blocking:
1. **Merge #10 and #14** — human, with the admin override. Everything else is
downstream: the loop agents clone `main`, which has none of this.
2. **Decide #11's scope**, then fetch `/tmp/sylph-wasm-compile.bundle` from the
Pi and push it.
3. **Decide the trunk-action replacement** (§6).
4. **Then Phase 7** of [`GITEA-SETUP.md`](GITEA-SETUP.md): start the **decoder
alone**, watch one full iteration — notifications polled, a PR rather than a
bare push, no merge button — before starting the port.
### Credentials, all on the desktop, all `chmod 600`
```
~/.sylph-git-credentials write:repository — the push credential
~/.sylph-gitea-token-decoder the decoder agent's, four scopes
~/.sylph-gitea-token-port the port agent's, four scopes
~/.sylph-claude-token long-lived Claude auth for the containers
```
`~/.sylph-gitea-api-token` (`write:issue`) lives **on the Pi**, because that is
where `tools/gitea-setup` runs. Never paste any of them into chat.
⚠️ **This desktop was updated to `rustc 1.98.1` on 2026-09-06** to match the
runner. Anything verified here *before* that ran on 1.92.0 — the fmt counts (774)
and test counts (207/0/14) matched CI exactly and therefore carry; a clippy
result from before does not.
## 9. Standing constraints
* **Never commit game content**, under any directory name. 545 MB reached a
branch on 2026-09-04 under a name the ignore list did not happen to mention.
`.gitignore` now describes the *shape* (`/export*/`, media by extension).
* **Agents never merge**, never push to `main`, never rewrite history. One
force-push was authorised, once, to fix commit authorship before merge — a
recorded exception, not a precedent.
* **The Explorer shows static data only** — the ISO, the embedded PE, savegames.
Never anything generated by a Sylpheed run.
* **Never self-screenshot**; ask the human to capture. Ask before a
watch-and-verify `--ui` launch.
* **Never judge emulator crash or stability from a Bash-launched run.**