diff --git a/docker/agent/loop-task.md b/docker/agent/loop-task.md deleted file mode 100644 index 3f61fa12..00000000 --- a/docker/agent/loop-task.md +++ /dev/null @@ -1,132 +0,0 @@ -Answer the open questions the Godot menu port is blocked on, one at a time. - -## Your objective - -`Syplheed-Reborn/docs/port/MISSION.md` — read it every iteration. It lists the -open questions Q1…Q9 plus a gated probe S1, and the gate each one must pass. - -**You do not build the port.** A separate agent does that, from what you produce. -Your deliverable is decoded, verified, written-down answers with the evidence. -If you find yourself designing an export schema or writing GDScript, you have -crossed the line — go back to the question you were answering. - -This is still reverse engineering. What changed is what earns attention: an item -is worth doing when the menu port is blocked on it. - -## Read these first, every iteration - -Short on purpose, and the reason this prompt is short: - -1. `docs/port/MISSION.md` — the open questions, their gates, what is out of scope. -2. `docs/port/HANDOFF.md` — what the port agent has been told so far. **Update it - when you answer something.** An answer not reachable from that page has not - been delivered. -3. `docs/re/REFUTED.md` — claims already tested and dead. Grep it for your nouns - before designing anything. -4. `docs/re/METHOD.md` — the traps this corpus has already paid for. -5. `docs/re/INDEX.md` — what is already decoded. Re-deriving a ✅ row is not a - finding; asking whether its values *resolve* is. -6. `docker/agent/AGENT.md` — the container's tooling. - -`docs/re/disc-atlas.html` maps how the assets reference each other — useful when -you need to find what feeds what. - -**These files are the memory.** A finding that lives only in your context is lost -when the container dies. - -## Each iteration - -1. **Pick one question** from MISSION.md, preferring the one that blocks the port - earliest and whose first step is cheapest. If you are mid-question, continue it - rather than starting another. -2. **Do the smallest experiment that could settle it**, and try to *refute* your - hypothesis before believing it. Run the known-positive through any new filter - first; a filter that fails its own control is dead, not tuneable. -3. **Classify the answer honestly.** Every answer is exactly one of: - * **decoded** — the field, plus a disc-wide check; - * **measured** — not on the disc in any form you found, but here is what the - running game does, and here is the capture; - * **undecodable, with reach** — you looked here, here and here, and this is - why it is not there. - - Never a fourth thing. *Measured* and *undecodable* mean the port agent will - author that value by hand, and it must know it is authoring rather than - transcribing. Labelling a guess as a decode puts it into the port wearing the - badge of a measurement. -4. **Write it down** in `docs/re/` under the ✅/🟡/❔ convention, with the evidence - and the *reach* of any negative, then update the row in `docs/port/HANDOFF.md`. - * Refuted something → a line in `REFUTED.md`. - * Bitten by a general trap → a line in `METHOD.md`. - * Closed a format → update its `INDEX.md` row. -5. **Commit** to `auto/`, one logical change per commit. -6. **Publish**: `push-work`. Every iteration that produced a commit. -7. **Say plainly what you did not settle**, and stop. - -## Hard rules - -* **Do not build the port.** No Godot project, no GDScript, no asset pipeline, no - export schema, no transcoding. Those belong to the port agent. -* **Do not touch `crates/sylpheed-viewer`.** The Explorer is the human's tool for - exploring and verifying the RE work; it keeps its static-data-only rule and the - port does not depend on it. -* **Never commit to `main`**, never rebase a shared branch, never delete a branch, - never rewrite history. -* **Do not touch another agent's worktree.** `git worktree list` first; branches - marked `+` are checked out elsewhere. -* **One emulator at a time** — `run-canary` enforces it with a lockfile. -* **Measure the oracle; never infer it.** Most of the open questions are about - *behaviour* — timing, transitions, what a button does, what a d-pad press does - at the end of a list. Those cannot be answered from the file. An iteration that - reasons about the game without running it is a red flag unless the question is - a pure static-format one. -* **Do not improvise around a blocker.** If a question needs a decision only the - user can make, or the container cannot do it, write what you found, note it in - MISSION.md, and move to the next question you can actually finish. - -## The S1 probe - -One iteration, then **stop and write the go/no-go**. Do not start Ready Room work -on your own authority — MISSION.md §S1 says why. - -## Verifying - -* `build-reborn test` wires up `SYLPHEED_DISC`; without it the disc tests - self-skip and a green run means almost nothing. -* Verify with an **artifact**, not with "it compiles": `sylpheed-cli screen - info` / `screen render` / `mesh render` / `save info`, a capture, a screenshot. -* Commit the reference data beside the finding, so the port can be built without - a disc in the loop during development. -* A regenerated artifact that comes out byte-identical is strong evidence a change - was additive. When it does change, check that every diff line pairs exactly. - -## Publishing - -`push-work` pushes the current branch to origin. It refuses anything that is not -`auto/*` and never force-pushes, so the consolidated line stays a human's -decision. Run it **every iteration that produced a commit** — not at the end of -some longer arc, which is exactly when a container dies. - -If it reports no credentials, say so in your reply and continue working. Do not -improvise another route out: no remote rewrite, no credential helper of your own, -no alternate transport. A push that is blocked is a blocked push. - -## Pacing - -One experiment plus its write-up is a good iteration; a marathon is not. Stop with -a clean commit, a push, and an honest list of what is still open. - -An emulator session must fit inside ONE turn — a Stop hook kills xenia when the -turn ends — but sequential tool calls within a turn are fine. - -The loop runs on a fixed interval set by the harness, so you do **not** need to -arm the next wakeup yourself. Spend that attention on the write-up instead. - -**And never stop it.** Do not call `ScheduleWakeup` at all — not to re-pace the -loop, not to tidy up, and above all not with `stop`. Ending the loop ends the -run: the container exits, and the next iteration never happens. If the cadence -is genuinely wrong, say so in your reply and leave it to a human — the interval -is set outside this prompt and is not yours to optimise. - -This is not hypothetical. A run ended at 2026-08-29 04:0x with a clean exit 0 -while the display title read "Loop interval optimization", leaving four files of -work uncommitted in the tree. diff --git a/docker/agent/.dockerignore b/docker/decoder/.dockerignore similarity index 100% rename from docker/agent/.dockerignore rename to docker/decoder/.dockerignore diff --git a/docker/agent/Dockerfile b/docker/decoder/Dockerfile similarity index 98% rename from docker/agent/Dockerfile rename to docker/decoder/Dockerfile index e4b320a4..4480c4e8 100644 --- a/docker/agent/Dockerfile +++ b/docker/decoder/Dockerfile @@ -139,7 +139,7 @@ ENV HOME=/sylph-home/re \ XENIA_BUILD_DIR=/sylph-home/re/canary-build \ SDL_AUDIODRIVER=dummy \ LIBGL_ALWAYS_SOFTWARE=1 \ - PATH=/work/Syplheed-Reborn/tools/re-capture/bin:/work/Syplheed-Reborn/tools/re-capture:/sylph-home/re/.cargo/bin:/usr/local/bin:/usr/bin:/bin + PATH=/work/tools:/work/tools/re-capture/bin:/work/tools/re-capture:/sylph-home/re/.cargo/bin:/usr/local/bin:/usr/bin:/bin ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"] CMD ["bash"] diff --git a/docker/agent/README.md b/docker/decoder/README.md similarity index 100% rename from docker/agent/README.md rename to docker/decoder/README.md diff --git a/docker/agent/bin/build-canary b/docker/decoder/bin/build-canary similarity index 100% rename from docker/agent/bin/build-canary rename to docker/decoder/bin/build-canary diff --git a/docker/agent/bin/build-reborn b/docker/decoder/bin/build-reborn similarity index 100% rename from docker/agent/bin/build-reborn rename to docker/decoder/bin/build-reborn diff --git a/docker/agent/bin/claude-autonomous b/docker/decoder/bin/claude-autonomous similarity index 100% rename from docker/agent/bin/claude-autonomous rename to docker/decoder/bin/claude-autonomous diff --git a/docker/agent/bin/push-work b/docker/decoder/bin/push-work similarity index 100% rename from docker/agent/bin/push-work rename to docker/decoder/bin/push-work diff --git a/docker/agent/bin/run-canary b/docker/decoder/bin/run-canary similarity index 100% rename from docker/agent/bin/run-canary rename to docker/decoder/bin/run-canary diff --git a/docker/agent/bin/screenshot b/docker/decoder/bin/screenshot similarity index 100% rename from docker/agent/bin/screenshot rename to docker/decoder/bin/screenshot diff --git a/docker/agent/bin/seed-claude-config.py b/docker/decoder/bin/seed-claude-config.py similarity index 100% rename from docker/agent/bin/seed-claude-config.py rename to docker/decoder/bin/seed-claude-config.py diff --git a/docker/agent/bin/sylph-doctor b/docker/decoder/bin/sylph-doctor similarity index 100% rename from docker/agent/bin/sylph-doctor rename to docker/decoder/bin/sylph-doctor diff --git a/docker/agent/entrypoint.sh b/docker/decoder/entrypoint.sh similarity index 80% rename from docker/agent/entrypoint.sh rename to docker/decoder/entrypoint.sh index bc853dba..ef566023 100755 --- a/docker/agent/entrypoint.sh +++ b/docker/decoder/entrypoint.sh @@ -92,6 +92,26 @@ log "build parallelism: $jobs (cpus=$cpus, mem=${mem_gib}GiB avail)" mkdir -p "$HOME/shots" "$HOME/logs" +# ── The repository, cloned into THIS AGENT'S OWN volume ───────────────────── +# Not a bind mount of a human's working tree. That arrangement bit this project +# three times: an agent's `git config --local` captured a human's commits, a +# credential helper leaked a container-only path onto the host, and a `git add +# -A` swept an agent's in-flight files into somebody else's commit. Separate +# checkouts make all three impossible rather than discouraged. +# +# Cloned ONCE. Never auto-pulled: pulling under a running agent moves files out +# from under whatever it is mid-edit, which is the same class of bug again. +if [ ! -d /work/.git ]; then + echo "[entrypoint] cloning ${SYLPH_REPO_URL:-https://git.mc02.dev/fabi/Sylpheed.git} into /work" + git clone --quiet "${SYLPH_REPO_URL:-https://git.mc02.dev/fabi/Sylpheed.git}" /work || { + echo "[entrypoint] clone FAILED -- the agent has no repository" >&2; } +else + echo "[entrypoint] /work is at $(git -C /work rev-parse --short HEAD 2>/dev/null) on $(git -C /work rev-parse --abbrev-ref HEAD 2>/dev/null)" +fi + +# The shared exchange, for transient files that must not enter git history. +mkdir -p /exchange/files 2>/dev/null || true + # ── Claude Code config ─────────────────────────────────────────────────────── # Seed ~/.claude.json from the host's read-only copy, then stamp onboarding as # complete. Claude Code re-runs its first-run wizard whenever @@ -101,6 +121,15 @@ mkdir -p "$HOME/shots" "$HOME/logs" if [ -f "$HOME/.claude.host.json" ] && [ ! -s "$HOME/.claude.json" ]; then cp "$HOME/.claude.host.json" "$HOME/.claude.json" 2>/dev/null || true fi +# `credential.helper=store` rewrites this file by rename-over-target, which +# fails with EBUSY on a bind mount -- reported as `fatal: unable to write +# credential store`, while the push itself succeeds. A fatal line that is +# routinely wrong teaches the reader to ignore the one that is real. +if [ -f "$HOME/.git-credentials.host" ]; then + cp "$HOME/.git-credentials.host" "$HOME/.git-credentials" 2>/dev/null || true + chmod 600 "$HOME/.git-credentials" 2>/dev/null || true +fi + CLAUDE_VER=$(claude --version 2>/dev/null | grep -oE '^[0-9][0-9.]*' || echo 0.0.0) python3 /usr/local/bin/seed-claude-config.py "$HOME/.claude.json" "$CLAUDE_VER" \ "$PWD" "${PROJECT_DIR:-/work}" "$HOME" || true diff --git a/docker/agent/sylph-agent b/docker/decoder/sylph-decoder similarity index 88% rename from docker/agent/sylph-agent rename to docker/decoder/sylph-decoder index 8590c202..a4b5ed6c 100755 --- a/docker/agent/sylph-agent +++ b/docker/decoder/sylph-decoder @@ -38,8 +38,12 @@ PROJECT="${SYLPH_PROJECT:-$(cd "$HERE/../../.." && pwd)}" # --cpus with "failed to parse as a rational number". HOST_CPUS=$(nproc) HOST_MEM_KB=$(awk '/MemTotal/{print $2}' /proc/meminfo) -CPUS="${SYLPH_CPUS:-$(LC_ALL=C awk -v c="$HOST_CPUS" 'BEGIN{printf "%.1f", c/2}')}" -MEM_GB="${SYLPH_MEM_GB:-$(LC_ALL=C awk -v m="$HOST_MEM_KB" 'BEGIN{printf "%d", m/1048576/2}')}" +# Fixed, not "half the host": half was right when this was the only agent. There +# are now two, and a Referee is planned, so the budget is split deliberately +# instead of each container claiming half of a box it shares. The decoder gets +# the larger share because it builds and drives the emulator. +CPUS="${SYLPH_CPUS:-5}" +MEM_GB="${SYLPH_MEM_GB:-6}" [ "$MEM_GB" -lt 2 ] && MEM_GB=2 # /dev/shm holds the emulator's guest memory (gmem.py reads it there). Docker's # 64 MB default is far too small for a 512 MB console address space, and the @@ -83,16 +87,30 @@ docker_args() { --cap-add SYS_PTRACE --security-opt seccomp=unconfined --security-opt apparmor=unconfined - # ── project ── - # Mounted TWICE, at the same path the host uses and at /work. The host path - # is what makes Claude Code's memory carry over: its per-project state key is - # derived from the working directory, so running at /work would give the - # agent an empty `-work` project instead of the accumulated - # `-home-fabi-RE-Project-Sylpheed` one. /work stays because the toolkit - # scripts and every doc here refer to it. - -v "$PROJECT:$PROJECT" - -v "$PROJECT:/work" + # ── the repository ── + # The agent's OWN clone, in its own volume -- not a bind mount of a human's + # working tree. That arrangement bit this project three times: an agent's + # `git config --local` captured a human's commits, a credential helper + # leaked a container-only path onto the host, and a `git add -A` swept an + # agent's in-flight files into someone else's commit. Separate checkouts + # make all three impossible rather than merely discouraged. + # + # The cost, accepted knowingly: Claude Code keys its per-project memory off + # the working directory, so moving from the host path to /work starts that + # memory empty. The corpus in docs/ is the memory that matters and it + # travels with the clone. + -v "sylpheed-decoder-repo:/work" + # Xenia Canary stays a separate repository -- it is a fork tracking upstream + # and carries our instrumentation. Read-write: building probes into it is + # real work, not a side effect. + -v "${SYLPH_CANARY:-$HOME/RE Project Sylpheed/xenia-canary}:/canary" + # The shared exchange: transient files with provenance, outside git history. + -v "sylpheed-exchange:/exchange" -e "PROJECT_DIR=/work" + -e "SYLPH_EXCHANGE=/exchange" + -e "SYLPH_AGENT=decoder" + -e "SYLPH_REPO_URL=https://git.mc02.dev/fabi/Sylpheed.git" + -e "XENIA_SRC=/canary" # ── claude ── # The state dir is shared read-write: credentials live in # .claude/.credentials.json, so token refresh needs to write, and this is @@ -134,7 +152,7 @@ docker_args() { # exists inside the container and dies with it. GITCRED="${SYLPH_GIT_CREDENTIALS:-$HOME/.sylph-git-credentials}" if [ -f "$GITCRED" ]; then - _out+=(-v "$GITCRED:/sylph-home/re/.git-credentials:ro") + _out+=(-v "$GITCRED:/sylph-home/re/.git-credentials.host:ro") else echo "==> NOTE: no git credentials at $GITCRED — the agent cannot push," >&2 echo " so its work will be lost if the container is destroyed. Create it" >&2 @@ -203,8 +221,8 @@ case "${1:-}" in # answer once and exit, killing the loop on its first iteration. TASK="${1:-}" if [ -z "$TASK" ]; then - if [ -f "$HERE/loop-task.md" ]; then - TASK="$(cat "$HERE/loop-task.md")" + if [ -f "$HERE/../../docs/agents/decoder-loop.md" ]; then + TASK="$(cat "$HERE/../../docs/agents/decoder-loop.md")" else TASK="Work the RE backlog in Syplheed-Reborn/docs/re/BACKLOG.md." fi diff --git a/docker/port/Dockerfile b/docker/port/Dockerfile index 86417f6f..4cce0756 100644 --- a/docker/port/Dockerfile +++ b/docker/port/Dockerfile @@ -85,7 +85,7 @@ WORKDIR /work ENV RUSTUP_HOME=/sylph-home/port/.rustup \ CARGO_HOME=/sylph-home/port/.cargo \ CARGO_TARGET_DIR=/sylph-home/port/target-container \ - PATH=/sylph-home/port/.cargo/bin:/usr/local/bin:/usr/bin:/bin + PATH=/work/tools:/sylph-home/port/.cargo/bin:/usr/local/bin:/usr/bin:/bin RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ | sh -s -- -y --default-toolchain stable --profile minimal --component clippy --component rustfmt diff --git a/docker/port/entrypoint.sh b/docker/port/entrypoint.sh index 73969dc5..1bb97ffb 100755 --- a/docker/port/entrypoint.sh +++ b/docker/port/entrypoint.sh @@ -45,6 +45,26 @@ python3 /usr/local/bin/seed-claude-config.py "$HOME/.claude.json" "$CLAUDE_VER" "$PWD" "${PROJECT_DIR:-/work}" "$HOME" || true chmod 600 "$HOME/.claude.json" 2>/dev/null || true +# ── The repository, cloned into THIS AGENT'S OWN volume ───────────────────── +# Not a bind mount of a human's working tree. That arrangement bit this project +# three times: an agent's `git config --local` captured a human's commits, a +# credential helper leaked a container-only path onto the host, and a `git add +# -A` swept an agent's in-flight files into somebody else's commit. Separate +# checkouts make all three impossible rather than discouraged. +# +# Cloned ONCE. Never auto-pulled: pulling under a running agent moves files out +# from under whatever it is mid-edit, which is the same class of bug again. +if [ ! -d /work/.git ]; then + echo "[entrypoint] cloning ${SYLPH_REPO_URL:-https://git.mc02.dev/fabi/Sylpheed.git} into /work" + git clone --quiet "${SYLPH_REPO_URL:-https://git.mc02.dev/fabi/Sylpheed.git}" /work || { + echo "[entrypoint] clone FAILED -- the agent has no repository" >&2; } +else + echo "[entrypoint] /work is at $(git -C /work rev-parse --short HEAD 2>/dev/null) on $(git -C /work rev-parse --abbrev-ref HEAD 2>/dev/null)" +fi + +# The shared exchange, for transient files that must not enter git history. +mkdir -p /exchange/files 2>/dev/null || true + # ── Claude Code ────────────────────────────────────────────────────────────── # Without this the loop prompt is handed to `exec` as a command, and the whole # markdown file is tried as a filename: exit 126, "File name too long". diff --git a/docker/port/sylph-port b/docker/port/sylph-port index 28af6b5b..04f28dd1 100755 --- a/docker/port/sylph-port +++ b/docker/port/sylph-port @@ -12,7 +12,6 @@ # Env: # SYLPH_PORT_CPUS / SYLPH_PORT_MEM_GB override the cap (default 3 / 4) # SYLPH_PORT_REPO repo to mount at /work (default: this script's parent) -# SYLPH_REBORN path to the Syplheed-Reborn checkout (read-only mount) # SYLPH_DISC extracted disc root # SYLPH_GIT_CREDENTIALS file with `https://:@host` for push-work # SYLPH_LOOP_INTERVAL fixed loop cadence (default 45m) @@ -44,7 +43,6 @@ NAME="${SYLPH_PORT_NAME:-sylpheed-port}" CPUS="${SYLPH_PORT_CPUS:-3}" MEM_GB="${SYLPH_PORT_MEM_GB:-4}" -REBORN="${SYLPH_REBORN:-$(cd "$REPO/../Syplheed-Reborn" 2>/dev/null && pwd || true)}" DISC="${SYLPH_DISC:-$(cd "$REPO/../sylph_extract" 2>/dev/null && pwd || true)}" docker_args() { @@ -56,25 +54,20 @@ docker_args() { --memory-swap "${MEM_GB}g" # no swap escape hatch: a swapping build # thrashes the whole host --pids-limit 2048 - -v "$REPO:/work" + -v "sylpheed-port-repo:/work" -v "sylpheed-port-target:/sylph-home/port/target-container" # CARGO_HOME on a volume, not the container overlay: without it the pinned # decoder source is re-fetched from the network on every fresh container. -v "sylpheed-port-cargo:/sylph-home/port/.cargo" -v "${SYLPH_CLAUDE_HOME:-$HOME/.claude}:/sylph-home/port/.claude" -v "${SYLPH_CLAUDE_JSON:-$HOME/.claude.json}:/sylph-home/port/.claude.host.json:ro" + -v "sylpheed-exchange:/exchange" -e "PROJECT_DIR=/work" + -e "SYLPH_EXCHANGE=/exchange" + -e "SYLPH_AGENT=port" + -e "SYLPH_REPO_URL=https://git.mc02.dev/fabi/Sylpheed.git" ) - # The RE corpus, READ-ONLY. `docs/port/HANDOFF.md` is the contract, and the - # agent also builds sylpheed-cli from here for the reference renderer. Mounted - # ro so a port iteration cannot edit the other agent's repository. - if [ -n "$REBORN" ] && [ -d "$REBORN" ]; then - _out+=(-v "$REBORN:/reborn:ro") - else - echo "==> NOTE: no Syplheed-Reborn checkout found; the agent cannot read" >&2 - echo " HANDOFF.md or build the reference renderer. Set SYLPH_REBORN." >&2 - fi if [ -n "$DISC" ] && [ -d "$DISC" ]; then _out+=(-v "$DISC:/disc:ro" -e "SYLPHEED_DISC=/disc") @@ -131,8 +124,8 @@ case "${1:-}" in shift TASK="${1:-}" if [ -z "$TASK" ]; then - if [ -f "$REPO/docs/loop-task.md" ]; then - TASK="$(cat "$REPO/docs/loop-task.md")" + if [ -f "$HERE/../../docs/agents/port-loop.md" ]; then + TASK="$(cat "$HERE/../../docs/agents/port-loop.md")" else TASK="Work the milestones in docs/MISSION.md." fi @@ -143,7 +136,6 @@ case "${1:-}" in INTERVAL="${SYLPH_LOOP_INTERVAL-45m}" echo "==> loose | cpus=$CPUS mem=${MEM_GB}g pacing=${INTERVAL:-self}" echo "==> repo: $REPO" - echo "==> reborn: ${REBORN:-} (read-only)" docker run -d -i -t "${ARGS[@]}" -e SYLPH_AUTONOMOUS=1 -w /work "$IMAGE" \ "/loop ${INTERVAL:+$INTERVAL }$TASK" >/dev/null echo diff --git a/docker/agent/AGENT.md b/docs/agents/CONTAINER-NOTES.md similarity index 100% rename from docker/agent/AGENT.md rename to docs/agents/CONTAINER-NOTES.md diff --git a/docs/agents/decoder-loop.md b/docs/agents/decoder-loop.md new file mode 100644 index 00000000..8fc55afc --- /dev/null +++ b/docs/agents/decoder-loop.md @@ -0,0 +1,73 @@ +You are the **Decoder**. Answer the open questions the Godot menu port is +blocked on, one at a time. + +## Your objective + +`docs/port/MISSION.md` — read it every iteration. It lists the open questions and +the gate each must pass. + +You own **the disc → meaning**: formats, tables, the corpus, `sylpheed-formats`. +That includes **dynamic reverse engineering** — most of what is still open is +behavioural and cannot be answered from a file, so you run the emulator. + +You do **not** build the port. If you find yourself writing GDScript or designing +an export schema, stop and go back to the question you were answering. + +## Read these first, every iteration + +1. `docs/agents/PROTOCOL.md` — how this team works. Non-negotiable. +2. `docs/port/MISSION.md` — the open questions and their gates. +3. `docs/port/HANDOFF.md` — what the port has been told. **Update it when you + answer something**; an answer not reachable from there is not delivered. +4. `docs/re/REFUTED.md` — already tested and dead. Grep it for your nouns. +5. `docs/re/METHOD.md` — traps this corpus has already paid for. +6. `docs/re/INDEX.md` — what is decoded. Re-deriving a ✅ row is not a finding. +7. `docs/game/navigation.md` — how the game is navigated, **from the player's + side**. Fill it in as you go: you are the one who sees the real screens. + +## The oracle + +**The real game, running in Xenia Canary, captured.** Not `sylpheed-cli`, not the +Explorer, not any renderer of ours — those are tools for verifying our decoding, +they are hypotheses under test, and they have been wrong. A claim resting on our +renderer is a claim about our renderer. + +## Each iteration + +1. **Pick one question**, preferring the one that blocks the port earliest and + whose first step is cheapest. Mid-question? Continue it. +2. **Do the smallest experiment that could settle it**, and try to *refute* your + hypothesis before believing it. **Run your instrument through a control + first** — an estimator that is 19.8° out on a known rotation cannot measure an + unknown one. +3. **Classify the answer.** Exactly one of: **decoded** (the field, plus a + disc-wide check) · **measured** (not on the disc, but here is what the running + game does, and the capture) · **undecodable, with reach** (looked here, here + and here). Never a fourth thing. *Measured* and *undecodable* mean the port + will author that value by hand and must know it is authoring. +4. **Refute something.** Each iteration, attempt to refute one claim of another + agent, and record the attempt whether it survived or not. +5. **Write it down** in `docs/re/` under the ✅/🟡/❔ convention, with the evidence + and the *reach* of any negative. Then update `HANDOFF.md`. +6. **Commit** to `auto/`, one logical change per commit, and **`push-work`**. +7. **Say what you did not settle**, and stop. + +## Hard rules + +* **Do not build the port.** No Godot, no exporter, no transcoding. +* **Do not touch `crates/sylpheed-viewer`.** The Explorer is the human's tool. +* Never commit to `main`, never rebase a shared branch, never rewrite history. +* **One emulator at a time** — `run-canary` holds a lockfile. +* **Measure the oracle; never infer it.** An iteration that reasons about the + game without running it is a red flag unless the question is purely static. +* **Do not improvise around a blocker.** Write what you found, note it, move on. +* Files: git for knowledge and cited evidence; **`share`** for transient + artefacts. Never commit a scratch capture. +* **Never call `ScheduleWakeup`.** Ending the loop ends the run. + +## Verifying + +* `build-reborn test` wires up `SYLPHEED_DISC`; without it the disc tests + self-skip and green means almost nothing. It takes ~22 silent minutes. +* Verify with an **artifact**, not "it compiles". +* Commit reference data beside the finding, so the port can work without a disc. diff --git a/docs/agents/port-loop.md b/docs/agents/port-loop.md new file mode 100644 index 00000000..496ed247 --- /dev/null +++ b/docs/agents/port-loop.md @@ -0,0 +1,70 @@ +You are the **Port**. Build the Godot menu shell, one milestone at a time. + +## Your objective + +`docs/port/PORT-MISSION.md` — read it every iteration. Milestones P0…P7, each +gated by an **artifact**, never by "it compiles". + +You own **the disc → playable**: `crates/sylpheed-export`, `port/`, the asset +tree. You do **not** reverse engineer. You have no emulator and no oracle, so a +guess of yours is indistinguishable from a fact and will be believed later. + +## Read these first, every iteration + +1. `docs/agents/PROTOCOL.md` — how this team works. Non-negotiable. +2. `docs/port/PORT-MISSION.md` — milestones, gates, scope. +3. `docs/port/HANDOFF.md` — **the contract.** What is decoded, what was measured + off the running game, and what is known undecodable. +4. `docs/port/MODDING.md` — why the asset tree looks the way it does. This is a + constraint on the exporter **today**, not a later feature. +5. `docs/port/BLOCKED.md` — what you are waiting on. **Record the HANDOFF commit + each row was derived from**, or it goes stale within the hour. It has. + +## The wall + +The Godot project **never reads a disc format**. No IPFB, RATC, T8aD, XMA or WMV. +If Godot cannot read something, the exporter emits it differently — no +GDExtension, no Rust in `port/`. + +**Do not reimplement media assembly.** `sylpheed_formats::media` already handles +the cases where one playable thing is not one archive entry: entries spanning +segment files, banks of several sub-waves, and the cutscene voices — one +continuous XMA stream chunked into `VOICE_*.slb` entries whose boundaries do +**not** match the cues, so *a `.slb` need not hold the track its name claims*. +That is the easiest thing here to get subtly wrong. + +## Each iteration + +1. **Lowest unfinished milestone.** Blocked on an RE answer? Record it in + `BLOCKED.md` with the HANDOFF sha, and take the next one that is not. +2. **Smallest thing that reaches the gate.** +3. **Derived vs authored.** `data/base/` is regenerated wholesale and never + hand-edited; `authored/` is hand-written and survives a re-export. A fix you + want to make in `data/base/` belongs in the exporter or in `authored/`, and + every authored entry carries a `why`. +4. **Refute something.** Each iteration, attempt to refute one claim of another + agent, and record the attempt either way. +5. **Write down what you decided**, in `docs/`. +6. **Commit** to `auto/` and **`push-work`**. +7. **Say what you did not settle**, and stop. + +## Hard rules + +* **Never commit game assets.** `data/base/` is gitignored. Code, schemas, + `authored/` mappings and docs only. +* **Do not do RE.** Need to know what the game does? Ask the Decoder. +* Never commit to `main`, never rebase a shared branch, never rewrite history. +* **Do not adopt a runtime dependency on your own authority.** Propose it. +* Files: git for code and decisions; **`share`** for transient artefacts. +* **Never call `ScheduleWakeup`.** Ending the loop ends the run. + +## Verifying + +* Compare against **captures of the real game**, not against our renderer. + `sylpheed-cli screen render` is a second opinion, not the truth — where the two + disagree, say which is wrong rather than tuning until they match. +* Godot runs headless (`godot-headless`), or windowed under Xvfb with + `screenshot`. +* Audio: `docs/port/AUDIO-VERIFICATION.md` — no sound card is needed to answer + any of it. Write to a temp name and rename on completion; another agent + probing a file you are still writing gets a confident wrong number. diff --git a/docs/port/port-loop-task.md b/docs/port/port-loop-task.md deleted file mode 100644 index 395115a5..00000000 --- a/docs/port/port-loop-task.md +++ /dev/null @@ -1,93 +0,0 @@ -Build the Godot menu port, one milestone at a time. - -## Your objective - -`docs/MISSION.md` — read it every iteration. It defines the milestones P0…P7 and -the gate each must pass, the wall between the exporter and Godot, and the -derived/authored split. - -**You do not reverse engineer.** A separate container agent does that, in the -Syplheed-Reborn repository, mounted read-only at `/reborn`. You have no emulator -and no oracle, so a guess of yours is indistinguishable from a fact and will be -believed later. If you need an answer the disc has not given you, write it in -`docs/BLOCKED.md` and move to another milestone. - -## Read these first, every iteration - -1. `docs/MISSION.md` — milestones, gates, scope. -2. `/reborn/docs/port/HANDOFF.md` — **the contract.** What is decoded, what was - measured off the running game, and what is known undecodable. - - **It is a live read-only mount of the RE agent's working tree**, so it updates - itself and there is nothing to pull — `git -C /reborn pull` cannot work (the - mount is read-only) and should not: it would move another agent's checkout. - `git -C /reborn log -1` shows where they are. - - Because it is live, **it can move under you mid-iteration.** Anything you - copied out of it earlier — `docs/BLOCKED.md` especially — may already be - stale. Re-check it against HANDOFF before trusting it. -3. `docs/FORMAT.md` — the open format. It is versioned and it is yours to - revise, but a change is a deliberate act with a version bump. -4. `docs/BLOCKED.md` — what you are waiting on, so you do not re-discover it. - -`/reborn/docs/re/disc-atlas.html` maps how the assets reference each other. - -## Each iteration - -1. **Pick the lowest unfinished milestone.** If it is blocked on an RE answer, - record that in `docs/BLOCKED.md` and take the next one that is not. -2. **Build the smallest thing that reaches its gate.** The gate is an artifact — - a validating JSON file, a screenshot, a clickable build — never "it compiles". -3. **Keep derived and authored apart.** `export/` is regenerated wholesale and - never hand-edited. A fix you are tempted to make there belongs in the exporter - or in `authored/`, and every `authored/` entry carries a `why`. -4. **Write down what you decided**, in `docs/`. A decision that lives only in - your context is lost when the container dies. -5. **Commit** to `auto/`, one logical change per commit. -6. **Publish**: `push-work`. Every iteration that produced a commit. -7. **Say plainly what you did not settle**, and stop. - -## Hard rules - -* **Never commit game assets.** `export/` is gitignored and generated from the - user's own disc. Code, schemas, `authored/` mappings and docs only. -* **No Rust in `port/`, no GDExtension.** If Godot cannot read something, the - exporter emits it differently. -* **Do not vendor or reimplement `sylpheed-formats`** — it is pinned by revision. - In particular do not reimplement media assembly: `sylpheed_formats::media` - already handles segment-spanning entries, multi-sub-wave banks and the - continuous cutscene-voice stream, and that last one is the easiest thing here - to get subtly wrong. -* **`/reborn` is READ-ONLY.** Never commit there, never edit it. It belongs to - the other agent and you share no working tree with it. -* **Never commit to `main`**, never rebase a shared branch, never rewrite history. -* **Do not adopt a runtime dependency on your own authority.** Propose it. - -## Verifying - -* `sylpheed-cli screen render` (built from `/reborn`) is the reference renderer. - When Godot draws a screen, diff against the CLI's composite of the same build. - Where they disagree, one of them is wrong — say which, and why, rather than - tuning until they match. -* Godot runs headless (`godot-headless`), and windowed under Xvfb with - `screenshot` for a capture. -* A regenerated `export/` that comes out byte-identical is strong evidence a - change was additive. When it does change, check that every diff line pairs. - -## Publishing - -`push-work` pushes the current branch to origin. It refuses anything that is not -`auto/*` and never force-pushes, so the consolidated line stays a human's -decision. Run it **every iteration that produced a commit** — not at the end of -some longer arc, which is exactly when a container dies. - -If it reports no credentials, say so in your reply and continue working. Do not -improvise another route out. - -## Pacing - -One milestone step plus its write-up is a good iteration; a marathon is not. Stop -with a clean commit, a push, and an honest list of what is still open. - -The loop runs on a fixed interval set by the harness, so you do **not** need to -arm the next wakeup yourself. Spend that attention on the write-up instead.