`SYLPHEED_DB=/xenia-rs/sylpheed.db` named a path that no longer exists. The
xenia-rs repo was retired by the consolidation, its local clone was deleted, and
the `/xenia-rs` mount that served the database was removed in #61 because it
pointed at nothing. The env var stayed. So the decoder had NO database: `zq.py`
and `/sylph-dis` -- most of what a static-RE brief asks for -- could not run.
Mounted read-only at the container's repo root instead, which is where `zq.py`
looks when `$SYLPHEED_DB` is unset, so there is no variable left to drift out of
step with the mount. That drift is the whole bug: a path in an env var and a
path in a mount, maintained separately.
Read-only is deliberate. The host owns the file, DuckDB takes an exclusive lock
to write, and two agents plus the human sharing one database would corrupt it.
Regenerating means writing elsewhere and pointing $SYLPHEED_DB at it.
Missing-file cases now say so and print the command that builds one, rather than
starting an agent that discovers it mid-iteration.
Verified in the real agent image: DB mounted, no env var set,
`zq.py fn 0x824609C8` -> `Pak_FindEntryByName`, `zq.py classes` lists RTTI.
The brief's tooling table also claimed the old path, and said nothing about the
oracle binary; both corrected. It now records that the built Canary carries
RE-INPUT/RE-DRAW but NOT the audit_61 branch probe -- measured with `strings`
on both built binaries, zero hits; it is on two other branches (fork issue #1).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Issue #49 removed game assets from git. It also, silently, removed the only
transport agents had for showing each other evidence: each agent works in its
OWN clone, so "commit the screenshot, the other one pulls it" was the mechanism,
and PROTOCOL.md's file table said so in as many words -- "evidence cited by a
finding -> git". That row sat directly above "🔴 Never commit game content",
which is how 76 MB accumulated: the two contradicted each other and the table
won, because it was the one that told you what to do.
WHAT REPLACES IT
* one host directory, `Sylpheed/docs/re/captures/`, bind-mounted read-write
into BOTH agents at /work/docs/re/captures. All three -- host, decoder,
port -- see the same files live, every citation resolves, and nothing can
reach git history. Read-write on purpose: showing each other a screenshot
is the point.
* PROTOCOL.md's table rewritten. Cited evidence -> present but never
committed; derived measurements (csv/tsv/txt/log/json) -> still git, they
are our numbers not game content; evidence that must cross MACHINES ->
attached to the issue or PR, because a bare clone has no captures.
Verified, not assumed: container A wrote a .png there, a SEPARATE container B
read it back, the host saw it, `git status` reported 0 changes, and
`git check-ignore` named the rule.
THE CHECKER WAS RED ON EVERY CLEAN CHECKOUT
A fresh clone/worktree/CI has no captures, so it called all 134 citations
dangling and exited 1. A gate that is red before anyone changes anything is one
people learn to ignore -- the exact failure this file already carries a comment
about. It now distinguishes "no captures here" (expected, explains itself,
exit 0) from "these are missing" (real, exit 1, unchanged when assets ARE
present). Both paths tested.
ALSO
* `sylph-decoder` no longer mounts `xenia-rs` -- retired repo, gone from disk,
the mount pointed at nothing.
* CONSOLIDATION.md closed: it still described captures as committed and the
history fork as undecided. Both are settled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Issue #49: the repos carry code, tooling and docs only.
Untracks 143 screenshots, 3 savegame blobs and `tools/re-capture/ob_digits.png`
(a digit-template sheet cut from game frames) -- 146 files, 76.4 MB. They stay
in the working tree and are gitignored, so the pages' relative links still
resolve where the captures exist and nothing ships.
Derived measurements (csv/tsv/txt/log/json/jsonl/npy) are our own numbers, not
game content, and stay tracked -- they are what most claims rest on.
`check-capture-citations` had its contract inverted, and it is the half worth
reading:
* presence now comes from the WORKING TREE, not `git ls-files`. The assets are
deliberately untracked, so asking the index would report every screenshot as
missing and fail all 203 citations.
* a NEW failure: a game asset that IS tracked. A screenshot that sneaks back
in is invisible in review -- a binary shows as "Bin 0 -> 1234567 bytes" --
and is permanent once merged, since removing it later needs a history
rewrite. So that half has to be loud.
Verified:
* selftest 8/8, including the new rule
* scan: 212 present, 212 cited, 0 dangling, 0 tracked -> exit 0
* force-add one PNG -> "game assets TRACKED: 1", exit 1, selftest red
⚠️ This does NOT remove the blobs from history; a clone still fetches them.
That needs a filter-repo rewrite and a force-push, which is a separate,
human-run step.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`GAME_CONTENTS.md` sat in the workspace root through the whole consolidation,
adopted by neither repo -- the corpus documents formats in depth but never said
what files the disc holds or where they sit.
Re-measured rather than transcribed, and that caught a real error: the source
placed `resource3d/`, `DefTables` and `MiscBin` under `dat/`. All three are in
`hidden/` -- which is why `SYLPHEED_RES3D` points at `hidden/resource3d`.
Counts, the language table and `media_id` (0x2D2E2EEB, from the XEX header)
re-verified against the retail extract.
Half the source was pre-RE speculation phrased as status -- `dat/*.pak` marked
"Unknown, magic bytes TBD" when the container is decoded disc-wide, plus a table
guessing each archive's contents from its name. Carrying that forward would put
claims into the corpus the corpus has already refuted, so it is dropped and the
page says what was dropped and why. INDEX.md stays the single authority on
format status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resuming happens on the other machine, so anything that lived only in this
workspace either comes into the repo or gets named as something to carry.
**The launchers come in.** `run-canary-safe.sh` (Wine) and
`run-canary-native-safe.sh` (native) are the ONLY sanctioned way to start Canary
from the editor — they force software Vulkan and refuse to launch while a
hardware Vulkan device is visible, because Canary on the AMD GPU takes VS Code
with it. They sat in the workspace root, outside git, hardcoding one machine's
absolute paths, which is exactly why they could not be checked in. They now
derive the workspace from their own location and honour `$SYLPHEED_ISO` and
`$CANARY_BIN`, so the layout is a default rather than a requirement. Same for
`run-canary-native.sh` (interactive, hardware Vulkan — not from the IDE),
`diagnose-freeze.sh`, `live-guest-state.sh`, `heaptrack-wrap.sh` and the
`asound-null.conf` the native launcher needs beside it.
Both safe launchers were run from `tools/` before this commit: the native one
reached content in 20 s (`VERDICT: HEALTHY`, title + 25,398 XMA lines, no
faults), the Wine one ran 25 s and logged 4 `ADV.wmv` hits.
**`docs/agents/HANDOFF-2026-09-18.md`** records what changed since 2026-09-16,
what is on the server, what cannot travel through git, and the traps this machine
paid for — chiefly that the guest-PC branch probe exists only on the fork's
snapshot branch, so a rebuild from `sylpheed-re` silently loses it.
**`HANDOFF-2026-09-06.md`** gets a correction note rather than an edit: a plain
clone works again now that the 545 MB branch is gone, and its baselines are two
baselines old.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Project Sylpheed ships as a retail XEX2 (human decision, 2026-09-16), so
Phase 2's key harvest is closed rather than left looking unfinished.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Record the end state at the top of the page: four repositories archived, their
clones gone from this machine, the Phase 7 drops done or never present here, and
Canary's default branch moved off `xenia-rs`. The retention question and the
history rewrite stay open for the human.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing here changes what a tool computes; it changes where tools look.
- tools/re-capture: 33 censuses globbed /work/sylph_extract, a path that has
existed nowhere since /work became a clone, so they matched nothing and
printed empty results. They now resolve the disc through a new disc.py
from $SYLPHEED_DISC and exit loudly without it (the #44 fix, generalised).
Nine scripts that imported siblings from the retired Reborn checkout or an
old session scratchpad now import from their own directory. unitgroup.py
only needs the variable when --pak is not given.
- sylpheed-xex: the loader only ever uses the XEX2 retail key. The dead
devkit key and a doc comment claiming a devkit fallback that does not
exist are gone; Project Sylpheed is a retail XEX2, so no XEX1 key either.
- sylpheed-viewer: real_font_rasterizes looked for /tmp/sylph_extract and so
always skipped. It reads $SYLPHEED_DISC now, and passes against the disc.
- Comments and docs that named xenia-rs, the Reborn repository or /work/*.pe
as places to look now name sylpheed.db, Canary's ppc_context.h and the
flat .pe; docs/re/README.md no longer says the native Canary build does not
run.
Historical records keep their original paths: findings that were measured
against /work/xenia-rs/sylpheed.db still say so.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three untracked files in the project root had no home in either repository:
- the Xbox 360 technical reference report (a research compilation) becomes
docs/reference/xbox360-re-technical-reference.md, unchanged;
- XBOX360_ARCHITECTURE.md becomes docs/reference/xbox360-architecture.md,
trimmed to its platform facts. Its format "status" sections (PAK unknown,
mesh unknown, audio TODO) and the `just sniff` workflow predate every
decoder in this repo and were wrong;
- generate_export_docs.py becomes tools/generate_export_docs.py, the path the
committed xbox360-exports.* already name as their generator. It lived
inside a Canary checkout, so it now takes --canary and --out and fails
loudly on a tree that is not Canary.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Instructions and generated text that still sent readers to `xenia-rs`, which is
archived and deleted locally. Provenance and dated research records are left as
they are — "lifted from xenia-rs", or a finding stating what database it was
measured against, is still true, and rewriting it would falsify the record.
Changed, because each one tells someone what to do today or writes stale text:
docs/re/README.md the RE toolchain guide queried `xenia-rs/sylpheed.db`
with `xenia-rs/zq.py`, and its "Dynamic" bullet said
to prefer xenia-rs's probe suite OVER Canary. Now:
`sylpheed.db` at the repo root (how to build it and
re-stamp names), `tools/zq.py`, dynamic = Canary, and
`.rdata` reads from the `.pe` (offset = VA - 0x82000000)
in place of `--dump-addr`.
challenge-mission-gate.md, structures/achievements.md
reproduction commands `python3 xenia-rs/zq.py dis …`
crates/sylpheed-xexdb/SCHEMA.md
titled after the retired `xenia-analysis` crate,
citing `xenia-rs dis`, pointing at a `xenia-analysis`
source path
sylpheed-xexdb/src/formatter.rs
WROTE "generated by xenia-rs" into every disassembly
it produced (no test pins the banner)
sylph-xexdb.rs, db.rs "SQLite" / "DuckDB writer for xenia-rs"
sylpheed-formats/src/hash.rs
pointed at `xenia-rs/RE_SYMBOLS.md`, now at
`docs/re/RE_SYMBOLS.md`
Verified: every rewritten reproduction command was extracted from the doc and
run as written against the regenerated database — all 5 exit 0 and return the
disassembly they describe. The README's function count (25 676) is the database's
own. `cargo fmt --all -- --check` clean.
Not changed, deliberately: the README's Oracle bullet says Canary's native Linux
ELF "crashes / does not run". That is about Canary, not xenia-rs, and it conflicts
with a July note that the native build works — unverified either way here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 3 lifted the DuckDB tool out of `xenia-rs` but not the two files that go
with it, and this branch's own code already depends on one of them: the FK rule
in `db.rs` and `db_schema_golden.rs` exists *because* `apply_re_symbols.sql`
re-stamps reverse-engineered names onto `functions` after every regeneration.
Sylpheed referenced that file three times and contained it nowhere — on no
branch. CONSOLIDATION.md's end state is "Sylpheed holds everything", and the
`xenia-rs` clone is scheduled for deletion.
docs/re/RE_SYMBOLS.md byte-identical to xenia-rs/RE_SYMBOLS.md (cmp)
tools/apply_re_symbols.sql every statement identical; the two comment lines
that name paths now name this repo's paths
Verified against the real 336 MB database, which carries all 16 foreign keys,
inside a transaction that was rolled back (the database is unchanged):
every statement runs, and 208 functions carry reverse-engineered names. Run
from both the old location and this one.
Not harvested: `xenia-rs/zq_dis.py`, an untracked six-line subset of
`zq.py dis` with a hardcoded path — superseded, nothing to keep.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes the page's one explicitly outstanding result. Run in sylph-ci:local
(rustc 1.98.1, byte-identical to the runner) with the disc present and
--no-fail-fast: 45 suites, 367 passed, 1 failed, 14 ignored. The +9 against the
disc-less #35 run (377) is exactly #35's nine imports.rs unit tests.
The single failure is ui_prm_primitives_disc, red on main since #23 and
bisected there, not introduced by #32.
Records the three things that stopped the number being obtained, as rules:
--no-fail-fast (an earlier run silently covered half the workspace), a memory
cap sized to the host (the default 7 GB OOM-kills slb_leading_segment_disc on
this machine with the disc mounted), and a ~36 GB disk budget for a cold target
volume.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Re-checked against the server rather than the page. `Sylpheed` has 7 tags and
all 7 are `formats-pin-*`. The eighth this page counts, `formats-pin-2026-08-29`
without a letter suffix, is in `Syplheed-Reborn` — a repo a Sylpheed rewrite
would not touch, and one Phase 5 archives read-only regardless.
The pin itself is exactly as claimed: `formats-pin-2026-09-01` resolves to
`e2630413`, and the in-container build fetches that git dependency over the
network for real, so the blocker is live rather than theoretical.
Also updates the disk figures the "don't rewrite" recommendation rests on: 90%
used and 96 GB free, not 83% and 154 GB, with `target/` at 34 GB. The
recommendation is unchanged and the argument is now stronger — reclaiming
118 MB by rewriting history while 34 GB of rebuildable output sits beside it is
the wrong lever.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This page says not to trust its own ledger on arrival, so the checks were run
again rather than read. Nothing was archived and nothing was deleted — both are
the human's, and both phases stay open.
The substantive finding is that Phase 5's containment check is the wrong test
for half the list it names. `xenia-rs` and `xex2tractor` were never absorbed;
their disposition is harvest-then-archive. Run the check literally against
`xenia-rs` and it returns 42 of 42 refs "not present in Sylpheed" — every line
true, every line unactionable, which is the failure this page already names
once about Phase 0's first gate. For those two the invariant is that the
harvested artefacts are in Sylpheed and the repository is archived rather than
deleted; that is now checked separately, and it shows the harvest is real but
lives only on the unmerged PR branches, so Phase 5 should follow them rather
than precede them.
Containment itself holds for the two repos it does apply to: 21/21 and 2/2.
Also answers Phase 7's one open question — nothing cites `texcompare/`, and all
four `ship_render` citations name the example program, not the output
directory — and records that most of Phase 7's list does not exist on this
machine, that `has_wiki: true` is a flag rather than content (all six wikis
return 404, never initialised), and that the disk is at 90%, not the 83% the
page records.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This page recorded it as "not changed here, because the right replacement is a
decision". The decision was taken: PR #35 `38cc170` resolves `$SYLPH_XEXDB`,
then `<repo root>/sylpheed.db`, then refuses with both named — no third
fallback, because the database is an untracked build artefact and a default
that silently resolves to the wrong one is worse than no default.
Leaving the page saying otherwise would be the defect this section is about.
`SCHEMA.md`'s retired-repo heading is still outstanding and stays listed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found 2026-09-14 while pulling the repos on the other machine: `xenia-rs` had
468 uncommitted lines plus an untracked 338-line `imports.rs` in the very crate
Phase 3 lifted, written 2026-09-10 and sitting there for four days. Verified
absent from `iterate-4A` and from the lifted crate in PR #32 — it existed in
exactly one place, and that place was not git.
Phase 0 is titled "Secure the single-copy work" and its gate passed anyway,
because the gate iterates `refs/heads` and a dirty working tree has no ref.
Canary's two dirty files were secured only because a human already knew about
them; nothing found them. Phase 1 then declared the archive honest while this
sat beside the branches it pushed. Phase 5 ends with "delete the local clone"
and Phase 7 drops the emulator, so a deletion was scheduled against it.
Records: the finding and the `git status --porcelain` half the gate was
missing (run across all four repos — only Canary is dirty, and both entries are
already on Phase 7's drop list); that it is secured on `harvest/import-thunk-
naming` and ported in `feat/xexdb-import-naming`; a fifth entry in the
wrong-claims list; and a `0b` row in the status table.
Also records three pieces of Phase 3 residue found while porting, the sharpest
being that `tools/zq.py` still defaults its database path to a location inside
`xenia-rs` — the repository Phase 5 archives and Phase 7 drops. Left unchanged:
the right replacement is a decision, not a guess.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Paused at six of eight phases, all committed and pushed, nothing left in a
working tree. The page now carries a STATUS section the other machine can
finish from: what is done, which PR each phase is in, what to run next, and
what is deliberately left for the human.
🔴 FOUR THINGS THIS PAGE ASSERTED WERE WRONG, and each is corrected in place
rather than quietly amended:
* "10 dangling citations" was ONE. Eight were DIRECTORY references, which
resolve and are absent only from `git ls-tree`; one was a path ending a
sentence, with the full stop pulled into the match. I reported that 10 to
the human twice.
* `extract -r` is not how the .pe was made and nothing depends on it. The
plain extract is, and the tool Phase 3 keeps reproduces it byte-for-byte.
* The Phase 0 gate compared branches by SHA and called seven safe branches
unpushed -- a branch that is an ancestor of a pushed one is preserved.
* There is no DirectXShaderCompiler drift; the ' m' is nested submodule dirt.
The pattern is worth more than the corrections: every claim that was REASONED
turned out wrong, and every one that was RUN held. Phase 2's gate refuted its
own premise, Phase 3's proved two alarming row deltas were an improvement, and
Phase 4's check caught a deletion I had just made. The page now says to re-run
the gates rather than trust the results recorded in it.
Phase 5 (archive) and Phase 7 (drops) are open and marked NOT STARTED, with
the reason each needs the human: archiving is reversible and deleting a
repository is not, and the drops are on the human's disk.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CONSOLIDATION.md Phase 4. `tools/port/check-citations` has checked
`docs/port/*.md` since it was written; `docs/re/` never had one, and
`docs/re/captures/` is the largest thing in the repository -- the one place a
file can be added, never cited, and never noticed.
captures committed : 212
cited by a page or a tool : 212
cited but NOT committed : 0
🔴 THE PREMISE I STARTED FROM WAS WRONG FOUR TIMES, AND EACH CORRECTION IS IN
THE TOOL RATHER THAN JUST IN MY HEAD:
* "10 dangling citations" -- the real number was ONE. Eight were DIRECTORY
references, which resolve and are simply absent from `git ls-tree`; one was
a path at the end of a sentence with the full stop pulled into the match.
A checker that cries wolf nine times in ten teaches you to ignore it.
* The one real dangler, `live-submenu-unidentified.png`, was COMMITTED and
then deleted on 2026-08-30 while the page kept citing it and kept making
the claim it backs. Restored from 77e54e95 rather than dropping the link:
deleting evidence while keeping the conclusion is the thing this corpus
exists to prevent.
* Two "orphans" are opened BY FILENAME from `screen_match.py`, which builds
the directory separately. A path-only scan called them unreferenced and
deleting them would have broken the tool. The check now counts any
basename named anywhere.
* Pruning then EMPTIED two directories that pages cite as directories, and
the check went red on the very citations that made them evidence. It
caught its own damage. A file inside a cited directory is cited.
The selftest covers all of it, including the failure that actually happened:
an earlier version passed five green ticks while the scan silently returned
NOTHING, because `git grep -E` is POSIX ERE and cannot compile `(?:`. A
selftest that cannot see the failure that occurred is decoration.
46 orphaned captures dropped, 30.7 MB from the checkout. ⚠️ That reclaims no
repository space -- the blobs stay in history -- and it is not meant to. It
means every capture here is now evidence some page or tool actually uses.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Written for the other machine, which asked. Everything in it was read off the
running host rather than remembered.
Also adds docker/ci/ -- the CI image recipe and a capped runner -- because the
image existed on exactly one host and its Dockerfile was in a scratch directory
under /tmp, which was swept. That is the same shape as every other thing this
consolidation has turned up: something correct that exists in one place.
docker/ci/Dockerfile rust 1.98.1 + the apt list copied from ci.yml
docker/ci/Dockerfile.ffmpeg + ffmpeg, which sylpheed-export shells out to
docker/ci/run 6 CPUs / 7 GB / NO SWAP, named cargo volumes
The rule the runner exists to enforce: every heavy command goes in the capped
container. CARGO_BUILD_JOBS caps codegen units, not rustc's threads, not the
linker, not the test harness -- a bare host build is unbounded and has frozen
this box repeatedly.
The document also records the two agent defects the other machine will meet:
a brief change does not reach a RESUMED session, and nothing brought an agent
back to its own red PR (fixed in #24, which is itself subject to the first).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Written for the other machine, which asked. Everything in it was read off the
running host rather than remembered.
Also adds docker/ci/ -- the CI image recipe and a capped runner -- because the
image existed on exactly one host and its Dockerfile was in a scratch directory
under /tmp, which was swept. That is the same shape as every other thing this
consolidation has turned up: something correct that exists in one place.
docker/ci/Dockerfile rust 1.98.1 + the apt list copied from ci.yml
docker/ci/Dockerfile.ffmpeg + ffmpeg, which sylpheed-export shells out to
docker/ci/run 6 CPUs / 7 GB / NO SWAP, named cargo volumes
The rule the runner exists to enforce: every heavy command goes in the capped
container. CARGO_BUILD_JOBS caps codegen units, not rustc's threads, not the
linker, not the test harness -- a bare host build is unbounded and has frozen
this box repeatedly.
The document also records the two agent defects the other machine will meet:
a brief change does not reach a RESUMED session, and nothing brought an agent
back to its own red PR (fixed in #24, which is itself subject to the first).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CONSOLIDATION.md Phase 2. Each of these existed only in a repository about to
be retired, or as an untracked file on one disk -- which, for anything that
has to survive a machine, is the same as not existing.
xex2-format.md 39 KB the XEX2 container format
xbox360-exports.{json,md} 1.1 MB 2,913 exports: xboxkrnl, xam, xbdm
ppc-instructions.{json,md} 340 KB the PowerPC instruction reference
LICENSE MIT -- this repo had none
The first two are byte-identical to xex2tractor's copies, verified with cmp,
and byte-identical again to the loose untracked copies in the project root:
three copies, zero of them tracked.
docs/reference/ is deliberately its own directory with a README that says
what it is NOT: nothing here is a finding, and no docs/re/ page should cite it
as evidence. Reference material filed beside measurements is how a borrowed
table starts getting read as a result.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PHASE 0 -- the single-copy work is secured. Canary 8e63a9542 commits the
--pad_file_repeat instrument that had been uncommitted while the numbers it
produced were already shipping in the port; sylpheed-re and
audit-handle-lifecycle-probes pushed; all 14 local branches reachable.
Two corrections found by running it:
* There is no DirectXShaderCompiler gitlink drift. `git submodule status`
shows no '+'. The ' m' is NESTED submodule dirt (DXC's own SPIRV-Headers
and SPIRV-Tools), which is upstream's business.
* The gate itself was wrong. It compared branches to remotes BY SHA and
called seven safe branches unpushed -- a branch that is an ANCESTOR of a
pushed branch is already preserved. It now tests reachability, which is
what it meant. A check that goes red for something unactionable is the
failure this repo keeps naming, and this page shipped one.
PHASE 1 -- the four retiring xenia-rs branches pushed (32/52/56/59 commits).
The loop is clean everywhere except pi/clippy, pi/clippy-clean and pi/reauth3,
unreachable by ancestry and redundant by patch-id: the one case where the
answer lives outside the gate.
PHASE 2 GATE -- run, and it corrected this page in two directions.
extract (xex2tractor, no -r) IDENTICAL to the project .pe
extract -r (xex2tractor) differs -- 1,903 bytes over 273 runs
extract (xenia-rs, iterate-4A) IDENTICAL, and .xex.json too
Right: the .pe is an xex2tractor extract. Wrong: it was made WITHOUT -r. So
the dependency this gate existed to protect does not exist -- the tool Phase 3
keeps already reproduces the file. -r drops from 'must port' to optional, with
a warning that it must never become the .pe, since it writes into the image
and would invalidate every byte-offset citation in docs/re/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The end state the human set: Sylpheed holds the port, the corpus and every
tool; the Canary fork holds the emulator and oracle. Four repos retire.
Eight phases, each with a gate. What matters more than the sequence is the
ledger: every absorption claim here was RUN, not reasoned.
Reborn's 21 remote + 8 local branch tips -> all ancestors of Sylpheed/main
Sylpheed-Godot's 2 tips + the root graft -> all ancestors
agent-backups/, 23 refs across 4 bundles -> all present in Sylpheed
3 unpushed pi/* branches, by patch-id -> all 4 patches already on main
issues / PRs / releases / wikis, 6 repos -> zero outside Sylpheed
Three things the check found that a plan built on the obvious reading would
have destroyed:
1. The F1 instrument -- Canary's --pad_file_repeat patch, the one behind the
repeat rate now shipped in the port -- is UNCOMMITTED, alongside 4 unpushed
commits and a branch contained in no remote. Phase 0 exists for this.
2. xenia-rs has 4 unpushed branches of JIT/multicore/native-threads work.
Dropping the emulator is a decision; dropping work that was never pushed is
a different act, done by accident.
3. The DuckDB tool is not on master, which last moved 2026-06-15. It is on
iterate-4A/apu-xma-stage1, +22 commits, 2026-09-10.
On the captures question the measurement contradicted the premise: 193 of 257
files are cited by a page or opened by a tool, and two are fixtures that
cargo test opens. 64 are orphans. Phase 4 builds docs/re/ the citation check
docs/port/ has had all along -- it already finds 10 dangling citations -- and
fixes those BEFORE deleting orphans, so the deletion cannot ratify them.
Deleting reclaims nothing anyway: the 118 MB is in history, and the build
depends on this repo by tag (formats-pin-2026-09-01, pinned in Cargo.lock).
That is Phase 5's explicit fork rather than a default.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reported by a human on a real controller: hold the left stick down, the
cursor moves one item and stops. The repeat never runs.
`held_direction()`'s own comment says "Polled at the DEVICE, never through
Input.is_action_pressed". It was not. The d-pad and keyboard branches polled;
the STICK branch read `_latched`, which is a reconstruction of the stick's
position from the event history.
That reconstruction is only as good as the last event seen. A stick held
still sends nothing, and one event reading below RELEASE -- a spring
settling, a deadzone-shaped value, a driver emitting a zero on focus change
-- clears it with no event afterwards to set it back. The port then believes
the stick is centred while the player is holding it, which is precisely the
symptom reported.
Now polls `Input.get_joy_axis()` against the game's own 0.61, which is what
the comment always meant. The latch stays as a fallback for INJECTED events,
so the script harness and verify-input keep testing something.
🔴 Every instrument here missed this because every instrument SUPPLIES the
input it measures: verify-input ticks repeat_due() directly, --script sends
InputEventAction which bypasses the input map, and the new --script=hold:
injects its own axis event. All three agreed with each other and none read a
device. Same shape as the 2026-09-01 report that opened gamepad.gd, one
level deeper, with the lesson already written at the top of that file.
So this adds the two things that would have caught it:
--script=hold:down:2.0 hold one real axis deflection and log every move
--input-probe print what the devices report, on change
⚠️ The fix itself is NOT verified. It matches the symptom exactly and was
found by reading, but only a human holding a stick can confirm it, and the
probe exists so the answer is measured either way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
REPEAT_DELAY = 0.402, REPEAT_INTERVAL = 0.134, from
docs/re/f1-repeat-measured-via-driver-patch.md -- 12 and 4 frames at the
run's achieved 29.87 fps guest rate, converted to seconds because this port
does not run at the guest's rate and it is the cadence that was measured.
The mechanism has been here since 2026-09-02 and inert on purpose. The
instruction it was waiting on is now vindicated in the most awkward way: the
draft it refused to ship had 0.40 / 0.20, so the guessed delay was nearly
right and the guessed interval was off by 50 %. The half that was wrong
would have been protected by the half that was right.
The delay and the interval are NOT equally well evidenced, and the code says
so at the constants. No physical controller exists in the Decoder's
container, so the measurement fed Canary's file driver the SDL driver's own
400/100 ms constants: the 402 ms that came back is the constant that went
in, and confirms the instrument. The 133 ms interval against a fed-in 100 ms
is the new fact -- the game paces repeats to its own frame consumption. One
run; the two-run minimum is not met and the finding says so itself.
Also: the prediction that this would turn verify-input's "a held stick is
ONE step, not six" red was wrong. It stayed green, because steps() never
advances a clock and so had never called repeat_due() at all -- the rate was
about to ship into a harness with no coverage of the feature, with a green
line that would have been read as coverage.
So verify-input gains a `repeat` subject: nothing before the delay, the
first repeat on the delay, the steady interval, cadence independent of frame
rate (the code claims this in a comment, so it is now asserted), and a
direction change restarting the delay. Each asserts THESE numbers, not the
shape -- a shape-only check would have passed on 0.40 / 0.20. The control
removes the premise, a held direction, and every controllable row inverts.
The first-repeat row measures from the arming tick, not from t=0: that tick
is the frame the press is handled, which is the origin the finding measures
its 12 frames from. Measured from zero it read 0.433 vs 0.402 and the
tolerance would have had to be widened to hide a units mismatch.
Closes#2
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`mesh_consistency_disc.rs` had the only conflict: this branch added a
`Sightings` type alias where #22 replaced the file's private `disc_root()`
with the shared `common::disc_root`. Both kept — they are unrelated edits
that happened to land in the same lines.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No emulator-requiring state/approved item was open this iteration (issues
#1, #3, #5 all landed and moved to state/needs-human; #25 still awaits
approval). Used the gap for corpus consistency instead of idling, per the
same "a correction that never reaches the row someone reads" failure this
project keeps naming.
1. menu-navigation-semantics.md's own Q4 table still said NEW GAME was
"not tested" and its own prose said it was "deliberately not pressed"
and hangs the emulator -- both refuted BY THIS SAME PAGE on 2026-08-28,
34 lines further down ("NEW GAME -- measured ... it is not a hang. It
opens DIFFICULTY then SELECT DATA"). The correction never propagated
backward into the table or the status line above it, so a reader
stopping at either would come away with the wrong (and already-refuted)
answer. Fixed in place, struck rather than deleted, with the actual
destination and an honest note that DIFFICULTY has no id-table name
match (already refuted separately) while SELECT DATA plausibly matches
GP_SELECT_STORAGE as a fresh, low-confidence guess.
2. Found the identical failure mode in my own recent work: HANDOFF.md's
original Q1-Q10 summary table (near the top of a 6600+ line file) still
quoted the 2026-08-30 "no auto-repeat" finding as current, three commits
after this same session measured 12 frames delay / 4 frames interval
through a repeat-capable driver and explained why the earlier negative
was a driver limitation, not a game fact. A reader who only sees the
summary table -- which is exactly what a long file trains a reader to
rely on -- would get the withdrawn answer. Fixed with an explicit note
pointing at the current entries rather than silently editing the number
in place, so the correction itself stays visible.
Refutation-shaped either way: two claims ("NEW GAME untested", "no
auto-repeat") checked against this corpus's own newer evidence and found
not to survive, recorded rather than left to be rediscovered.
Continues the static lead from two iterations ago
(f3-title-sting-mechanism-found-not-value.md) with the dynamic half it
named as the next step. Booted with --xma_param_probe=true (the same
census menu-audio-cues.md used for the menu's SE cues), no pad input,
recording continuously from window-open: a glyph time series (not a
threshold trigger) and every newly-seen XMA-PARAM stream, stamped on
arrival since Xenia's own log carries no timestamps.
Positive control, and a real one: the probe caught the title's two BGM
stems starting at t=147.6s, matching f3-title-plays-bgm-102-and-103.md's
already-established finding exactly -- the instrument finds a real stream
before being asked to find nothing.
From the plate's first visible activity through 68 seconds of build-in
plus fully-settled pulsing (killed at t=220.1s), zero new XMA streams
appeared beyond the two BGM stems and three unidentified early ones (boot
splash, not this question). No SE-range stream, no second BGM, nothing --
measured, not the prior static reach limit.
Refutation attempt this iteration, recorded either way: my first read of
the fine-grained glyph series said BGM and the plate's build-in start at
"essentially the same moment". Checking the raw per-sample data instead of
a coarse table refutes that -- first non-zero glyph reading is ~0.67s after
BGM onset, and immediately noisy rather than a clean climb. Corrected in
the doc rather than left as an overclaim for someone chasing frame-accurate
sync later.
Reference data: docs/re/data/f3-sting-{glyph-timeseries,xma-param-arrivals}.tsv
-- derived numeric/log-line data, not a capture of rendered game content.
Direct follow-through on this session's own named next step. The prior
result (f1-held-down-measured-no-repeat-via-file-driver.md) concluded the
file driver cannot show menu repeat because its GetKeystroke() never emits
a REPEAT-flagged event, and that the menu's repeat is very likely driven by
that flag rather than raw polled state. Testable, so tested: patched
/canary/src/xenia/hid/file/file_input_driver.h to add opt-in repeat behind
a new --pad_file_repeat cvar (off by default, every other scripted script
unaffected), using the SDL driver's own constants verbatim
(HID_SDL_REPEAT_DELAY/_RATE = 400/100, guest-time ms via
Clock::QueryGuestUptimeMillis) rather than re-deriving them. Incremental
rebuild, ~1 minute (only xenia_main.cc needed recompiling).
Control: the driver's own log confirms repeated keystroke events fire as
designed, zero crashes.
Result: re-ran the identical held-DOWN capture. The cursor that moved once
and stopped in the null result now cycles continuously through the whole
5-item menu, wrapping, for as long as the button is held -- the null result
was real for that driver path, and giving the driver the one thing it
lacked reverses it completely.
Measured at this run's achieved 29.87 fps guest rate: 12 frames (~402ms)
initial delay from the press-triggered step to the first repeat step; 4
frames (~133ms) steady-state interval for 13 of 15 gaps, 3 frames (~100ms)
for the other 2 -- slower than the raw 100ms constant driving it, which
this page flags but does not trace further (most likely the game batches
drained keystrokes per its own frame tick rather than reacting to each one
instantly). The 4-frame figure is what matters for the port: it's what the
cursor visibly does.
Honestly scoped: this measures what the game does when FED repeat events
shaped like the SDL driver's, not a capture through an actual physical
controller (none exists in this container) -- classified measured, not
decoded, for exactly that reason. One run only; the corpus's two-run
minimum isn't met, flagged rather than overclaimed.
f1_hold_capture.py gains an optional `repeat` argument. The Canary source
patch itself lives in /canary, outside this repo (Canary source, not
sylpheed-formats) -- fully described inline in the finding doc so it can be
reapplied if that tree doesn't persist across a container reset.
Reference data: docs/re/data/f1-repeat-cursor-transitions.tsv -- every
transition's frame, guest tick and Y position, not the raw draw log.
Fifth boot attempt this session, first clean one, using the harness debugged
last iteration (env-safe input, wide capture window, signed-in profile,
blanked X root). Reached the menu, held DOWN 2.5s wall-clock, armed the F10
draw capture, released, killed -- zero crashes.
Achieved vs requested: 433 of 600 requested frames, spanning 14.43s of GUEST
time against ~4.5s wall-clock for the hold+tail window -- this run went at
~3.2x real time (a cheap static menu, nothing pacing it to a display
refresh). The 2.5s wall-clock hold covers roughly 8s of that guest time.
Method: track one quad shape's position per frame via read_draws.py, not a
screen diff -- the approach f1-menu-repeat-harness-built-not-answered.md
already named as correct. Exactly one shape-group moves at all: one jump of
~0.21-0.225 NDC units (matching the known one-menu-item pitch), complete
within 4 frames / 133ms guest time of arming, then flat (a ~0.0125 pulse
wobble only) for the remaining 427 frames / 14.3s of guest time. Broadening
the match threshold finds no other moving element.
This is a STRONGER negative than the 2026-08-30 result it agrees with (that
one sampled a screen diff at ~4-5fps for 2.0s; this reads every quad every
frame for ~8s of guest-time held) -- it survives, rather than confirms, last
iteration's "coarse sampling hid a fast repeat" alternative, which does not
survive a per-frame instrument over a much longer window. Refutation
attempt against my own prior lean, recorded: it does not survive.
Reconciled, not left in conflict: the human's play-test and pad.py's
"auto-repeats" warning almost certainly went through a REAL controller
(Canary's SDL driver), which auto-repeats keystrokes at a documented
upstream 400ms/100ms (guest time) via the REPEAT keystroke flag. The file
driver deliberately never emits that flag, by design -- so it cannot show
repeat regardless of how carefully or how long it's held, which is exactly
the negative measured here. Still no F1 number; now a specific, well-
evidenced reason the file driver alone will never produce one, and a named
next step (add opt-in REPEAT-flag support to the file driver, matching the
SDL driver's constants, then re-run this exact capture) -- not attempted
this iteration, a second unit on an already-complete one.
Reference data: docs/re/data/f1-cursor-quad-y-per-frame.tsv -- the derived
per-frame Y position and guest tick, not the raw draw log itself (a capture
artifact, kept uncommitted per the corpus's game-content rule).
Tried to close out issue #1 with an actual draw-log measurement this
iteration: built f1_hold_capture.py to boot to the settled main menu via the
proven glyph-gated route, arm the F10 UI-draw capture, hold a direction, and
read cursor position per frame -- the instrument f1-menu-repeat-harness-
built-not-answered.md already validated but never got to run against a
reachable menu.
Four bugs found across four boot attempts:
1. tap() shelled out to pad.py without this script's own env, so the press
went to /tmp/xenia_pad.txt while Canary watched OUT/pad.txt -- an
unobserved press indistinguishable from a dead pad. Fixed with an
in-process tap() using the same pad() the hold uses; confirmed working
the next run (title 154.5s, menu 163.0s).
2. ui_draw_capture_frames/max were persisted at 3/20000 from a prior
session in xenia-canary.config.toml -- log_ui_draws is now a documented
no-op (F10 arms unconditionally) and these two cvars didn't visibly
respond to command-line overrides. Bumped to 600/400000 directly in the
config.
3. The real blocker: this container has no signed-in profile (no content/
directory at all -- a fresh container after a restart, which every
container is right after one). Without a profile the title's sign-in
dialog sets IsUIActive() true, which reproduces
structures/title-a-press-fault.md's already-diagnosed unbounded-
keystroke-queue crash -- verified byte-for-byte against that page's own
addresses (PC 0x868 past sub_82457038, 0x828F3xxx registers, identical
host/guest address arithmetic), looping continuously from before F10 was
ever pressed. Fixed by creating a profile (--create_profile_if_none) and
signing in (--logged_profile_slot_0_xuid), matching boot_menu.sh, which
already did this and so never hit it. Confirmed: zero crashes with the
fix, dozens per run without it.
4. Found but not re-verified: no xsetroot blank before launch, so a stale
X-root frame from a killed prior run gave a false "TITLE" read at 2.6s,
before any real window existed -- skip_intro.sh already blanks the root
for exactly this reason. Fixed in the script.
Ran out of budget before a clean end-to-end run landed. Still no number for
issue #1 -- the Port keeps -1.0. Flagged prominently (HANDOFF, REFUTED.md)
because bug 3 will hit any bare run-canary invocation in any fresh
container, not just this script.
Continuing the F1 investigation rather than starting a fresh one. Last
iteration left two competing hypotheses open (Keystroke-queue-driven vs
polled-state-driven repeat) and flagged C_PAD_RINGBUF's producer as the
cheapest thing to trace next -- named but not traced.
Traced it this time: C_PAD_DECODER's own constructor (sub_8220B610)
allocates C_PAD_RINGBUF (52-byte control struct, 1024-byte backing buffer,
confirmed against its own Shift-JIS trace strings -- "C_PAD_RINGBUF
initialization" and its allocation-error message). Its update function
(sub_8220B8C0) takes the input-manager singleton as a parameter and reads
the ring at offsets 12, 36, 40, 44 and 48 -- not just the one button word.
Offsets 36-48 are four consecutive fields read together through the same
int-to-double conversion an analog axis would use. XamInputGetKeystrokeEx
has no field for a stick position, so a structure carrying four axis-shaped
fields cannot be a keystroke queue -- it reads as a periodically-refreshed
polled-state snapshot. My own prior reading of the "ring buffer" name as
implying a queue was the wrong inference; refuted by tracing it, recorded
either way per adversarial duty.
This shifts the balance toward the second, previously-uncertain hypothesis:
the file driver's GetState() was always capable of showing real repeat (no
modification needed), and nav_repeat_and_b.py's null result is more likely
a sampling artifact of its ~4-5 fps screen-diff detector than a structural
driver limit. Revises "what would close it" accordingly -- re-run the
existing draw-log position-tracking instrument, gated on the menu properly,
before reaching for a driver change.
Not found: the actual producer that writes into C_PAD_RINGBUF each frame --
narrowed to "reachable from the input-manager singleton fetch in
sub_821A9DC8," not traced to completion. Still no number for issue #1;
this narrows the path to one, further than last iteration but not there.
Issue #1's premise (the human's play-test: a held direction repeats) directly
contradicted menu-navigation-semantics.md's 2026-08-30 "no auto-repeat"
row. Read the instrument before trusting either: nav_repeat_and_b.py drove
input through Canary's --hid=file driver, and that driver's GetKeystroke()
is explicitly, deliberately built to deliver exactly one event per held
press ("scripted input wants precisely one event per press, and repeat is
what makes menu steps overshoot" -- file_input_driver.h's own comment).
input-pad-read-path.md already established the game reads menu input via
this same Keystroke API. A driver engineered to prevent repeat cannot be
evidence the game lacks it -- the counter's control (a tap gives 1 spike)
proved the counter works, not that the driver could show more than one.
Not a clean reversal, and said so: the same driver's GetState() holds a
button continuously with no edge suppression, and pad.py's own docstring --
written by an earlier session driving this exact tool -- warns that a longer
dpad hold "auto-repeats and overshoots," describing an observed effect
through this same driver. The two pieces of evidence disagree and this page
does not resolve which wins.
Also read from Canary's source: the SDL input driver (what a real controller
goes through) auto-repeats keystrokes at 400 ms initial delay then 100 ms
interval, guest time (HID_SDL_REPEAT_DELAY/RATE, upstream Xenia, not a
project change) -- a concrete, testable prediction for what the real number
could be if the menu treats repeat-flagged keystrokes as nav steps, matching
the human's "medium pace" description. Not yet measured.
Refutation attempt this iteration, recorded per adversarial duty: targeted
the 2026-08-30 "no auto-repeat, ✅ measured" claim. Survives only partially --
demoted to unsettled, not flipped to a confident opposite. New REFUTED.md
section (Menu navigation and input) and the row in
menu-navigation-semantics.md both corrected in place, old text kept per
convention.
What would close it: trace C_PAD_RINGBUF's producer (keystroke ring vs
polled state) statically, or add an opt-in repeat mode to the file driver
and read cursor position off the draw log per frame. Neither run this
iteration -- this is the static half, and reversing a standing claim is
enough for one unit without stacking a build-and-boot run on top of it
unverified.
f3-title-plays-bgm-102-and-103.md's BGM census resolved a literal
`addi r5,r0,<imm>` before every call to the play primitive, which is blind by
construction to a cue id supplied any other way. Two of the primitive's 34
callers ARE supplied another way -- one is a register passthrough inside a
generic 6-caller wrapper `sub_821CCCB0(obj, cueId)` with a -1 "don't play"
sentinel. One of its six callers chains back to slot 1 of GamePart_Title's
OWN dispatch table at 0x820a3dec, identified by its adjacent
RegisterToFactory<0, class silph::GamePart_Title> string -- the same
convention already used disc-wide for the other 28 GameParts, not a
neighbourhood guess this time.
Traced as far as static analysis goes: the field that would carry the cue id
traces to a 19-caller shared helper's return value, too common to be
title-specific -- reads as "allocate a sound-emitter handle", not "here is
the cue". The value most likely gets set at whatever runtime moment the game
wants this emitter to speak, which a disassembly listing does not contain.
Classified undecodable-with-reach, not a fourth thing -- narrower and
better-aimed than the prior reach ("SE goes through a different call").
Every address cited was independently re-decoded from the raw .pe bytes
(file offset = VA - 0x82000000), not taken from the database's own
mnemonic/operands columns, and all agree.
Refutation attempt this iteration: checked whether the "6 callers is
exhaustive" claim for sub_821CCCB0 survives indirect dispatch (a plain
kind='call' xref scan is blind to a vtable/function-pointer-array entry).
Checked function_pointer_array_entries and kind='ind_call' xrefs for all
four functions in the chain -- none found. The claim survives.
What would close it: --xma_param_probe=true during a title boot's build-in,
watching for a newly-decoded stream when the plate reaches full alpha with
no input -- the same technique menu-audio-cues.md used for the menu's SE
census. Not run this iteration; this is the static half only.
f6-out-of-sample-RESULT.md left three failures unexplained beyond "n=2 wasn't
enough". Reviewing what's already decoded: two of the three (the ptcopyright/
parent ramp ratio, the sweep-leads-plate lead) fail in the SAME direction
(0.75x, 0.71x of predicted), and f6-unit10 already established the sweep and
plate families are gated by separately-triggered parents -- a ratio across
two independently-triggered elements has no structural guarantee of being a
constant, unlike a ratio internal to one element family (which is exactly
what the passing three checks are). The gross mislabeling that caused the
ORIGINAL 1.7x conflict was fixed two days before the prereg was written, so
that's ruled out as the cause here; whether the cross-group phase genuinely
varies boot to boot vs. an artifact in the frame-based ratio math is still
open, and needs more captures to tell apart -- filed as a follow-on, not run
here.
check_labels.py conflated two different claims under one "N LABEL(S) DRIFTED"
verdict: identity checks (which element -- clock-free, still 3-for-3 out of
sample) and timing checks (a cross-element ratio and a self-consistency curve
fit -- 0-for-2 out of sample). Split into two reported groups; only identity
gates the exit code now. Tolerances untouched -- widening them to pass f6c
would be tuning the check on the case that failed it, the same error class
already named twice in this corpus. Verified with synthetic data shaped like
the real f6c residue (3/3 identity, 0/2 timing, exit 0) and confirmed the
selftest's injected mislabel still fails an identity check (exit 1) -- no
capture exists in this fresh container to run it against real logs.
The withdrawn sweep->plate lead (0.138-0.141) stays withdrawn. Nothing here
reinstates a number.
Two gaps the first post-migration decoder iteration exposed, both in the same
place — the loop ends before it can observe its own result.
1. Step 1 read notifications but never looked at the agent's own open PRs. It
opened #23, labelled the issue `state/needs-human`, and stopped; nothing in
the brief would ever bring it back to a red check. Next iteration it planned
to start a new question on top.
2. It called the merge resolved on `cargo build -p sylpheed-cli` — the crate the
conflict was in. The break was in `sylpheed-viewer`, which neither side of
the merge edited: one branch added a `ComposeOptions` field, the other had a
literal construction of it. The crate you just fixed is the one place the
breakage cannot be.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GITEA-SETUP.md listed `propose-work` as unbuilt, and this PR builds it without
touching that list, so merging would leave the doc describing a tool as missing
that exists. Strike it the way `gitea-verify` already is: say what was built,
then say what is still missing — here, an end-to-end run, which only the agent
box can do because only it holds a token that can make the POSTs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`decoder-loop.md` has had `## Each iteration` since 2026-09-04, ending in
7. Commit, `push-work`, open the PR, label the issue `state/needs-human`,
and stop.
`port-loop.md` has never had that section at all. It states the rules -- "you
cannot close your own work", "you move an item to state/needs-human", work
reaches main "through pull requests" -- but nowhere gives the STEPS that carry
them out. The Port agent was told the rule and never the procedure.
Two steps were missing entirely, and both are Phase 7 check items:
* READ YOUR NOTIFICATIONS first. They are polled and nothing pushes them, so
an agent that skips this never receives anything addressed to it -- and the
symptom is silence, not an error. That is GITEA-SETUP.md Phase 7 check 1.
* OPEN THE PR AND SET THE LABEL. That is Phase 7 check 2.
The second carries a trap worth naming in the brief rather than in a runbook:
`docker/port/bin/push-work` ONLY PUSHES. Verified by reading it -- no /pulls
call, no `Closes #`, no label; its refusals (no main, no force-push) are
intact. `propose-work`, which would do all three, is still unbuilt --
GITEA-SETUP.md's own words are "the other two thirds being manual is how they
get skipped". The capability is there (`pull_request_write` and `issue_write`
are both in the pinned MCP tool set), so this is an instruction gap, not a
permissions one.
Written to mirror the Decoder's shape while using the Port's own vocabulary --
a gate is an artifact, verify against captures rather than our renderer, state
the expected number first, `authored/` entries carry a `why`, never an invented
number where a measured one is pending.
Found by the Pi agent while answering a Phase 7 readiness question. My own
check had been decoder-only, which is why the asymmetry survived it.
Refs #11
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
§§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>
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.