Skip to content

fix(qa): harness reliability — 4 gate-fidelity + #623 recovery fixes - #966

Merged
100yenadmin merged 2 commits into
mainfrom
fix/harness-reliability
Jun 16, 2026
Merged

fix(qa): harness reliability — 4 gate-fidelity + #623 recovery fixes#966
100yenadmin merged 2 commits into
mainfrom
fix/harness-reliability

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented Jun 16, 2026

Copy link
Copy Markdown
Member

Root-caused via a 4-surface analysis workflow; each fix is TARGETED and preserves its integrity gate (risk-mitigations tested). (1) party_traveled no longer false-caps a complete single-scene drama; (2) a persona player crash (rc!=0) is retried+reclassified as harness-inconclusive (evidence gap), not a quality fail; (3) PART_A_DEADLINE derived from the cold-open tier + liveness grace (fixes flaky handoff legs); (4) #623 empty-mark recycle + hang-escalation closed. Tests: assert_behavioral 38 / release_readiness 47 / dm_beat_mark 8 / fast_gate 222 green.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed single-location story progression to allow an “in-place” arc to pass once the campaign clock advances and the arc resolves.
    • Improved beat recycling detection by tracking whether a beat is a cold-open or a continuation, tightening empty-mark handling.
    • Refined timeout/retry behavior to avoid unnecessary retries after deadline-killed routine beats.
    • Reclassified persona runs with harness errors as inconclusive evidence gaps instead of scoring failures.
  • Tests

    • Added/expanded coverage for single-location progression, cold-open vs continuation marking, and harness-error release readiness behavior.

…ry fixes

The QA measurement harness was the meta-blocker: noisy/flaky signals that conflate harness
failures with product quality. Four root-caused, TARGETED fixes (each preserves its integrity
gate — risk-mitigations enforced + tested):

1. party_traveled false-cap (assert_behavioral.py): a COMPLETE single-location drama (clock
   advanced AND a quest resolved AND >=8 beats) no longer RED-caps every lens <=2.5 like a frozen
   stall. Conjunctive in-place-progression exception; a frozen opening (day==1/morning, no quest)
   still fails >=2 conjuncts and stays RED. Additive — can only let a run PASS.

2. persona rc!=0 mis-counted as a quality fail (sweep_v2.sh + ui_playtest_app.sh +
   release_readiness.py): a NON-quota player-process CRASH (player_rc!=0) is now retried once,
   then stamped part_b.harness_error → surfaced as a cross_persona_sat EVIDENCE GAP
   (harness_contaminated, still blocks release as "re-measure"), NOT a score_pass quality fail.
   Discriminator is the process exit, never the score — a played low-score run exits rc=0 and
   still fails the quality gate.

3. Mac-handoff PART_A_DEADLINE drift (ui_playtest_app.sh): the flat 420s mint deadline had
   drifted BELOW the DM cold-open's own 500/550s timeout → healthy-but-slow mints abandoned
   (flaky scripted/codex legs). Now derived from the cold-open tier + 90s margin, with a
   liveness-bounded grace (extends only while dm.combined.jsonl streams / the DM proc is alive;
   a dead cold-open fails promptly). can_act:true+minted_run PASS condition unchanged.

4. #623 silent-drop recycle + hang escalation (dm_beat_mark.py + lib_beat_driver.sh + play*.sh):
   (a) an empty pre-beat mark on a CONTINUING beat (first==0, a mark-write bug) no longer
   fails-open and stamps recycled prose "genuine" (masked dead beat); a true first-prose cold
   open (first==1) is unaffected. (b) a deadline-killed (rc=124) ROUTINE beat no longer escalates
   the retry to the cold-open tier (the ~14-15min single-beat hang) — it routes to the visible
   failure path; cold opens + non-124 transient errors still retry.

Tests: assert_behavioral 38, release_readiness 47, dm_beat_mark 8 (new file), fast_gate 222 — all green.
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8db8cf21-ec9f-4606-9733-e91ea48241e0

📥 Commits

Reviewing files that changed from the base of the PR and between 41da240 and 26f2efa.

📒 Files selected for processing (2)
  • qa/assert_behavioral.py
  • qa/test_assert_behavioral.py

📝 Walkthrough

Walkthrough

The PR hardens the QA automation pipeline across four areas: (1) adds a first (cold-open vs. continuing) discriminator to pre-beat marking so recycled prose is detected, wiring it through dm_beat_mark.py, lib_beat_driver.sh, play.sh, and play_party.sh; (2) replaces Part A's fixed poll deadline with tier-derived liveness-aware polling; (3) introduces part_b.harness_error in ui_playtest_app.sh, reclassifying player-agent crashes as INCONCLUSIVE in release_readiness.py, and adding sweep-level retry for non-quota failures; (4) extends the party_traveled behavioral gate to allow single-location in-place arcs.

Changes

QA Harness Hardening

Layer / File(s) Summary
Beat first signal in pre-beat mark
qa/dm_beat_mark.py, qa/lib_beat_driver.sh
cmd_mark stores the first (cold-open/continuing) signal in the mark JSON; cmd_check force-fails continuing beats whose pre-beat mark came back empty; clawdnd_dm_prebeat_mark threads the argument through and stops suppressing mark-write errors.
dm_beat_mark tests
qa/test_dm_beat_mark.py
New pytest module covers cmd_mark recording first, cmd_check force-failing continuing empty marks, cold-open/legacy fail-open behavior, real-baseline genuine/recycled outcomes, unreadable mark fail-open, and CLI wiring.
DM turn rc=124 no-retry + first wiring
scripts/play.sh, scripts/play_party.sh
Both scripts pass first into clawdnd_dm_prebeat_mark and add a conditional that routes deadline-killed (rc=124) continuing beats directly to failure handling instead of performing the escalated retry.
Part A liveness-aware cold-open polling
qa/ui_playtest_app.sh
Replaces fixed 420s deadline with tier-derived value; adds _file_mtime_epoch and coldopen_is_live helpers; polling loop uses a hard deadline plus a bounded grace extension that continues only while the cold-open is live.
Part B harness error detection and run.json annotation
qa/ui_playtest_app.sh
Introduces PART_B_HARNESS_ERROR global; sets it true when player_rc is non-zero and failure text does not match quota/429 phrases; extends inline Python run.json writer to emit harness_error under part_b.
Release readiness: harness error → INCONCLUSIVE
qa/release_readiness.py, qa/test_release_readiness.py
Captures part_b_harness_error per persona; excludes those personas from score_pass_failed_personas; adds them as cross_persona_sat evidence gaps; narrows failed_part_b to exclude harness-error personas from arc_completed; surfaces harness_error_personas in signals output. Test asserts the reclassification contract end-to-end.
Sweep non-quota harness retry
qa/vm/sweep_v2.sh
run_persona retries ui_playtest_app.sh once on non-zero exit when backend log has no quota/429 indicator; quota trips still short-circuit to the honest quota path.
party_traveled in-place arc exception
qa/assert_behavioral.py, qa/test_assert_behavioral.py
Replaces visited>=2 hard gate with a disjunction: passes if visited≥2 OR (clock_advanced AND arc_resolved AND session_beats≥8); tests verify PASS/FAIL across frozen, unresolved, and beat-count edge cases, plus a regression where event stream calls set_quest_status but snapshot quest remains active.

Sequence Diagram(s)

sequenceDiagram
  participant sweep as sweep_v2.sh run_persona
  participant playtest as ui_playtest_app.sh
  participant release as release_readiness.py

  sweep->>playtest: run Part A + Part B
  playtest->>playtest: coldopen_is_live() liveness gate (Part A)
  playtest->>playtest: player_rc != 0 AND no quota/429 text?
  alt harness error (non-quota)
    playtest->>playtest: PART_B_HARNESS_ERROR=true
    playtest->>sweep: write run.json {part_b: {harness_error: true}}
    sweep->>playtest: retry once (non-quota path)
    playtest->>sweep: rc (after retry)
  else quota / session limit
    playtest->>sweep: short-circuit to quota path (no retry)
  end
  sweep->>release: aggregate run.json results
  release->>release: part_b_harness_error → exclude from score_pass_failed_personas
  release->>release: add to cross_persona_sat evidence_gaps
  release->>release: exclude from arc_completed failed_part_b
  release-->>sweep: harness_contaminated=true, harness_error_personas=[...]
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • electricsheephq/WorldOS#841: Modifies the same qa/dm_beat_mark.py cmd_mark/cmd_check pipeline and qa/lib_beat_driver.sh wiring that this PR extends with the first discriminator.
  • electricsheephq/WorldOS#567: Both PRs modify the rc=124 / beat first handling in scripts/play.sh and scripts/play_party.sh DM-turn control flow.
  • electricsheephq/WorldOS#374: Introduced run_part_b in qa/ui_playtest_app.sh; this PR extends that same function with part_b.harness_error propagation.

Poem

