feat(playtest): /move reports whether the jump happened, and what it fell off - #25
Conversation
…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
Sweep re-run — 68 trials against the new telemetry
Take-off verdict: 68 trials judged, 68 agree, 0 disagree.
The 18 land where the physics says they should: What it fell off:
No overlap, and every one of the 16 bridge-height rows is independently Counts differ from the original sweep, and that is measurement, not This comment is AI-generated. |
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
Adversarial review — CLEAR TO MERGE (rounds: 3)
Waived by Kyle: F10, F11, F12 — "Waive re-verify, merge", chosen at the cap-residue prompt. All three are documentation-accuracy fixes; 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 ( Zero-context reviewer + neutral judge on disputes (no disputes were raised); anchored at 0f6d9b7. This comment is AI-generated by the adversarial-review loop. |
#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>
Backlog Active item 4 — the two follow-ups the planet-1 aim sweep produced. Both
are the same shape as
diedAtin #21: the harness already knows the answer andthrows it away before the seat sees it.
What was wrong
jumpedmeant "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
jumpAtXset within ~14px of planet-1'sfall edge is usually first seen after the astronaut has left the ledge. All eight
sweep trials at
jumpAtX>= 646 reportedjumpedhaving 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
rightaftertouchdown. Every one reported the same thing as a jump that never came near the
bridge:
respawnedwith a pitdiedAtaround{776, 600}.What changed
/movenow returnsjump: {tookOff, pressedAt, apexY}whenever it actuallypressed, and pushes
jumpedonly once the astronaut is observed to rise 8px abovethe press point —
jump-ignoredotherwise. A press made while already climbing iscalled inert immediately, since being airborne is precisely why the game refuses
it.
apexYis the highest point reached after the press, which also separates aclean 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 theastronaut 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 thebridge,
{~650, 476}is the ground at the pit's lip.Both are inferred from sampled
y, because the game exposes no contact flag(
BridgeStatecarriesastronautYand nothing about what is under the feet) andthe 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 abovestanding height, so a post-death sample reads exactly like a rise.
aim-sweep.shrecords both fields per trial and cross-checks the driver's verdictagainst the trajectory trace, which is polled from
/stateand owes nothing to/move. The phone's world glance carrieslastStoodAttoo — "did they fall offthe 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:
apexYfroze when the verdict resolved (reporting the take-off at 454 instead ofthe 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:
jumpAtX:598barejumped, respawnedtookOff:true, pressedAt:{612,476}, apexY:362{612,476}jumpAtX:650barejump-ignored, respawnedtookOff:false, pressedAt:{656,476}, apexY:476{656,476}jumpAtX:626 untilX:780jumped, reached-xtookOff:true, apexY:362The full 68-trial
aim-sweep.shre-run is in flight; its cross-check against theindependent trace lands as a comment on this PR before merge.
npm run checkpasses (pre-commit hook, full run).