Skip to content

feat(playtest): /move reports whether the jump happened, and what it fell off - #25

Merged
ksdisch merged 4 commits into
mainfrom
feat/jump-outcome-telemetry
Aug 14, 2026
Merged

feat(playtest): /move reports whether the jump happened, and what it fell off#25
ksdisch merged 4 commits into
mainfrom
feat/jump-outcome-telemetry

Conversation

@ksdisch

@ksdisch ksdisch commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Backlog Active item 4 — the two follow-ups the planet-1 aim sweep produced. Both
are the same shape as diedAt in #21: the harness already knows the answer and
throws it away before the seat sees it.

What was wrong

jumped meant "the driver pressed jump", not "the astronaut left the ground".
The game grants a jump only from the ground, so a press issued when the astronaut
is already airborne — past a pit's edge, say — does nothing at all. The driver
polls at 60ms (14px at 240px/s), so a jumpAtX set within ~14px of planet-1's
fall edge is usually first seen after the astronaut has left the ledge. All eight
sweep trials at jumpAtX >= 646 reported jumped having never left the ground,
and pilot 4 (#23) burned a finding on the same ambiguity from the seat's side: its
run B could not tell an attempted jump from a no-jump, because both replies were
byte-identical. A seat cannot diagnose an aim it never got to test.

A death said where the fall ended, never what it fell off. Nineteen of the 48
bare-jump sweep trials landed on the bridge and then ran off its far edge —
correct aim, correct power, killed by a move that kept holding right after
touchdown. Every one reported the same thing as a jump that never came near the
bridge: respawned with a pit diedAt around {776, 600}.

What changed

/move now returns jump: {tookOff, pressedAt, apexY} whenever it actually
pressed, and pushes jumped only once the astronaut is observed to rise 8px above
the press point — jump-ignored otherwise. A press made while already climbing is
called inert immediately, since being airborne is precisely why the game refuses
it. apexY is the highest point reached after the press, which also separates a
clean jump from one that clipped a ceiling (362 vs 395 on planet-1).

A death adds lastStoodAt: {x, y} — the last spot the driver observed the
astronaut resting on a surface. It names the edge instead of bounding it from the
right, and separates the two outcomes by height alone: {~820, 429} is the
bridge, {~650, 476} is the ground at the pit's lip.

Both are inferred from sampled y, because the game exposes no contact flag
(BridgeState carries astronautY and nothing about what is under the feet) and
the harness stays zero-diff on constellation. Resting is two consecutive samples
at the same height that were arrived at from above
. The second clause is
load-bearing: near a jump's apex the astronaut is barely moving vertically, so two
samples there can round to the same y — but the sample before them is always lower
(larger y) because it was still climbing, while a real landing is approached from
higher up. Without it, a bare jump over the bridge would report "last stood on"
somewhere in mid-air.

Everything about the jump is gated on the sample belonging to the same life. The
game respawns in the frame it bumps respawnCount, and spawn sits 36px above
standing height, so a post-death sample reads exactly like a rise.

aim-sweep.sh records both fields per trial and cross-checks the driver's verdict
against the trajectory trace, which is polled from /state and owes nothing to
/move. The phone's world glance carries lastStoodAt too — "did they fall off
the bridge or never reach it" is the same question from the other side of the
couch.

Verification

Two bugs were caught by smoke-testing against the live game before the sweep:
apexY froze when the verdict resolved (reporting the take-off at 454 instead of
the peak at 362), and the post-respawn sample poisoned it, which could flip a jump
that never happened into a take-off. Both fixed; smoke re-run confirms:

trial events jump lastStoodAt
jumpAtX:598 bare jumped, respawned tookOff:true, pressedAt:{612,476}, apexY:362 {612,476}
jumpAtX:650 bare jump-ignored, respawned tookOff:false, pressedAt:{656,476}, apexY:476 {656,476}
jumpAtX:626 untilX:780 jumped, reached-x tookOff:true, apexY:362 absent (no death)

The full 68-trial aim-sweep.sh re-run is in flight; its cross-check against the
independent trace lands as a comment on this PR before merge.

npm run check passes (pre-commit hook, full run).

…fell off

Two blind spots the planet-1 aim sweep found, both harness-side. `/move` pushed
`jumped` the moment the driver set the jump input, which is a different claim
from "the astronaut left the ground": the game grants a jump only from the
ground, so a press issued while already airborne past a pit's edge does nothing.
All eight sweep trials at `jumpAtX` >= 646 reported `jumped` having never left
the ground, and pilot 4 spent a finding on the same ambiguity from the seat's
side. And a death carried only `diedAt`, so "landed on the bridge, then ran off
its far edge" and "never got near the bridge" were the same reply — 19 of 48
trials were the first and read as the second.

`/move` now returns `jump: {tookOff, pressedAt, apexY}` and pushes `jumped` only
once the astronaut is seen to rise 8px off the press point, `jump-ignored`
otherwise; a press made while already climbing is called inert at once, since
airborne is exactly why the game refuses it. A death adds `lastStoodAt` — the
last spot it was resting on a surface, which names the edge instead of bounding
it. Both are inferred from sampled y: `BridgeState` exposes no contact flag and
the harness stays zero-diff on constellation. Resting is two consecutive samples
at one height ARRIVED AT FROM ABOVE, the last clause being what keeps a jump's
apex from reading as a surface.

Everything about the jump is gated on the sample belonging to the same life. The
game respawns in the frame it bumps the count and spawn sits 36px ABOVE standing
height, so a post-death sample looks exactly like a rise — unguarded, it turned a
jump that never happened into a take-off. Caught in smoke-testing, along with an
`apexY` that froze when the verdict resolved and reported the take-off (454)
instead of the peak (362).

`aim-sweep.sh` now records both fields per trial and cross-checks the driver's
take-off verdict against the trajectory trace, which is polled from `/state` and
owes nothing to `/move`. `lastStoodAt` on the phone's world glance rides along,
since "did they fall off the bridge or never reach it" is the same question from
the other side of the couch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WcKTqMArCFofDVJJEJdtM2
@ksdisch

ksdisch commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

Sweep re-run — 68 trials against the new telemetry

./aim-sweep.sh re-run in full against constellation 604a422, log
logs/20260813-203716-aim-sweep.log. Every trial records /move's own
tookOff / lastStoodAt alongside the trajectory trace, which is polled
from /state and owes nothing to /move — so the two are independent and the
summary scores one against the other.

Take-off verdict: 68 trials judged, 68 agree, 0 disagree.

count trace says
/move reported jump-ignored (tookOff:false) 18 apexY 476 on every one — never left the ground
trace-derived no-jump count 18 exact match

The 18 land where the physics says they should: jumpAtX 638–654 in block A,
650 in block B, and block C's parks at 636+ — the band where the astronaut has
already crossed the fall edge by the time a 60ms poll first sees it. Before this
change every one of them reported jumped.

What it fell off:

trial group n lastStoodAt.y
trace saw a bridge rest 16 429 ×15, 430 ×1
trace saw no bridge rest 43 476 ×42, 477 ×1
no death (nothing to report) 9 field absent

No overlap, and every one of the 16 bridge-height rows is independently
confirmed as a bridge rest by the trace. Their lastStoodAt.x runs 828–840 —
the far lip of a bridge spanning 722–818, which is exactly the "landed, kept
holding right, walked off the end" death. Each of those previously reported as
an ordinary pit death: respawned plus a diedAt in the pit, indistinguishable
from a jump that never came near the bridge.

Counts differ from the original sweep, and that is measurement, not
regression.
AIM-SWEEP-2026-08-13.md recorded 8 no-jump and 19
landed-then-walked-off trials; this run measured 18 no-jump across all three
blocks (9 in block A alone) and 16 bridge-height deaths. These are freshly run
trials, not a re-scoring of the old ones — the driver's 60ms poll is 14px of
travel, so a jumpAtX near the edge lands on either side of it between runs,
and that report already flagged its 19 as "a floor, not a point estimate". What
reproduces exactly is the thing under test: the driver's verdict matches the
trace on all 68 trials, in both directions.

This comment is AI-generated.

ksdisch and others added 3 commits August 13, 2026 20:57
F1 (should-fix): AIM-SWEEP-2026-08-13.md pointed at a backlog Shipped entry that
did not exist, and the commit cited a 68-trial re-run that was still running.
The re-run has since completed; BACKLOG.md now carries the Shipped entry with its
measured numbers, and the doc points at the finished log.

F2 (should-fix): DESIGN.md and both seat prompts stated `jump` and `lastStoodAt`
as unconditional while the driver omits them — no `jump` when the requested x is
never reached, no `lastStoodAt` on an armed death or a move that never caught a
rest. Absence-as-signal is the house rule but only `diedAt` documented it; now
all three say what an absent field means.

F3: the rest detector's arrived-from-above guard was short-circuited by
`y2 == null`, true on every move's first sample, so a move begun within a pixel
of a jump's apex could report mid-air as a ledge. A missing predecessor is not
evidence of arriving from above — the first sample now records nothing and the
second picks it up.

F4: the flat-pair tolerance of 1px admitted the first sample of a fall, which
covers only ~1.6px in 60ms; that is the `430`/`477` rows in the re-run. Tightened
to an exact match, verified live: bridge landings still report 429 (836,429 and
828,429), no slip. The residual sampling bias is now stated next to the field the
way `diedAt`'s is.

F5: the post-loop take-off resolver could spend 240ms past a budget that
MOVE_HARD_CAP_MS documents as absolute. It is now bounded by what is left of the
move's own budget, and a press too late to judge reports `tookOff: null` with no
event rather than a guess. The verdict event can still follow a terminal one;
that is documented instead of silently true.

F6: the sweep's new tallies counted trials that never died as missing
`lastStoodAt` telemetry, and a verdict with no trace fell out of the accounting
entirely. Both are now named the way the UNCLASSIFIED line already was.

F7: the laptop prompt asserted a cause for `tookOff: false` the driver does not
establish. It reports an observation — no rise seen — which is nearly always
"already airborne" but also covers a jump cut short within a poll.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WcKTqMArCFofDVJJEJdtM2
…e-fix one

F8 (should-fix): round 1's fix pointed the backlog and the sweep report at
`logs/20260813-203716`, quoting its figures as the measurement of this feature —
but that log was produced by the code as it stood BEFORE the same commit
tightened the rest detector. Two quoted values, the `430` and `477` rows, are
exactly the 1px-slack artifacts the F4 fix removed, so the shipped driver cannot
emit them. Nothing in the tree exercised the code being landed.

Re-ran the full 68-trial sweep at this HEAD (`logs/20260813-210539`) and
repointed both documents at it. The headline holds: 68 of 68 take-off verdicts
agree with the independent trace, 0 disagree, none unresolved. The rest detector
now answers only 429 or 476 across 50 deaths, with no off-by-one values.

The strict test also costs something, and the same run measures it, so all three
documents now say so instead of claiming a clean sweep: 9 deaths report no
`lastStoodAt` — every one a block-C stage-two move that starts parked at the lip
and crosses the fall edge inside the first poll, where the loose test used to
fabricate a rest out of a 1px fall step — and 1 of 15 real bridge rests was
scored as the earlier ground rest. It under-reports; it does not invent.

F9: the no-jump comparison contrasted the re-run's total with its own sub-part.
Restated against this report's own 16.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WcKTqMArCFofDVJJEJdtM2
F10 (should-fix): the backlog entry claimed "three rounds … all fixed and
verified" while round 3 had not run. Third time this branch has written a
verification claim ahead of the verification. The entry now records what the
review did and points at the PR for its disposition, which is where a verdict
belongs.

F11: DESIGN.md asserted the detector "under-reports rather than invents" two
lines above the one case that is neither — a stale answer, where a move rested
early and then landed somewhere new too briefly to register. That mode is now
named as its own limit. The same text blamed the driver for the single
driver-vs-trace disagreement; on the evidence the trace is the likelier error
(the same aim's other three repetitions show no bridge contact at an identical
apex, and the trace's bridge test is a 28px band against a ~26px/sample descent),
so the text now reports the disagreement without deciding it.

F12: the nine silent deaths were explained as "crosses the fall edge inside the
first poll", which is wrong — two of them took off jumping at x=628/636, short
of the 656 edge. The cause is the three-sample rest window: block C's stage two
starts parked at the lip with its `jumpAtX` 20px behind it, so the press lands on
sample one and the astronaut is airborne before the window can close.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WcKTqMArCFofDVJJEJdtM2
@ksdisch

ksdisch commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

Adversarial review — CLEAR TO MERGE (rounds: 3)

# Severity (final) Finding Disposition
F1 should-fix Doc cited a backlog Shipped entry and a re-run that did not exist Fixed in 12b1a0f, verified R2
F2 should-fix jump / lastStoodAt documented as unconditional, but omitted on real paths Fixed in 12b1a0f, verified R2
F3 nice-to-have Rest detector's apex guard disabled on every move's first sample Fixed in 12b1a0f, verified R2
F4 nice-to-have 1px pair tolerance admitted the first sample of a fall (the 430/477 rows) Fixed in 12b1a0f, verified R2
F5 nice-to-have Take-off resolver ran past the documented hard maxMs cap; verdict event after terminal event Fixed in 12b1a0f, verified R2
F6 nice-to-have Sweep counted non-deaths as missing lastStoodAt telemetry Fixed in 12b1a0f, verified R2
F7 nice-to-have Prompt asserted a cause for tookOff: false the driver does not establish Fixed in 12b1a0f, verified R2
F8 should-fix Shipped verification numbers were measured on the pre-fix driver Fixed in e8ef09d, verified R3
F9 nice-to-have No-jump comparison contrasted a total with its own sub-part Fixed in e8ef09d, verified R3
F10 should-fix Backlog claimed "three rounds … all fixed and verified" before round 3 ran Fixed in 0f6d9b7, re-verify waived
F11 nice-to-have "Under-reports rather than invents" asserted above the one stale-answer case; disagreement blamed on the driver Fixed in 0f6d9b7, re-verify waived
F12 nice-to-have Nine silent deaths explained by the wrong cause Fixed in 0f6d9b7, re-verify waived

Waived by Kyle: F10, F11, F12 — "Waive re-verify, merge", chosen at the cap-residue prompt. All three are documentation-accuracy fixes; 0f6d9b7cc changes no driver code, so the measured 68/68 result is unaffected by them.
Follow-ups (nice-to-have): none outstanding — every nice-to-have was fixed rather than deferred.
Coverage: complete in all three rounds (every changed file read in full, plus phone.mjs, verify-rails.sh, and the cited sweep logs).

The loop's most useful catch was F8: round 1's fixes tightened the rest detector, which silently invalidated the verification numbers committed alongside them — two quoted values were artifacts the fix existed to remove. The sweep was re-run against the shipped build (logs/20260813-210539) and both documents repointed. Three of the twelve findings (F1, F8-secondary, F10) were the same pattern: writing a verification claim into the durable record ahead of the verification.

Zero-context reviewer + neutral judge on disputes (no disputes were raised); anchored at 0f6d9b7.

This comment is AI-generated by the adversarial-review loop.

@ksdisch
ksdisch merged commit f347d5c into main Aug 14, 2026
3 checks passed
@ksdisch
ksdisch deleted the feat/jump-outcome-telemetry branch August 14, 2026 02:44
ksdisch added a commit that referenced this pull request Aug 14, 2026
#26)

* feat(playtest): fold pilot 4's arm-ordering protocol into the seat prompts

Pilot 4 finding 4: all three arm-timeouts and both stale-freeze deaths were
casts that landed before the arm was placed, including two ~90s deadlocks
where both seats blocked at once. Its recommendation was a protocol fix, not
a driver one. The laptop prompt now states the ask and the armed /move must
go out in the same turn, and what to do about a stale ask; the phone prompt
holds a freeze cast until the partner says they are arming, and re-casts
instead of answering "already cast" to an armed partner. Platform casts are
exempt on both sides — a platform stands and waits, so order never matters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2UogP8hYqztBhUACZ7vk3

* docs(playtest): pilot 5 — the jump verdict works live, and its death was invisible

Two runs against constellation 604a422, the first live co-op with PR #25's
jump telemetry in the seats' hands and the first under pilot 4 finding 4's
arm-ordering protocol. Run A's laptop reasoned an aim from lastStoodAt pit
data and got the harness's first seat-issued take-off verdict; the jump
overflew the bridge because untilX 850 never cut input, and the death
completed after the move returned — one of three such invisible deaths, now
a new backlog item. The protocol fix measured clean (15/15 arms fired, zero
timeouts vs pilot 4's three), five stale-platform arms fired instantly and
died (folded into the trigger-life item), and the faster pace hit the
free-tier token cap sooner: 3m27s and 1m54s of play.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2UogP8hYqztBhUACZ7vk3

* fix(playtest): pilot 5 review round 1 — run B's last 28s restored, prompt seam closed

F1/F2: the report truncated run B at 03:33:20, missing three deaths, three
freeze arms, and a second tookOff:true jump whose death WAS captured
(diedAt {896,602}, lastStoodAt {632,476}) — every run B aggregate re-derived
from the full transcript (8 deaths, 18/18 arms, 12 freeze arms, 19 total
deaths), the one-trial caveat now two trials, and finding 3 reframed around
the terminator dependence run B exposes. New finding 7: the seat read that
complete record as "Cleared the pit" — telemetry present, misread. F3: the
Illuminate was requested and the phone's hold was against a real ask; both
inverted claims removed. F4: the laptop prompt now says the word "arming"
is load-bearing and how to go un-armed on purpose; the phone prompt casts
on an explicit un-armed ask instead of holding forever. BACKLOG figures
follow the report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2UogP8hYqztBhUACZ7vk3

* fix(playtest): pilot 5 review round 2 — run B's one protocol drift, named as such

F7: round 1's fix upgraded the discipline claim to "all four seats without
drift" while restoring the very data that refutes it — run B's laptop asked
for a platform, idled ~35s, and armed 21.8s after the cast landed, dying
unprotected at x=384. Finding 2 and the protocol section now scope the claim
(run A's laptop clean on all ten ask->arm pairs, both phones clean, run B's
laptop dropped it once) and carry the price comparison: the same drift that
bought pilot 4 a 90s deadlock buys one unprotected run under a platform's
indefinite wait. The five instant fires split 4 persistence + 1 own-cast
drift everywhere they are counted, and the cadence figures note the idle gap
inside run B's 1m54s window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2UogP8hYqztBhUACZ7vk3

* fix(playtest): pilot 5 review round 3 — split the 35s gap where the transcript splits it

F9: the drift paragraph charged the whole ~35s to the seat, but the phone's
confirmation existed 13.1s after the turn ended and then took 20.8s to reach
the idle seat — against 1.2-6.1s for every busy-seat delivery and a 1.5s
extension poll. The avoidable cost is ~18s and is now stated as such in all
three places the figure appeared, and the 20.8s idle-wake latency is
recorded as finding 8: one observation, flagged as wanting a scripted
reproduction before it is trusted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2UogP8hYqztBhUACZ7vk3

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant