Commit Graph

26 Commits

Author SHA1 Message Date
sylph-pi
b5705043bb docs: record the third softening, which was authored dirty
The two instances already in "Checks that were kind once" were correct
when written and decayed. The third was wrong on its first commit, and it
arrived by a different route: the check and the tree's failure to pass it
land in the same change, so the softening writes itself.

Concretely — the Clippy step had never run (no component in the
toolchain), and the tree is not clippy-clean, so fixing the step and
turning it red are the same commit. The first draft paired the fix with
`continue-on-error: true` and a comment promising removal once the debt
was paid: an expiry date nobody set, in the shape #12's closing line had
already ruled out for rustfmt. Reverted on reading it.

Adds the distinction, a table separating decay from dirty authorship, and
an earlier tell than the mechanical test:

    If you are writing the softening in the same commit as the check,
    the thing you want is an issue, not a flag.

The mechanical test is unchanged and still correct; this only catches the
same failure sooner, at the keyboard rather than at review.

Refs #12, #13

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
2026-09-05 15:55:04 +02:00
MechaCat02
2d5496f754 protocol: findings before citing code, and checks that were kind once
Two rules that look unrelated and are one failure, plus the change that makes
the second enforceable.

1. A FINDING REACHES `main` BEFORE THE CODE THAT CITES IT. A citation resolving
   only on a peer branch is dead the moment it merges. Not hypothetical: 495
   decoder and 366 port commits sit off `main`, and `port/scripts/boot.gd`
   already cites two docs/re pages present on neither its own branch nor main.

2. A CHECK MAY ONLY SOFTEN AGAINST A CONDITION IT CAN TEST -- the Pi agent's
   wording, and better than mine, because it is applicable while writing rather
   than a call to be vigilant. The mechanical form:

       Can this branch tell the difference between "not yet" and "no longer"?

   `gitea-protect --verify` printed  "not a collaborator (yet)" and continued,
   so the only instrument checking Write-not-Admin could not report that gate
   being REMOVED. `check-citations` reported peer citations instead of failing
   them, because under the old topology that was unfixable from the container.
   Both were correct AND kind when written; neither recorded that the kindness
   had a scope. Nobody edits these into being wrong -- the world moves and the
   allowance stays, which is why they survive review. The smell is leniency with
   an expiry date nobody set; the fix is the testable-condition rule.

check-citations gains `--for-merge`, which turns the peer class into a failure.
A flag rather than a new default because BOTH readings are still live: mid-work
on a topic branch the peer class really is unfixable noise. What the old code
could not express is where the code is GOING, and that is a condition the caller
can state. Measured on this tree: 19 citations resolve only on a peer branch --
which is the size of the #7-depends-on-#8 edge, not the 2 I had counted in
boot.gd.

The selftest gains that third class, because a flag whose classification is
unexercised is the shape this rule exists to catch. Controlled: emptying
PEER_REFS makes the peer case collapse into "nowhere" and the selftest reports
🔴 BROKEN, rc=2.

⚠️ Pre-existing and NOT from this change: the default run already exits 1 on 4
citations of `export/...` paths. Those are the generated tree, gitignored by
design, and main's copy of the tool fails identically. The CITE regex treats
`export/` as a repo prefix. Reported, not fixed -- it is the port's file and its
call whether the regex or the citations are wrong.
2026-09-04 18:27:39 +02:00
MechaCat02
fcf6ec0497 docs: the status block said nothing existed while nine issues were live
Phases 1-4 and 6 are done on the instance. This file still opened with "Nothing
exists on the instance: no agent users, no API tokens, no labels, no milestones,
no branch protection" -- every clause of which was false by the time the merge
that carried it landed.

Replaced with a table of measured state, and each row says what was MEASURED
rather than what was run:

  * protection is verified behaviourally -- a real push to main refused with
    `pre-receive hook declined`, as the repository owner -- not read off a
    settings page. That distinction is the whole subject of this file.
  * the tokens are probed: right identity, 403 on branch_protections for both
    agents, so the Write-not-Admin carve-out is demonstrated and not asserted.
  * the labels are 11 because the instance holds 11.