🐇 Hop, hop through the dungeon halls,
No recycled prose to fool these walls!
If the clock stood still, the arc's undone—
Eight beats at least before we're won.
Harness crashes? Inconclusive, dear—
This bunny marks the evidence clear! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately summarizes the main change: four targeted QA harness reliability and gate-fidelity fixes plus #623 recovery improvements, matching the changeset content.
Description check ✅ Passed Description covers the core changes and includes a 4-fix summary with rationale. The template's optional CLA section and validation checklist are not completed, but the core content requirement is met.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
qa/ui_playtest_app.sh (1)

1119-1121: 💤 Low value

Minor: -r flag is unnecessary for named files.

grep -r is for recursive directory search. When targeting specific files ("$RUNDIR/backend.log" "$PLAYERDIR/player.err"), the -r flag has no effect but adds slight confusion. Consider using just -qiE.

This doesn't affect correctness since grep handles it gracefully.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qa/ui_playtest_app.sh` around lines 1119 - 1121, The grep command on lines
1119-1121 includes the `-r` flag, which is unnecessary when searching specific
named files like "$RUNDIR/backend.log" and "$PLAYERDIR/player.err". The `-r`
flag is only needed for recursive directory searches and adds unnecessary
confusion here. Remove the `-r` flag from the grep invocation, keeping only the
`-qiE` flags with the specific file paths.
qa/test_assert_behavioral.py (1)

698-736: 💤 Low value

Consider adding test for clock-not-advanced with arc-resolved.

The four tests thoroughly cover the happy path and three failure modes. One edge case not explicitly tested: day=1, tod="morning" (clock NOT advanced) with quest_completed=True (arc resolved). This would confirm that clock_advanced is independently required, not just correlated with arc_resolved.

Since the implementation uses a strict AND, the current tests provide adequate coverage—but an explicit test would guard against future refactors that might accidentally broaden the exception to arc-only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qa/test_assert_behavioral.py` around lines 698 - 736, Add a new test function
after test_party_traveled_still_red_when_arc_resolved_but_too_few_beats that
explicitly tests the case where the clock has not advanced (day=1,
tod="morning") but the arc is resolved (quest_completed=True). This test should
verify that party_traveled fails because clock_advanced is independently
required by the AND condition, even when arc_resolved is true. Create test
events with beats >= 8, set up state with day=1, tod="morning",
quest_completed=True, and visited_count=1, then assert that the output contains
"[FAIL] party_traveled" and the return code is 1, confirming that both clock
advancement and arc resolution are strictly required by the exception logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@qa/vm/sweep_v2.sh`:
- Around line 123-124: The retry gating condition at line 123 in the sweep_v2.sh
file uses the quota_tripped function with only the backend log (_bl variable) to
detect quota-related failures. However, the upstream classifier in
qa/ui_playtest_app.sh checks for quota/429 errors across both backend.log and
player/player.err files. Update the quota_tripped call to align with the
upstream behavior by including both log sources in the quota detection logic,
ensuring that quota-triggered aborts are correctly identified and do not consume
the single retry allocation.

---

Nitpick comments:
In `@qa/test_assert_behavioral.py`:
- Around line 698-736: Add a new test function after
test_party_traveled_still_red_when_arc_resolved_but_too_few_beats that
explicitly tests the case where the clock has not advanced (day=1,
tod="morning") but the arc is resolved (quest_completed=True). This test should
verify that party_traveled fails because clock_advanced is independently
required by the AND condition, even when arc_resolved is true. Create test
events with beats >= 8, set up state with day=1, tod="morning",
quest_completed=True, and visited_count=1, then assert that the output contains
"[FAIL] party_traveled" and the return code is 1, confirming that both clock
advancement and arc resolution are strictly required by the exception logic.

In `@qa/ui_playtest_app.sh`:
- Around line 1119-1121: The grep command on lines 1119-1121 includes the `-r`
flag, which is unnecessary when searching specific named files like
"$RUNDIR/backend.log" and "$PLAYERDIR/player.err". The `-r` flag is only needed
for recursive directory searches and adds unnecessary confusion here. Remove the
`-r` flag from the grep invocation, keeping only the `-qiE` flags with the
specific file paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9c1b3513-a2e6-43a9-8de2-5d4be760b9c2

📥 Commits

Reviewing files that changed from the base of the PR and between ea6289a and 41da240.

