Files
Sylpheed/docs/agents/HANDOFF-2026-09-06.md
Fabian Hamm 34f19ae66e docs: the handoff's resume steps assume a machine this is not
§§1-9 were written on fabi-Hyrican-PC. On the other desktop the ~/.sylph-*
credentials do not exist, the Pi does not resolve, and stable is 1.90.0 rather
than the 1.98.1 §8 records -- so §8.2 (fetch the WASM bundle) and §8.4 (Phase 7)
cannot be run from here at all. Says which of the four steps can.

Measured rather than carried over: protection holds (10/10), fmt is 774 hunks
across 154 files, check-citations is 19, and the tests are 207/0/14 across 30
suites. Two of those need reading carefully:

  * clippy DIVERGES. The runner is rustc 1.98.1 -- read out of job 794's log,
    not assumed -- and is clean; here 1.90.0 exits 101 on only_used_in_recursion
    at vfs.rs:85. That is #15 ceasing to be theoretical. It is NOT evidence that
    CI's green is fake, which is the §7 lesson-5 inference in the other
    direction.
  * the test tally matches to the unit while measuring something else. 15
    *_disc.rs files resolve disc_root() through a hardcoded absolute path, so
    unsetting SYLPHEED_DISC does not skip them: the disc suites RAN here (1936 s,
    mesh_consistency_disc alone 1220 s) and skipped on CI (2.4 s total) -- and
    both report 207/0/14, because the skip path returns from a test that still
    passes. Good news for #14, since this run is the stronger evidence; and worth
    an issue, since SYLPHEED_DISC looks like a control and is not one.

I got that last one wrong first -- inferred "the counts cannot match" from "the
fallback resolves", which is §7's shape a sixth time, recorded as such.

Also: a plain `git clone` of this repo fails three ways on the pack that still
carries the 545 MB; --filter=blob:none works. And §6's tokio claim was
challenged and survived -- every use is inside a #[cfg(test)] module.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 22:20:30 +02:00

20 KiB
Raw Blame History

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 gameProject 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.

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 protectedenable_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/fullnetmio, 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 testcan 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

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: 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.

10. The second machine — measured on fabi-MS-7C37, 2026-09-06

§§19 were written on fabi-Hyrican-PC. This section was written on the other desktop, and every line is a command run here. Nothing above is carried across untested — §7 is the reason.

Everything §§45 say about the server checks out exactly: 13 open issues, #10 and #14 open and mergeable, Gitea 1.25.5. What does not transfer is the box.

What is different here, and what it costs

§ says here
8 four ~/.sylph-* credentials, chmod 600 none exist. ~/.git-credentials holds a fabi@git.mc02.dev token that reads branch_protections — an endpoint both agent tokens are refused on — so it is not an agent token. Whether it can push is untested
8.2 fetch /tmp/sylph-wasm-compile.bundle from the Pi raspberrypi.fritz.box does not resolve here, and there is no host key for it. ba6c5da is unreachable from this machine
8 "updated to rustc 1.98.1 … to match the runner" that was the other desktop. Here stable = 1.90.0, with a 1.92.0 also installed
5 the agent images no sylph-decoder / sylph-port image on this box

So of §8's four steps, only 1 (the human's merges) and 3 (the trunk-action decision, an edit to ci.yml) can be done from here. 2 and 4 cannot, at all.

🔴 git clone of this repository does not work

Three attempts died on GnuTLS recv error (-9) / Recv failure: Connection reset by peer, at 135 MB, 4.6 MB and 73 MB. A default clone fetches every branch, and auto/port-p6-audio's history still carries the 545 MB of game content §9 describes — gone from the tree, still in the pack.

What works, in seconds:

git -c http.version=HTTP/1.1 clone --filter=blob:none \
    https://git.mc02.dev/fabi/Sylpheed.git

Blobs fault in on demand. The tree is 108 MB in 1 007 files (98 MB of it docs/re/captures/), and it was verified byte-for-byte against git ls-tree -r -l — worth doing, because the clone printed "checkout failed" partway and then recovered silently. ⚠️ One cost: check-citations reaches into peer branches, so on a blobless clone it faults blobs over that same flaky link and takes minutes rather than seconds.

The numbers that carry, and the one that does not

Expected values taken from §§45 before running, per R2:

expected measured here
gitea-protect --verify protection holds holds, 10/10, rc=0 carries
cargo fmt --all -- --check 774 hunks 774, across 154 files carries
check-citations --for-merge 19 19, rc=1 carries
cargo test --workspace 207 / 0 / 14, 30 suites 207 / 0 / 14, 30 carries — see below
cargo clippy --workspace -- -D warnings not comparable exit 101 🔴 diverges

154 files corroborates rather than adds: §7's split of the fmt debt into "133 that cannot collide" and "21 that are the real blocker" sums to exactly it.

gitea-protect --verify needs SYLPH_GIT_CREDENTIALS=~/.git-credentials here, since its default is one of the four missing files.

🔴 The test count carries — and that is what is wrong with it

It matched to the unit. It should not be read as the two runs having done the same thing, because they did not.

15 files under crates/sylpheed-formats/tests/*_disc.rs resolve the disc through a disc_root() whose second branch is a hardcoded absolute path/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja). So unset SYLPHEED_DISC does not disable them: that directory exists on this box, and the disc suites ran.

CI (job 794) here
test execution wall time 2.4 s, slowest suite 0.29 s 1 936 s, mesh_consistency_disc alone 1 220 s
tally 207 / 0 / 14, 30 suites identical

Identical because the skip path is eprintln!("SKIP: …") plus an early return from a test that still passes. A skipped disc test and a fully exercised one both score 1 passed. And the message is invisible either way — cargo test captures a passing test's stderr, so neither log contains a SKIP: line. The absence of one proves nothing; only the clock separated these two runs.

So ask the question this project keeps having to ask — what would this check still report if the corpus were entirely absent? — and the answer is 207 / 0 / 14.

Two consequences, and the first is good news:

  • this run is strictly stronger evidence than CI's: 207 passed with the disc corpus actually exercised, on x86_64, at 593b378.
  • SYLPHEED_DISC looks like the control and is not one. Whether the disc suites run is a property of the machine's directory layout, invisible in the command and in the output. Worth an issue — it is the .gitignore lesson again, naming an instance instead of the condition.

📌 A sixth instance for §7, and it is mine. I inferred "the counts cannot match" from "the fallback resolves" — an adjacent property, never tested — and wrote it into this section before the run finished. The run returned 207 / 0 / 14. The correction was the same as every other time in that table: run it, and count.

🔴 The clippy gate genuinely disagrees between the two toolchains

This is #15 ceasing to be theoretical, and it needs stating carefully, because it is §7's lesson 5 arriving from the other side.

Both sides measured with the same command, both versions read rather than assumed:

  • the runnerrustc 1.98.1 (48a229cea 2026-09-01), read out of job 794's own log. cargo clippy --workspace -- -D warnings finishes in 9.26 s with no lint. Native is green on runs 206, 207, 208 and 209 — four consecutive, not three.
  • hererustc 1.90.0 / clippy 0.1.90. The same command exits 101, on exactly one lint:
error: parameter is only used in recursion
  --> crates/sylpheed-formats/src/vfs.rs:85:10
  = note: `-D clippy::only-used-in-recursion` implied by `-D warnings`

Without -D warnings it is a warning and clippy exits 0 — so the disagreement sits exactly at the gate.

What this does not mean. It does not mean #14 is wrong, and it does not mean CI's green is cached or ungated. That is the inference §7 records as the sharpest of its five failures, and the evidence points the other way: the runner's log shows the step running, on this code, clean. d8807c4 already collapsed one else { if } "so both toolchains agree" — this is the same class, one lint on.

What it establishes is narrower and more useful: the gate's verdict depends on which stable happened to be current, and there is now a named reproducible case rather than an argument. That is #15's evidence.

📌 For whoever picks up #13/#14: "clippy is clean" is not a property of the tree, it is a property of the tree and a toolchain. Until #15 pins one, say which one you ran.

Refutation attempted, and survived

Per the adversarial duty — §6's claim that sylpheed-formats declares tokio as a normal dependency it never uses. It is referenced, in ship.rs and xiso.rs, which looked like a refutation. It is not: every one of those sits inside a #[cfg(test)] module (ship.rs:497, xiso.rs:178). The library's non-test code does not use tokio, so moving it to dev-dependencies is sound and the examples/ targets keep compiling. Claim survives — recorded because a survived challenge is stronger than an unchallenged one, not because it changed anything.