And a standing note that this block is the part most likely to be wrong, with
what to believe instead: `gitea-protect --verify` and the issue list MEASURE,
this block REMEMBERS. A remembered status is a cache with no invalidation, which
is the same failure as a 1,227-line BLOCKED.md and as the two documents this
runbook was split across an hour ago.
2026-09-04 17:55:28 +02:00
MechaCat02
ce9fc6eea6 Merge branch 'pi/gate-limit' into agents/gitea-mcp 2026-09-04 17:55:00 +02:00
MechaCat02
9aeeb8c574 docs: the tool creates 11 labels, not 12 -- I counted its own definition
Caught by the Pi agent against the live instance after Phase 4 ran. The tool
creates 5 state/*, 2 agent/*, 4 kind/* = 11.

Where the 12 came from is worth a line, because it is a shape that recurs:

    $ grep -c '^mklabel' tools/gitea-setup
    12
    $ grep -n '^mklabel' tools/gitea-setup | grep -v ':mklabel "'
    74:mklabel() { # name colour description

I counted the function DEFINITION as a call. A measurement taken one token away
from the thing being measured -- the same shape as reading protection off a
settings page and reachability off a DNS record, which is now three today. The
version that cannot make this mistake is counting what the instance holds, and
that is what found it.
2026-09-04 17:50:50 +02:00
b67b6243e6 agents: name what branch protection does not gate, and stop the tool contradicting it
Two things that read as protection while being none.

Phase 2's rule binds everyone who reaches Gitea through the API or the web, and
does not bind anyone with `gitea admin` in the container -- which includes the
supervising agent that created the agent accounts and minted their tokens. From
that shell the rule is editable and an admin token is one command away. That is
the boundary of what the phase buys, not a hole to plug there, and the document
read as though the gate were universal. Phases 1 and 2 gate the two CONTAINERISED
agents, whose design assumption is that policy lives where they cannot reach it;
a supervisor with a host shell is not in that set.

And `gitea-setup` finished by telling the reader to go and build a Gitea project
board by hand, four sections after the doc explains that a board is a second copy
of the state to hand-sync and is precisely the failure that produced a 1,227-line
BLOCKED.md. A tool instructing you to do the thing its own documentation argues
against is the drift this whole surface exists to end.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
2026-09-04 17:48:58 +02:00
MechaCat02
184ca0b556 docs: fold the page's revisions into the file, and separate wrong from unchecked
The runbook existed as two documents -- a published page and this file -- with
no mechanism keeping them equal, only an intention to remember. Two versions was
the predicted outcome of that, not an accident on top of it. This is the fold,
and the rule that follows it: THIS FILE IS THE SOURCE, the page is derived from
it. When something is urgent enough to push to the page first, it lands here in
the same turn, not "shortly after".

Four things the file did not carry:

  * YOUR OWN PUSHES TO main STOP. `enable_push: false` compiles to CanUserPush,
    which returns false with no bypass for admins or the owner -- quoted from
    the source. Three commits went in by direct push the day this was written,
    so the first notice would have been mid-task. Now a check step.
  * the token files' MACHINES, which the table had lost.
  * do NOT add `write:repository` to the `fabi` token. That scope IS a push
    credential. Written down because that advice was given, in chat, by the
    author of this file.
  * Gitea 1.25.5 confirmed from the desktop too, not just the Pi.

And one thing deliberately NOT folded in: the page said the desktop's outbound
HTTP was blocked, and that is false. `python3 -c 'urllib...'` returns
200 {"version":"1.25.5"} from this box. What is refused here is `curl`, by a
local permission prompt -- which I read as a network constraint and then
published as one. The Phase 3 locations stand; the reason given for them did not.

The "not verified" section now separates WRONG from UNCHECKED. Four entries are
wrong -- requiring an approval does not close the gate, the check could not have
caught that, the token scope, the reachability -- and the pattern in all four is
identical: a property inferred from something ADJACENT to it (protection from a
settings page, reachability from a DNS record) instead of tested directly. That
is the frozen-splash failure, committed in the document about avoiding it. The
first two were caught by the other agent, which is the argument for the review
gate this file exists to build.
2026-09-04 17:18:46 +02:00
799fa93383 tools: apply and re-check the branch protection rule, rather than clicking it
Phase 2 as a file. Six settings where two are load-bearing and both were missing
from the first draft is the shape of thing that gets mis-clicked at 1am, so it
goes through the API: what was applied is readable in a diff, and `--verify`
can re-check it later instead of it being checked once.

--verify states its expectations INDEPENDENTLY of what the apply path sends.
A check derived from "whatever we posted" cannot fail -- it re-derives the
expectation from the thing under test, which is the same instrument-shaped
failure as a check that passes on an instance with no rule at all.

It also asserts both agents are still Write and not Admin, because an agent
promoted to Admin can edit the rule and then merge, so a green rule proves
nothing on its own. That is the `gitea-verify` card from "Still to build";
what is left of it is only putting it on a timer.

`block_admin_merge_override` stays false on purpose, and the reasoning is in
the file: approvals are whitelisted to `fabi`, and Gitea will not let `fabi`
approve a `fabi` PR -- so with the override blocked, a human-authored PR could
never reach one approval and could never merge at all. The override is not a
hole in the agent gate because the agents are Write, not Admin. Phase 1.2 pays
for that; this is where it is spent.

Reads the repository-scoped credential that already exists on the agent box
(~/.sylph-git-credentials) rather than the issue-only ~/.sylph-gitea-api-token,
which every branch-protection endpoint refuses. That keeps the setup needing no
new credential, and keeps push rights on one machine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
2026-09-04 16:49:58 +02:00
eccb789c0b docker: give each agent its own Gitea hands, and close the cross-approval hole
Phase 5 of docs/agents/GITEA-SETUP.md, plus a correction to Phase 2 that the
runbook could not have known it needed.

gitea-mcp v1.7.0 goes into both images, pinned by the sha256 the release
publishes and smoke-tested with `--version` at build time, so a bad pin fails
the build instead of the agent. Each entrypoint registers it at user scope for
that container's own identity, remove-then-add so a restart is idempotent.

The token is passed BY PATH. `-e GITEA_ACCESS_TOKEN=$(cat …)` would write it in
cleartext into ~/.claude.json, which every session in the container reads;
GITEA_ACCESS_TOKEN_FILE is new in the pinned version and leaves the secret in
its read-only mount. Verified against the binary's own --help, not assumed.

The tool filter stops being an experiment. The names are in the release README:
each agent gets issues, notifications, labels, milestones and pull requests, and
NOT `pull_request_review_write`. That one matters because separate identities
open a hole the runbook did not name: Gitea refuses to let an author approve
their own pull request, and does nothing about sylph-decoder approving
sylph-port's. Two agents could satisfy `required_approvals = 1` between
themselves and then merge, since branch protection blocks pushes to main and
never blocked merges.

Withholding the tool is defence in depth; the controls are in branch protection,
and both docs now say so: approvals whitelisted to the human so an agent's
approval does not count, merges whitelisted to the human so an approved PR is
still merged by a person. Phase 2's check gains the step that actually tests it
-- approve the throwaway PR yourself, then confirm the agent STILL has no merge
button. Without that step, the check passes on an instance where the agents can
merge each other's work.

Also settles two entries on the runbook's own "not verified" list: the tool
filter names, and the Gitea version (1.25.5, whose API schema carries
enable_merge_whitelist and enable_approvals_whitelist under those names).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
2026-09-04 16:39:12 +02:00
MechaCat02
9652a5ad77 agents: the ordered runbook for standing the Gitea surface up
WORKFLOW-gitea.md said what the working surface is and why. It did not say how,
in what order, or how to know a step worked -- so it was a destination with no
route. This is the route.

Seven phases, each with a check, each marked 👤 human or 🤖 me:

  1 identities   two agent users, Write NOT Admin
  2 protection   main behind a PR + 1 approval -- BEFORE tokens exist
  3 tokens       three principals, three tokens, three files
  4 structure    labels and bundles, and deliberately NO Kanban board
  5 MCP          gitea-mcp v1.7.0, per-agent identity, user scope not .mcp.json
  6 items        migrate the live findings only -- not 1,227 historical lines
  7 restart      and verify the three things that must be true

Phase 1 leads because it is not hygiene: Gitea does not let a PR's author
approve it, so while an agent IS `fabi` either the human cannot approve its work
or it can approve its own. The review gate does not exist until the agents are
distinct people. (It also fixes 495 commits of agent work attributed to the
human's email.)

Phase 2's check is a real push and a real PR, not a reading of the settings
page. The reason protection lives in the server rather than in a brief is that
it should not depend on good behaviour -- so verifying it should not either.

Phase 5's install facts are checked, not remembered: gitea-mcp v1.7.0,
`gitea-mcp_Linux_x86_64.tar.gz`, `-t stdio -H <host>`, `GITEA_ACCESS_TOKEN`.
The `--tools` filter is flagged as an EXPERIMENT that might exclude the merge
tool as defence in depth -- explicitly not a substitute for phase 2.

Ends with what is still to build (propose-work, an attachment uploader,
gitea-verify, the wiki landing page) and a "what I have not verified" section:
the approve-your-own-PR behaviour, the --tools names, the Projects API, and the
Gitea version -- the API was unreachable from my sandbox three times running.
2026-09-04 16:25:10 +02:00
MechaCat02
a1ac3fa4c1 agents: rewrite the briefs for the Gitea workflow
The two loop files ARE the prompts -- `sylph-port` and `sylph-decoder` read them
off the host at launch -- so the workflow change had to land here or it would
not reach the agents at all.

PROTOCOL.md gains four sections:

  * Work items -- issues, milestones as bundles, the state labels, and that
    `state/blocked` uses DEPENDENCY EDGES, never prose. A prose blocker is what
    let a 1,227-line BLOCKED.md go stale.
  * Messages -- an ask is a `kind/ask` issue, not a SendMessage. With the part
    that matters: 🔴 NOTHING PUSHES. Notifications are polled, at the top of
    every iteration, and therefore an agent must NEVER wait on an ask -- set the
    edge, take the next item. The channel this replaces dropped 21 consecutive
    messages to a stale session id and reported success each time.
  * Pull requests -- one item per branch, `Closes #N`, and you may not merge
    your own. Branch protection enforces it; the rule is written down so the
    agent knows it, not so it depends on the agent.
  * Each iteration, in order -- notifications, sync, one unit, hand over, stop.

Also: evidence a human must look at now attaches to its issue, and a blunt
"never commit game content, under any directory name" with the 545 MB that
prompted it.

The two briefs shrink 697 -> 298 lines. They had accreted five dated focus
blocks between them -- sole-focus orders, F1-F6 queues, one-off "merge this
branch on your first iteration" instructions -- which is a queue, and a queue
belongs in the tracker. What is KEPT is what outlives its bug:

  * ask of any check, what would this still report if the feature were absent?
    Three instruments passed a splash that never animated.
  * the instrument must sit at or above the thing that can break -- the
    InputEventAction / input-map miss.
  * R1, and grep REFUTED.md before proposing.
  * the .pe is primary and the database is somebody's analysis of it.
  * the oracle is the real game in Canary, not any renderer of ours.

⚠️ NOT YET TRUE when this lands: the agents have no Gitea users, no API tokens
and no MCP server, so the issue tooling these briefs assume does not exist yet.
The agents are stopped. Setting that up is the prerequisite for restarting them.
2026-09-04 16:20:49 +02:00
MechaCat02
a23c321831 port: land the play-tested work, and only that
Takes the port branch up to 77320d5e -- the state the human play-tested on
2026-09-02 -- for SOURCE paths only. Not a branch merge: `auto/port-p6-audio`
is 366 commits and 938 files, and most of that must not land.

WHAT COMES IN (76 files, all human-confirmed working):
  * the logo splash animation. 08ed3dd1 found it: `pose_at` ASSIGNED the settle
    instant instead of clamping to it, so the splash never animated at all --
    and the same bug manufactured a passing harness result, because the harness
    photographed t past the settle. Confirmed by play-test: "cannot notice any
    obvious difference from the actual game."
  * gamepad input -- (A)/(B) bound additively (`ui_accept` ships with NO joypad
    binding), stick latched with hysteresis at the game's own 61% digitise
    threshold. This is what made (A), video-skip and Extras work at all.
  * menu navigation and flow, menu audio, the exporter, the authored
    declarations, and 23 verification tools under tools/port/.

WHAT IS DELIBERATELY LEFT ON THE BRANCH:
  * everything after c0ae460a -- the F5/F6 title-timing investigation, whose own
    tip commit calls itself a "hand-off for one-minute human checks". Unchecked
    by definition; it goes through the new review gate like anything else.
  * the OPTIONS menu work of 2026-09-03. Real, probably good, NOT play-tested.
  * the F1 repeat mechanism, which its own commit calls "deliberately inert".

WHAT MUST NOT LAND, AND WHY THE .gitignore CHANGED:
  545 MB of extracted game content was committed on that branch -- 850 sprite,
  audio and transcoded video files under `export-probe/` and `export-probe2/`,
  plus 246 MB of loose .wav and .tsv at the repo root. This repository's own
  rule, in this file, is "never game content".

  The rule was not missing. It was written, and it was tightened on that very
  branch, with a careful comment explaining why BOTH `export/` and `data/base/`
  had to be listed -- while the exporter was writing to a third name that
  nobody had thought to list. Enumerating names is the thing that failed. So
  the ignore rules now describe the SHAPE: any top-level `export*/`, game media
  by extension, and loose capture output at the root. Verified both ways -- it
  catches all four offenders and ignores nothing currently tracked.

Verified: `cargo check --workspace` clean; all nine GDScript files parse in
project context, with a positive control (an injected syntax error is detected,
3 lines) so the clean result means something. `tools/port/check-all` was NOT
run -- it needs the container, the export tree and a display.
2026-09-04 16:17:14 +02:00
MechaCat02
ad96fe97b8 agents: move the working surface to Gitea -- issues, PRs, and where files live
The human wants to direct this project from a web UI rather than chat or Remote
Control, so Gitea becomes the working surface. No new store: adding a second
copy of the truth is this project's defining failure mode, and Gitea already
holds the code. Its first-party MCP server (gitea/gitea-mcp v1.7.0, checked) has
issues, labels, milestones, PRs, attachments and notifications.

ISSUES replace BLOCKED.md. Milestones are bundles the human defines; issues are
items agents propose and the human approves. The state labels end in
`needs-human`, which is the state the whole model turns on and the one no
off-the-shelf tool models -- the market has converged on removing the human.
`blocked` uses Gitea's DEPENDENCY EDGES rather than prose, so "the Port is
blocked on the Decoder answering X" becomes queryable and closes itself.

PULL REQUESTS, the human's proposal, adopted -- and a bigger improvement than it
looks. Today's long-lived auto/* branches have drifted 280 and 373 commits apart,
which is unreviewable by construction. One PR per item makes the human gate
NATIVE rather than a label convention, binds the change to its item, and enforces
the sizing rule: an item too big to review in one sitting was too big to be an
item.

🔴 Agents must not merge their own PRs, and pull_request_write includes merge --
so this goes in BRANCH PROTECTION on main, not in a document asking them not to.
Same principle that fixed the build-jobs cap: policy where the agent cannot reach
it.

WIKI -- the human suggested it for RE findings, and that half is declined with
reasons. A finding's value is that it sits beside its evidence, versioned with
the code that consumes it; the wiki is a separate git repo, so a decode
correction and the exporter change depending on it could never be one reviewable
PR. And wiki edits bypass review: the REFUTED.md reclassification changed the
file both agents read to decide what not to try, and as a wiki edit it would have
been an unreviewed mutation of shared ground truth. The wiki takes human-facing
orientation instead -- runbook, navigation, container notes, and a landing page,
which closes the real gap that there is no view of what is happening except
container logs.

FILES: three needs, three homes. Agent-to-agent transient stays in /exchange.
Evidence a HUMAN must look at attaches to the issue it belongs to -- it travels
with the item and cannot be orphaned from the claim. Evidence a finding cites
stays in git. Note the MCP exposes attachment_read only; upload needs a direct
REST call.

tools/gitea-setup creates the labels and bundles, idempotently, with --dry-run.
Blocked on a token with write:issue -- the push credential is write:repository
and every issue endpoint refuses it, checked rather than assumed.
2026-09-04 15:46:57 +02:00
MechaCat02
620ec5e60b agents: one item only -- the title's animation timing -- and split work into human-checkable units
Two new findings from the human, both about WHEN a title animation starts, and
both handed over rather than guessed:

F5 Does (A) SNAP the title to finished, or ACCELERATE it? The human says they
   cannot tell and is right that they cannot -- a three-frame acceleration and a
   one-frame cut look identical to an eye. Two routes that should agree: a
   per-frame capture (acceleration shows intermediate alphas, a cut shows none)
   and the code (assigning a target time and raising a rate multiplier are
   different instructions). Their "looks more like a snap on multiple attempts"
   is recorded as a PRIOR, not a result.

F6 The title's sweeping white glow -- ptloop01/ptloop02, the blue PCB-like lines
   -- starts only when the plate appears in the real game, and starts earlier in
   the port. A lead from the exported declaration, mine and unverified: those
   elements are keyed at t = 0, 70, 100, 238, 250 while the plate reaches full
   alpha at 236, with pteff02 keyed at exactly 236 and ptlogo_back2eff and
   ptcopyright at 238. 236-238 is a synchronisation point in the declared data
   and a human just reported a behaviour change there. Flagged AGAINST itself
   too: 238...250 looks equally like an exit ramp -- ptcopyright uses that shape
   and starts nothing -- and the sweep lives in a nested .rat leaf with its own
   timeline.

F6 bears on clock: "shared" and on F4: if a title element does not move until
the plate arrives, either the declared data says so and our keyframe reading is
wrong, or something at the plate's arrival STARTS it, which is a mechanism
nobody has proposed.

And the process change, which is the human's and outlives this item:

  "attacking the 'whole' mission was too big for them to handle. Split the given
   missions and tasks into even smaller tasks which they can tackle and give to
   a human for feedback."

PROTOCOL.md gains "Work in units a human can check in a minute". A milestone is
not a unit of work, it is a bag of them. A unit is right-sized when it ends in
something a person can judge in under a minute WITHOUT READING ANYTHING, and
each one states its question, what the human looks at, and what it does NOT
cover. Do one, hand it over, stop -- an unverified fix under a second change
makes a regression two-variable.

The evidence for the rule is this week: the splash sat through a whole milestone
and took one day once scoped to "does it animate?". The bar is a HUMAN check,
not a green tool -- three instruments passed a frozen screen.
2026-09-02 20:14:46 +02:00
MechaCat02
de5f04038d agents: correct "both clocks" -- there is ONE, and F4 tests whether it is right
I wrote "whether the game snaps both clocks forward" into yesterday's F4 and the
human asked which clocks. There are none: authored/flow.json sets
`clock: "shared"`, so the title's two composited builds -- build 4 the artwork
(finishes t~=118) and build 2/3 the plate (full alpha t=236) -- run on ONE clock
started together. Left standing, that phrasing sends an agent hunting for a
second clock this corpus says does not exist.

Corrected in both briefs and in the playtest page, marked as a correction rather
than silently edited.

And the question is better than I first framed it. `clock: "shared"` is
AUTHORED, and the port's own plate-arrival-halves.md calls it "not falsified...
not confirmed to better than ~20 % either", with an unresolved anchor
disagreement inside one binary: the reconciliation picked t=118 while
settle_time() returns 160 and the boot prints "settles at t=160".

So F4 is a TEST OF THAT PREMISE, and the discriminator is observable -- press (A)
early, while the wordmark is still building in, and watch the ARTWORK rather
than the plate:

  advances the shared clock   -> the artwork SNAPS to finished
  only forces the plate       -> the artwork KEEPS ANIMATING its build-in

Both briefs now say to answer F4 before building on `shared`, and tell the port
not to choose what "jump" means.
2026-09-02 18:39:13 +02:00
MechaCat02
06f890361b agents: P5's gate is MET, and four findings from the same walk
"Menu walk and navigation is fine. Video skips too. Extras open. New Game
   shows new game intro video."  -- 2026-09-02

P5 is done. Its gate was "a human clicks through it", the retro said it had been
waiting on that and not on code for the whole milestone, and it has happened.
PORT-MISSION.md updated. The NEW GAME gap is accepted as-is.

Four findings, three of them the Decoder's:

F1 THE MENU REPEATS ON A HELD DIRECTION AND OURS DOES NOT. One step per
   deflection was authored as the conservative choice because nobody knew; a
   human has now watched the real game and it repeats, "at a medium pace... slow
   enough to see which item is selected". That settles the existence half of H1
   against us. The RATE is still unmeasured and must not be guessed -- the
   description bounds it and supplies no number. Decoder measures initial delay
   and repeat interval as frame counts; the port implements the mechanism and
   waits for the numbers.

F2 THE SFX ARE TOO LOUD BECAUSE THERE IS NO MIX AT ALL. Measured: confirm
   -17.7 dB mean / -0.0 dB peak, 3 dB hotter in mean than the music and 6.4 dB
   above move. No volume or gain value exists anywhere in export/ or authored/,
   so every clip plays at unity on one bus. Decoder: is per-cue or per-bus gain
   on the disc -- the cue table is the obvious place and cue 1103 is already
   decoded. Port: gains at PLAYBACK as data, and explicitly NOT normalisation in
   the exporter, which destroys the relationship between clips and cannot be
   undone by a modder.

F3 SOMETHING IS MISSING ON THE TITLE SCREEN. The export carries one music file
   and the port plays nothing on the title. Which cue does the title play, and
   is there a sting on the plate or on accept? A negative needs a positive
   control: find the menu's cue by the same method first.

F4 (A) SKIPS FORWARD THROUGH THE BOOT AND WE IMPLEMENT TWO OF THREE PRESSES.
   In the game: skip video, reveal plate immediately, accept plate. The middle
   one is missing here. Whether the game snaps both clocks forward or only
   reveals the plate is a question, not a detail -- and it is a cheap second
   route to the plate-arrival question, since a press that skips to the plate
   says where the game thinks the plate belongs.

H3, the plate delay, is ACCEPTED -- "feels the same... sufficient". Left
unattributed rather than closed green.
2026-09-02 18:32:38 +02:00
MechaCat02
a71dea9b8d agents: the logo splashes are DONE -- the human cannot tell them from the game
"Looks good! Cannot notice any obvious difference from the actual game.
   Mark logos as done."  -- 2026-09-02

Not "the check passes": a person compared the port against the real game and
could not tell them apart. That is the oracle, and it is the strongest result
this port has produced. The sole-focus order is lifted; both agents return to
their milestones.

The fix was one word -- pose_at ASSIGNED the settle instant instead of clamping
to it, so every query returned the settled pose whatever the clock said. The
same line manufactured the false green: the capture harness shoots after two
frames, so it was photographing t~=2 units, which looked settled only because
everything looked settled. The 0.01 % agreement that closed H2 was measured
through the accident. One bug produced the defect AND the evidence of its
absence.

Verified here before it went to the human, by film rather than by claim:
motion 16.4 % -> 27.7 %, distinct luma states 26 -> 43, the publisher ramp 6
steps -> 13 in one continuous run, and the developer splash's interrupting
0.50 s freeze gone. The publisher trajectory rises to a peak and settles back --
the crossfade signature.

The port then closed a gap motion-census names in its own header ("a wrong ramp
that moves every frame passes here") with a shape check pre-registered from the
disc, measured off a film, on a non-overlapped strip, in ratios so the texture
divides out: rise:last declared 1.20, measured 1.20 exact.

Kept as the standing lesson, because it is the fourth instance: an instrument
that sits below the thing under test cannot see it fail. Ask of any new check
what it would still report if the feature were entirely absent.

Explicitly NOT claimed: P5's gate is "a human clicks through it" and nobody has
said the milestone is met. The briefs say so, and say not to record it on the
human's behalf.

The decoder's end-to-end pipeline work returns to normal priority rather than
being dropped -- it is what decides whether the port's 60 units/s matches the
game. The ramp is now right in SHAPE and unverified in DURATION.
2026-09-02 18:15:03 +02:00
MechaCat02
d394c55cbb agents: the splash does not animate, and three instruments could not see it
A human on a GPU at ~140 fps: "the logos just switch, there is no animation at
all." Measured from a real boot with --film at 0.05 s, then per-frame change:

  splash moves        1.30 s of 7.95 s = 16.4 %
  publisher splash    0.30 s of motion, then 3.20 s FROZEN
  developer splash    0.35 s + 0.25 s, then 2.40 s FROZEN
  distinct luma states in 7.95 s   26

A 45-unit build-in cannot be drawn in 26 states, and a fade does not hold one
picture for 3.20 s. The frame counter says 24.8 fps achieved; both are true --
the port is DRAWING 25 times a second and CHANGING almost never.

🔴 Why every check passed, which matters more than the bug:

  frozen sweep     drives the clock BY HAND -- proves the renderer can draw
                   pose N, never that the poses are drawn in sequence
  settled compare  0.01 % against the capture -- a screen frozen 84 % of the
                   time matches a settled reference PERFECTLY, that is what
                   frozen means
  achieved fps     counts frames DRAWN -- the same pixels 25x/s scores
                   identically to animating

Every one measured throughput or a pose. None measured CHANGE. Same shape as
InputEventAction bypassing the input map: the instrument sat below the thing
that was broken, so the break could not appear in it.

tools/motion-census closes the class. It measures change and nothing else, and
its --selftest asserts it separates a fade (97.4 % moving) from a switch (2.6 %)
from a frozen film (0.0 %) -- a detector that cannot tell those apart would
report the same green line on all three.

Both briefs: this is the SOLE focus. The port reproduces before changing
anything and gates every fix on a film rather than a still. The decoder maps the
whole pipeline end to end -- disc bytes, the game's per-frame update (does it
interpolate between keyframes or hold?), what is submitted per frame, and what
Canary does to it before a capture records it -- delivered as a SERIES, not a
settled value.

The port should also record the refutation against itself: H2 reads ANSWERED on
the strength of the frozen sweep. The mechanism half stands, the blur is a baked
companion texture. The behaviour half does not.
2026-09-02 17:24:42 +02:00
MechaCat02
79783ff9ee agents: point each brief at its human branch, to merge on the first iteration
Both are pushed. The decoder's carries the R1 register reclassification and
tools/stale-instrument; the port's carries the two input fixes, verify-input
and BLOCKED H1-H3. Each branches from that agent's own tip, so it is a
fast-forward on the line they are already on -- and the port must merge before
touching input or it will re-derive a fix that is already asserted.
2026-09-01 17:59:59 +02:00
MechaCat02
1ad519d3ba agents: the splashes exactly, and stop photographing a moving thing
A human played the port on real hardware for the first time (2026-09-01) and
found four things. Two were port defects, fixed. Two are open and are now both
agents' focus: the PRESS (A) plate arrives late, and the splash fade/blur is
weaker than the game's.

Their verdict on method is the reason this is a brief change and not a ticket:

  "the agents were essentially guessing and trying to copy what one would see,
   but while they did get close it still is not quite right"

Close-but-not-right is the signature of reproducing APPEARANCE instead of
deriving MECHANISM. So the Decoder's focus block asks, in order: is there a
post-process pass at all, what is it, where do its parameters come from -- and
only then what curve. Both routes, dynamic (GPU state, shader constants, render
targets; add logging to Canary, it is theirs read-write) and static (.pe, the
DB, the paks), with each fact labelled by which produced it.

TEMPORAL-VERIFICATION.md is the other half, and it generalises past the
splashes. We have been photographing the game at time t, and t is never the
same twice: emulator speed varies with host load, Canary presents at ~28.1 fps,
the capture path costs a variable 0.1-10.8 s, and a long-lived x11grab stream
degrades and then freezes. The register already carries FOUR refutations of
exactly this shape. The replacement rule: record a film, not a photograph;
align by CONTENT, not by clock, and report the lag as a measurement rather than
minimising it away; prefer ordering, counts, durations and shape over any value
at a wall-clock instant; anchor on an event; report achieved fps against
requested fps.

Also into both briefs: the input set. The port had no joypad binding for (A) or
(B) and nobody noticed for a whole milestone, because --script sends
InputEventAction, which BYPASSES the input map -- so every check asserted the
code below the map and nothing about the map. The Decoder is asked to DECODE
the full set the game reads rather than discover it by pressing buttons; the
Port is told input is verified at the device level or not at all.

And both briefs now point at the R1 register reclassification, because two of
the ten re-opened entries land on this focus: "the declared keyframe timeline
reproduces the captured splash" is 🟡 our-reader, and the rest() pair is open
in BOTH directions -- while the two splashes are the only screens that reach
that fallback.
2026-09-01 17:59:17 +02:00
MechaCat02
f5315ddf59 decoder: tell it about the reference assets, and that the DB can be wrong
The mounts landed but the agent could not learn of them: I documented them in
CONTAINER-NOTES.md, which the decoder's prompt does not list, and then restarted
the container -- so a fresh session with no memory of the exchange had a 586 MB
database and a decompressed image sitting unmentioned in its filesystem.

Now in the PROMPT itself, not only in a document, because the prompt is the one
thing a new session is guaranteed to read. CONTAINER-NOTES.md is also added to
its reading list.

And the caveat that matters more than the asset. The .pe is PRIMARY -- the bytes
the console executed. The database is somebody's ANALYSIS of them, produced by a
disassembler that had to guess, and it is wrong in the ways disassemblers are
wrong: misdecoded mnemonics where data was read as code, function boundaries
short or long or merged or split, coverage missing entirely for code reached only
by indirect dispatch, and names that are derived rather than symbols.

So a finding resting on a database row is not established until the bytes agree:
read the same address out of the .pe and check. Where they disagree the image
wins, and the disagreement is itself worth recording, because it tells the next
reader which parts of the database to distrust.

A fast index into 9.2 MB of machine code, not a source of truth.
2026-08-29 15:23:47 +02:00
MechaCat02
d2d2592c92 decoder: mount the disassembly DB and the flat VA image
The decoder had neither, and reported the gap precisely: four scripts in this
repo READ /work/xenia-rs/sylpheed.db and nothing produces it, so the whole static
PPC route was consumers with the producer missing.

Both exist on the host and are now mounted read-only: the 586 MB database (25 481
functions, 851 classes with RTTI, EH tables, imports, 1.8M indirect-dispatch
candidates) and the decompressed image.

The image is the more useful of the two. It is a FLAT VA DUMP -- file offset =
VA - 0x82000000 -- so reading a known address needs no XEX decrypt, no LZX, and
no booted emulator. The decoder had independently recovered the same bytes by
dumping /dev/shm/xenia_memory_* and validating against the GamePart table, which
is good work and a sound method, but it noted itself that needing a running
emulator is a bad dependency for something the entire static corpus rests on. It
does not need one.

Also recorded that an earlier claim the .pe was STALE was tested and refuted, so
nobody re-litigates it, and that instructions.raw is an INT rather than hex.

Written down as reference material, explicitly NOT a deliverable: they are
read-only, they come from outside the repository, and a fresh checkout elsewhere
has neither. Reimplementing the producer belongs in sylpheed-formats, and until
it exists every static finding rests on an artefact this project cannot rebuild.
2026-08-29 15:17:52 +02:00
MechaCat02
7492b45095 agents: merge main at the start of every iteration
Both agents read the protocol, their mission and the shared tooling from their
OWN checkout, and both work on topic branches -- so without an explicit sync
they follow whichever version of the rules existed when the branch started.

Found concretely: tools/audio-capture and two protocol revisions were on main
while the decoder worked for hours from a branch that had neither. The port had
merged on its own initiative and did have them, which is exactly the kind of
divergence nobody notices until the two disagree about what the rules say.
2026-08-29 13:53:00 +02:00
MechaCat02
5a9b75e152 agents: they never spoke, the decoder lost the disc, and both shared one state dir
Three defects, all mine, found by checking instead of assuming.

**They never exchanged a word.** SendMessage=0, ListAgents=0 across both new
sessions. PROTOCOL.md specified in detail what a message MAY and MAY NOT do and
never said how to send one or that the other agent was addressable -- they knew
that last time only because the human told them directly, and rebuilding with
fresh volumes wiped it. Policy without mechanism is prose. Now documented with
the two addresses, a worked example, and an instruction to introduce themselves
on the first iteration rather than waiting to have a question.

**The decoder lost the disc and the ISO.** They used to arrive inside the project
mount and silently stopped when /work became a clone. Silently is the word: the
disc-gated tests SELF-SKIP without SYLPHEED_DISC and report green, so a whole
test suite would have passed while measuring nothing. Both are now mounted
explicitly, the ISO at a stable path so run-canary does not depend on host
directory names.

**Both agents shared one Claude state directory.** They share the host's
~/.claude, and once both working directories became /work they resolved to the
same projects/-work/ -- two supposedly independent agents writing to one place,
which undoes the point of separate checkouts. Each now has its own volume, seeded
once from the host with credentials only, so a token refresh writes locally and
neither can corrupt the host's auth.

Also widened the pacing rule. It banned ScheduleWakeup by name; the decoder then
scheduled itself an hourly cron job -- not harmful, but the same instinct that
ended a run yesterday, through a door I had left open. Now: no self-scheduling by
any route.

Mount audit after the changes: shared and intentional are the exchange volume and
the read-only credential seed. Everything else -- repo, Claude state, cargo,
target, canary, disc, ISO -- is per agent or one-sided.
2026-08-29 13:05:41 +02:00
MechaCat02
c58196b795 containers: each agent clones the monorepo into its own volume
The last structural fix for the collision class that has bitten three times. Both
containers now clone the repository into their OWN named volume instead of
bind-mounting a human's working tree, so an agent's local git config cannot
capture a human's commits, a credential helper cannot leak a container-only path
onto the host, and a `git add -A` cannot sweep another party's in-flight files.

Cloned once at startup and never auto-pulled: pulling under a running agent
moves files out from under whatever it is mid-edit, which is the same bug again.

Accepted knowingly: Claude Code keys per-project memory off the working
directory, so moving off the host path starts that memory empty. The corpus in
docs/ is the memory that matters and it travels with the clone.

Other changes:
* docker/agent -> docker/decoder; the launcher is sylph-decoder. Roles, not
  "the agent", now that there is more than one.
* /reborn is gone -- one repository now, so the port reads HANDOFF from its own
  checkout rather than through a live read-only mount of someone else's tree.
* Canary mounts separately at /canary; it stays a fork tracking upstream.
* A shared `sylpheed-exchange` volume at /exchange, with tools/ on PATH so
  `share` is available in both.
* The decoder's credential file gets the .host-copy treatment the port already
  had -- `credential.helper=store` rewrites by rename-over-target, which is
  EBUSY on a bind mount and reports a fatal that is not one.
* Budget split deliberately: decoder 5 cpu / 6 GB, port 3 / 4, leaving room for
  the planned Referee. "Half the host" was right when there was one agent.

Prompts move to docs/agents/ and are rewritten around the protocol: the oracle
is the running game, dynamic RE stays with the decoder, each iteration must
attempt to refute one claim of the other, and neither may verify its way out of
its own role.
2026-08-29 11:48:30 +02:00
MechaCat02
c27bdee021 agents: the team protocol, the share tool, and a player's-eye navigation doc
**navigation.md rewritten from the player's chair.** It was written from the
inside out -- GamePart ids, pak names, sprite names -- which is how WE find
things, not what the game shows anyone. Now it describes what is on screen, what
you press and what happens, with internals as footnotes. Most rows are open on
purpose: it exists to be filled in by playing, and the in-game tutorials are the
resource for the flight half.

**tools/share** gives transient files provenance without giving them history.
Three kinds of thing were travelling down one channel with opposite needs: code
and decoded knowledge want permanence, cited evidence wants permanence, and
"look at this PNG" wants no history at all. The third kind bloats a repository
forever; passing it by message is worse, because the receiver gets bytes with no
idea which build produced them. `share put` records who, when, what, the sender's
commit, and whether their tree was dirty -- because a capture taken from a
modified tree cannot be reproduced from the sha, and the receiver deserves to
know that before building an argument on it.

**docs/agents/PROTOCOL.md** is the contract. The parts that matter:

Dynamic RE stays with the Decoder -- most of what is open is behavioural and
cannot be answered from the file. What the planned Referee adds is different:
bias enters at what you CHOOSE to capture, so a corpus captured to a fixed
protocol by someone with no hypothesis is worth more than one captured to settle
an argument.

A message may point, ask, prioritise and challenge. It may not change scope,
redefine ground truth, or carry a finding instead of writing it down -- including
a message claiming to relay the human, because a relayed instruction has no
evidence attached and this project has watched a wrong belief travel further and
faster than its correction.

Adversarial duty is explicit: every iteration, try to refute one claim of another
agent and record the attempt either way. Run your own instrument through a
control first. Disagreements go to the human with both positions, not to
whoever is more certain.

And no agent may verify its way out of its own role: the Port has no oracle, the
Decoder builds nothing, the Referee interprets nothing.
2026-08-29 11:42:29 +02:00