📒 Files selected for processing (11)
  • qa/assert_behavioral.py
  • qa/dm_beat_mark.py
  • qa/lib_beat_driver.sh
  • qa/release_readiness.py
  • qa/test_assert_behavioral.py
  • qa/test_dm_beat_mark.py
  • qa/test_release_readiness.py
  • qa/ui_playtest_app.sh
  • qa/vm/sweep_v2.sh
  • scripts/play.sh
  • scripts/play_party.sh

Comment thread qa/vm/sweep_v2.sh
Comment on lines +123 to +124
if [ "$rc" -ne 0 ] && ! quota_tripped "$_bl"; then
note " $persona rc=$rc (non-quota harness crash) — retrying ONCE on a clean store"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align retry quota detection with the upstream non-quota predicate.

At Line 123, retry gating checks only backend.log. The upstream Part B classifier (qa/ui_playtest_app.sh) treats quota/429 based on both backend.log and player/player.err; this mismatch can consume the one retry on quota-aborted runs.

Suggested fix
-quota_tripped(){  # $1 = a run dir or a log path; rc 0 if a session-limit/429 is present
-  grep -qriE "session limit|HTTP 429|hit your (session|usage) limit" "$1" 2>/dev/null
+quota_tripped(){  # $@ = run dir(s)/log path(s); rc 0 if a session-limit/429 is present
+  grep -qriE "session limit|HTTP 429|hit your (session|usage) limit" "$@" 2>/dev/null
 }
@@
-  local _bl="qa/ui_playtest_runs/vm2-$persona/backend.log"
-  if [ "$rc" -ne 0 ] && ! quota_tripped "$_bl"; then
+  local _bl="qa/ui_playtest_runs/vm2-$persona/backend.log"
+  local _pe="qa/ui_playtest_runs/vm2-$persona/player/player.err"
+  if [ "$rc" -ne 0 ] && ! quota_tripped "$_bl" "$_pe"; then
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qa/vm/sweep_v2.sh` around lines 123 - 124, The retry gating condition at line
123 in the sweep_v2.sh file uses the quota_tripped function with only the
backend log (_bl variable) to detect quota-related failures. However, the
upstream classifier in qa/ui_playtest_app.sh checks for quota/429 errors across
both backend.log and player/player.err files. Update the quota_tripped call to
align with the upstream behavior by including both log sources in the quota
detection logic, ensuring that quota-triggered aborts are correctly identified
and do not consume the single retry allocation.

…tatus-blind tool count

Adversarial verification of FIX 4 found a real gate-weakening: arc_resolved used
coverage_from_tool_counts' quest_resolved bucket, which counts the set_quest_status TOOL NAME
regardless of the status argument. A frozen DM could pass party_traveled (and dodge the
behavioral RED-cap) on a dead scene with one advance_time + one set_quest_status(status="active").
Reproduced GREEN on a frozen single-scene run.

Fix: arc_resolved now requires an ACTUAL snapshot quest at status=="completed" (which a
set_quest_status("active"/"failed") does not produce). + a regression test that a frozen scene
with a set_quest_status tool call present stays RED. The legitimate single-scene-progression
case (a quest reaching status=="completed") still passes. assert_behavioral 39/39 green.
@100yenadmin
100yenadmin merged commit 179a52f into main Jun 16, 2026
3 of 5 checks passed
100yenadmin added a commit that referenced this pull request Jun 16, 2026
…ness-aware contract (#623) (#970)

test_coldopen_part_a_poll_window_outlasts_max_effort_coldopen pinned the literal
`PART_A_DEADLINE="${WOS_APP_PART_A_DEADLINE:-420}"` and the old `seq 1 "$part_a_polls"` for-loop —
both of which FIX 3 (#623, shipped in #966) intentionally replaced. #966's fast_gate doesn't run
test_adversarial_release.py, so the stale test slipped through and left main RED.

The production code is correct: 420 was SHORTER than the cold-open's own model-aware timeout
(clawdnd_dm_timeout 1 = 500/550s), so a healthy-but-slow mint was abandoned ~80s early — the exact
flake the test NAME warns about. FIX 3 derives the deadline FROM that tier + a positive margin and
makes the poll liveness-aware. Rewrite the test to assert that *contract* semantically (extract the
margin via regex, assert it's > 0 = the real "outlasts" guarantee; assert the hard-deadline +
coldopen_is_live grace; keep the no-hardcoded-`seq 1 70` guard) — so a regression to a flat / too-
short / non-positive-margin deadline now fails here, but the intended derivation passes. Test-only;
18/18 in test_adversarial_release.py green; fast_gate 222 intact.

Co-authored-by: Eva <arncalso@gmail.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