Skip to content

fix(prd): parse fenced stress-test JSON and bound the walk (#927) - #1040

Merged
frankbria merged 8 commits into
mainfrom
fix/927-stress-test-json
Aug 2, 2026
Merged

fix(prd): parse fenced stress-test JSON and bound the walk (#927)#1040
frankbria merged 8 commits into
mainfrom
fix/927-stress-test-json

Conversation

@frankbria

Copy link
Copy Markdown
Owner

Closes #927.

The silent false pass

extract_goals did a raw json.loads and returned [] on failure. The caller reads an empty goal list as "nothing to analyse" and reports "No ambiguities found — PRD is well-specified".

So a provider that wrapped its JSON in a markdown fence produced a clean bill of health after the user paid for the call. Fenced JSON is routine on the OpenAI-compatible and local providers this command explicitly supports.

The divergence was the bug

Every sibling LLM-JSON consumer in this repo already strips fences — in two subtly different shapes, across four call sites (executor.py ×2, prd_discovery.py ×2). This one had none.

So the fix is a shared leaf module, core/llm_json.py, whose parse_json_response raises rather than returning a falsy default. That is the whole lesson of the bug: a parser that degrades quietly turns a provider quirk into a passing grade.

It also tolerates prose around the block ("Sure! Here you go:"), which local models add routinely, and a fence with no language tag.

Failing loudly (AC2)

extract_goals now raises StressTestError on unparseable output, a non-list, or zero usable goals. An empty PRD is not a well-specified PRD. The SSE stream's existing blanket handler turns that into an error event, so both surfaces report it.

classify_and_decompose keeps degrading a single unparseable node to a leaf — there, unlike goal extraction, an empty result is not indistinguishable from success.

Bounded recursion (AC3)

The walk followed a model-supplied children list with only a depth cap, API-settable to 10. Breadth multiplies at every level and each node is one paid call.

  • MAX_CHILDREN_PER_NODE = 12 — truncates, and logs what it dropped
  • MAX_LLM_CALLS = 200 — a per-run budget
  • a truncated run returns the partial tree with partial: true rather than raising; the ambiguities already found are real, but their absence elsewhere is not evidence

Cancellation (AC4)

The router broke out of its loop on disconnect, which only stopped between top-level goals — the recursion inside one goal kept issuing billable calls. The disconnect is now latched into a flag the budget polls before every call, so an abandoned stream stops at the next node.

Testing

tests/core/test_stress_test_json_927.py — 15 tests across all four ACs.

Three existing tests asserted the silent-empty behaviour and now assert the raise; a fourth pins the fenced-response case that triggered all of this.

PRD suites: 251 passed. Full gate: 4945 passed, ruff clean.

Scope note

The four existing hand-rolled fence strippers in executor.py and prd_discovery.py are left in place. Converting them is mechanical but touches two unrelated subsystems; the shared module now exists for them to adopt, and this PR stays scoped to the parse sites the issue names.

**The silent false pass.** extract_goals did a raw json.loads and returned []
on failure; the caller reads an empty goal list as "nothing to analyse" and
reports "No ambiguities found — PRD is well-specified". So a provider that
fenced its JSON produced a clean bill of health *after the user paid for the
call*. Fenced JSON is routine on the OpenAI-compatible and local providers this
command explicitly supports.

Every sibling LLM-JSON consumer in this repo already strips fences — in two
subtly different shapes, across four call sites. This one had none. That
divergence is the bug, so the fix is a shared leaf module: core/llm_json.py,
whose parse_json_response *raises* rather than returning a falsy default. A
parser that degrades quietly is how a provider quirk becomes a passing grade.

extract_goals now raises StressTestError on unparseable output, a non-list, or
zero usable goals. An empty PRD is not a well-specified PRD. The SSE stream's
existing blanket handler turns that into an error event, so both surfaces
report it.

classify_and_decompose keeps degrading a single unparseable node to a leaf —
there, unlike goal extraction, an empty result is not indistinguishable from
success.

**Bounded recursion.** The walk followed a model-supplied children list with
only a depth cap (API-settable to 10); breadth multiplies at every level and
each node is one paid call. Adds MAX_CHILDREN_PER_NODE (12) and a per-run
_Budget with MAX_LLM_CALLS (200), returning the partial tree with
`partial: true` rather than raising — the ambiguities already found are real.

**Cancellation.** The router broke out of the loop on disconnect, which only
stopped between *top-level goals* while the recursion inside one kept issuing
calls. The disconnect is now latched into a flag the budget polls before every
call, so an abandoned stream stops at the next node.

Three existing tests asserted the silent-empty behaviour and now assert the
raise; a fourth pins the fenced-response case that triggered all of this.
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Claude Code is working…

I'll analyze this and get back to you.

View job run

@frankbria
frankbria enabled auto-merge (squash) August 2, 2026 04:04
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @frankbria's task in 5m 48s —— View job


GLM precision review — complete

  • Gather full PR diff and context (gh pr diff, gh pr view)
  • Read changed files in surrounding context (callers/callees)
  • Confirm each suspected defect against real code
  • Post inline comments for confirmed defects
  • Post summary comment

3 defects found (all minor) — gaps between the PR's stated ACs and the behavior of the surfaces it claims to cover:

  1. CLI unhandled raiseextract_goals now raises StressTestError; the CLI prd stress-test command doesn't catch it → traceback, not the clean error the AC2 "both surfaces report it" implies.
  2. Sync per-goal budgetrecursive_decompose defaults to a fresh _Budget(), so the sync CLI path is bounded goals × 200, not per-run 200 (AC3).
  3. Cancellation latch ineffective within a goal — the disconnected flag is set only at goal boundaries, so within-goal orphaned work runs to the 200-call cap, not "the next node" (AC4).

All three are strict improvements over pre-PR; none crash the happy path or lose data. Details and failure scenarios are in the 3 inline threads + the summary comment.

Comment thread codeframe/core/prd_stress_test.py
Comment thread codeframe/core/prd_stress_test.py
Comment thread codeframe/ui/routers/prd_v2.py Outdated
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

GLM precision review — 3 defects (all minor)

Scoped to concrete defects only (logic errors, unhandled error paths, billing/bounding gaps). Style, architecture, naming, and test coverage are covered by the general review.

severity file:line finding
minor codeframe/core/prd_stress_test.py:200 extract_goals now raises StressTestError, but the CLI prd stress-test command (cli/app.py:1752, :1794) never catches it → Typer traceback instead of the clean red-error + Exit(1) used elsewhere. The PR's "both surfaces report it" (AC2) holds for the web stream only.
minor codeframe/core/prd_stress_test.py:310 recursive_decompose defaults budget to a fresh _Budget(), so the sync stress_test_prd (CLI path) is bounded per-goal (goals × 200), not per-run (200) as AC3 and the async path enforce.
minor codeframe/ui/routers/prd_v2.py:297 The disconnected latch is set only at event-yield boundaries (between top-level goals); within a single goal's recursive_decompose it stays False, so an abandoned stream's within-goal work runs until the 200-call budget cap, not "the next node" as AC4 states.

All three are strict improvements over the pre-PR state and none lose data or crash the happy path; they are gaps between the PR's stated ACs and the behavior of the surfaces it claims to cover. Details + concrete failure scenarios are in the inline threads. Not approving or requesting changes.

All three were places where a stated acceptance criterion covered a surface
the code did not actually reach.

- CLI: `extract_goals` raises StressTestError, but `prd stress-test` never
  caught it, so AC2's "both surfaces report it" produced a Typer traceback on
  the CLI while the web stream got a clean error event. Now a red line + exit 1,
  matching every other failure in that command.

- Sync path: `stress_test_prd` let each goal default to its own `_Budget()`,
  bounding the walk at `goals x MAX_LLM_CALLS` rather than the documented
  per-run total AC3 asks for. One budget now spans the run, and
  `StressTestResult.partial` reports a truncated walk the way the stream
  already did — the CLI warns instead of presenting a partial tree as complete.

- SSE route: a whole top-level goal decomposes inside one `asyncio.to_thread`,
  so a latch updated only between yielded events stayed False for that entire
  billable walk. A concurrent poller now sets it while the worker thread runs,
  so the recursion stops at its next node rather than its next goal (AC4). The
  per-event check is kept alongside it — a stream faster than the poll interval
  would otherwise run to completion for a client already gone.

Tests: tests/core/test_stress_test_review_1040.py, one per finding. Verified
they fail (4/4) with the fixes reverted.
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @frankbria's task in 3m 50s —— View job


GLM precision review — re-run on synchronize

Reviewed the current PR state (e57ac55 + fix commit 4e4d7a0) against origin/main, file-by-file. The 3 prior findings are correctly resolved and I found no new concrete defects.

  • Gather full PR diff and recent commits (git diff origin/main HEAD, git show 4e4d7a0)
  • Read changed files + surrounding code (focus on the 3 fixes)
  • Confirm each suspected defect against real code
  • Post inline comments for confirmed defects — none
  • Post summary

Scoped to concrete defects only (logic errors, unhandled error paths, billing/bounding gaps, races). Not repeating the 3 already-resolved findings.


✅ GLM review: no defects found.

What I verified on each fix (reading the real code, not the diff alone):

Fix Verdict Detail
CLI unhandled raise (cli/app.py:1753, :1801) ✓ resolved Both stress_test_prd call sites are wrapped in try/except StressTestError[red]Error[/red] + typer.Exit(1), matching the command's other failure paths. The result.partial warning at the summary (:1820) runs only on non-exiting paths, so result is always bound when reached.
Sync per-goal budget (prd_stress_test.py:507) ✓ resolved One budget = _Budget() is created before the goal loop and threaded into every recursive_decompose call, including the recursion at :348 (budget is None is never hit on this path). is_cancelled=None, so take() short-circuits only on exhaustion → ceiling is MAX_LLM_CALLS, not goals × MAX_LLM_CALLS. partial=budget.stopped_early is reported; the break at :510 stops queuing further goals.
Within-goal cancellation (prd_v2.py:295–330) ✓ resolved _watch_disconnect flips the same disconnected flag the budget's is_cancelled=lambda: disconnected reads. budget.take() is checked at the top of every recursive_decompose node, so a disconnect is observed at the next node boundary — not the next goal. The per-event check is retained (a stream faster than the 1 s poll still aborts), and watcher.cancel() runs in finally on every exit path.

Notes (not defects)

  • One orphan call on disconnect is inherent, not a regression. extract_goals (prd_stress_test.py:573) and the in-flight classify_and_decompose call run as blocking provider.complete() and can't be interrupted mid-call. The recursion stops at the next node, which is exactly AC4's claim. The pre-fix ceiling was ~200 orphaned calls; the post-fix ceiling is 1.
  • StressTestResult.partial is additive (default False, single construction site at :539 using a keyword); the streaming complete event gaining a partial key is a dict, not a Pydantic model, so no schema break.
  • watcher is cancelled but not awaited. Cancelled tasks don't surface "exception never retrieved" (CancelledError is special-cased), so this is a style nit, not a leak or crash — not flagged.

The claude-review CI failure noted in the prior comment (Unable to get ACTIONS_ID_TOKEN_REQUEST_URL) is an unrelated workflow-permission issue affecting all open PRs.

@frankbria

Copy link
Copy Markdown
Owner Author

Review triage — GLM precision review (3 findings)

All three verified against the code before acting. All three are real, and all three are fixed in 4e4d7a0 — none were rebutted.

# Finding Verdict Action
1 extract_goals raises StressTestError but the CLI never catches it (cli/app.py:1752, :1794) Confirmed. Neither call site is wrapped; every other failure in that command prints a red line and Exit(1), so this one uniquely produced a Typer traceback. AC2 claims both surfaces report the failure — the web stream did, the CLI did not. Fixed: both call sites wrapped, [red]Error:[/red] <msg> + Exit(1).
2 stress_test_prd lets each goal default to a fresh _Budget() → per-goal bound, not per-run (prd_stress_test.py:310) Confirmed. recursive_decompose defaults budget=None → _Budget(), and the sync loop passed nothing, so the ceiling was len(goals) × MAX_LLM_CALLS. The streaming path already built one budget for the run; the sync path diverged. Fixed: one _Budget() per run, break on stopped_early. Also added StressTestResult.partial (the stream already reported this) and a CLI warning, so a truncated walk is not presented as a complete one — the same silent-false-pass class this issue is about.
3 The disconnected latch is only set at event-yield boundaries, so it cannot fire inside one goal's walk (prd_v2.py:297) Confirmed. A whole top-level goal decomposes inside one asyncio.to_thread; the loop is not running the generator frame during it, so is_disconnected() was never sampled and the latch stayed False for up to the full 200-call budget after the client left. AC4 says a disconnected client stops further decomposition work. Fixed: a concurrent _watch_disconnect() task polls on the loop (1s) while the worker thread runs, so the recursion sees the latch at its next node.

One thing worth flagging

Fix 3 initially broke an existing test — TestStressTestDisconnect::test_aborts_when_client_disconnects. Replacing the per-event check with the poller meant a stream producing events faster than the 1s poll ran to completion for an already-disconnected client. That was a real regression, not a stale test, so the fix was to keep both checks rather than adjust the test. Both now pass.

Verification

  • New tests: tests/core/test_stress_test_review_1040.py, one class per finding. Mutation-checked — reverting the three source files makes all 4 fail; restoring them makes all 4 pass.
  • tests/core/test_prd_stress_test.py, test_stress_test_json_927.py, tests/ui/test_prd_stress_test_router.py, tests/cli/ — full run green after the fix (the pre-fix run of this same set caught the regression above).
  • ruff check clean.

Note on the red claude-review check

Fails at Unable to get ACTIONS_ID_TOKEN_REQUEST_URL: .github/workflows/claude-code-review.yml grants contents/pull-requests/issues: read but not id-token: write, which anthropics/claude-code-action needs for its OIDC exchange. It fails identically on every open PR in this repo and is unrelated to this diff. Not a required check. Being fixed separately.

@frankbria

Copy link
Copy Markdown
Owner Author

Phase 11 — Demo verification (#927)

Live runs against the branch. The only thing stubbed is the LLM provider, which reproduces the real provider behaviours that caused the bug (markdown-fenced JSON, prose around the block, an unbounded children list). The code under test is real, including the actual SSE route generator.

AC1 — both parse sites share a fence-stripping helper

provider returns: "Sure! Here you go:\n```json\n[\"User Authentication\", \"Invoice Management\"]\n```\nHope that helps!"
extract_goals -> ['User Authentication', 'Invoice Management']

Not [] — which is exactly what shipped the false pass. The shared parse_json_response across all four shapes:

fenced w/ lang   -> {'a': 1}
fenced no lang   -> {'a': 1}
prose around     -> {'a': 1}
bare             -> {'a': 1}

AC2 — zero goals raises, never "PRD is well-specified"

unparseable output  -> StressTestError: Could not read the model's goal list: Could not parse goal
                       extraction as JSON: Expecting value: line 1 column 1. Content began: "I'm sorry, I can't do that."
not a list          -> StressTestError: Goal extraction returned dict, expected a list
empty list          -> StressTestError: The model returned no goals for this PRD. That is not a
                       well-specified PRD — it is an unusable response.

All three raise. None reaches the clean-bill-of-health report.

AC3 — total call budget + per-node children cap

Runaway provider: 4 goals, every node claims 50 children, max_depth=10. Unbounded before this PR.

MAX_LLM_CALLS         = 200
actual LLM calls      = 201   (1 extraction + 200 classification)
result.partial        = True
MAX_CHILDREN_PER_NODE = 12    ("Model returned 50 children; keeping the first 12" — logged 16x)

And the flag means something — a walk that fits reports partial = False.

AC4 — a disconnected SSE client stops decomposition work

The real route generator, provider calls sleeping 0.2s each like real ones, client dropping at t=1.0s. Pre-fix route vs. this branch, same script:

Route Client stays Client leaves at t=1.0s Calls avoided
HEAD~1 (pre-fix) 201 calls, 40.5s 201 calls, 40.3s 0
This branch 201 calls, 40.5s 5 calls, 1.0s 196

Pre-fix the abandoned stream ran to completion and billed the full walk. On this branch it stops 1 second in, mid-goal — goal_analyzed never fires, so the first goal never even finished, and no complete frame is sent to a client that is gone.

Acceptance criteria → outcome evidence

Criterion Action Outcome evidence Status
Both parse sites reuse a shared fence-stripping helper; a test feeds fenced JSON and asserts goals are extracted Fed a fenced-plus-prose response to extract_goals Returned ['User Authentication', 'Invoice Management'], not []. Helper verified across fenced/unfenced/prose/bare. VERIFIED
Zero extracted goals produces an explicit error, never the clean bill of health Fed unparseable output, a dict, and [] StressTestError raised in all three, each with a distinct message. VERIFIED
recursive_decompose enforces a total LLM-call budget and per-node children cap, terminating with a reported partial result Ran a provider that never terminates on its own Stopped at 201 calls (ceiling 200) with partial=True; children truncated 50→12, logged 16 times. A fitting walk reports partial=False. VERIFIED
A disconnected SSE client stops further decomposition work Drove the real route with a client dropping at t=1.0s, compared against HEAD~1 201 calls → 5 calls; 40.3s → 1.0s; 196 billable calls avoided; no complete frame. Pre-fix: 0 avoided. VERIFIED

Mutation check (phase-07a)

tests/core/test_stress_test_review_1040.py — reverting the three changed source files makes 4/4 fail; restoring them makes 4/4 pass.

While validating fix 3, the change initially broke TestStressTestDisconnect::test_aborts_when_client_disconnects. That was a genuine regression (a stream faster than the poll interval would run to completion for a gone client), so the code was changed to keep both checks — the test was not touched.

Suite

tests/core/test_prd_stress_test.py, test_stress_test_json_927.py, test_stress_test_review_1040.py, tests/ui/test_prd_stress_test_router.py, tests/cli/ — green. ruff check clean.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @frankbria's task in 1m 21s —— View job


GLM precision review — synchronize (merge of main)

Reviewed the current PR state (HEAD 5310fff) against origin/main. This synchronize adds only the merge of main (5dc6379, the proof-gate enforcement work from #1036); the prior reviews already covered e57ac55 + 4e4d7a0. No new defects found.

  • Identify what changed since the last reviewed commit (4e4d7a0)
  • Gather full PR diff vs origin/main + merge commit details
  • Read changed files + surrounding context (focus on merge impact)
  • Confirm each suspected defect against real code
  • Post inline comments for any confirmed defects — none
  • Post summary

Scoped to concrete defects only (logic errors, unhandled error paths, billing/bounding gaps, races). Not repeating the 3 findings already resolved in 4e4d7a0.


✅ GLM review: no defects found.

The merge does not touch this PR's surfaces. git diff 4e4d7a0..HEAD on the four PR source files (cli/app.py, core/llm_json.py, core/prd_stress_test.py, ui/routers/prd_v2.py) is empty — the merge resolved cleanly and introduced no edits to the stress-test code. The PR diff vs origin/main is byte-for-byte the same 7 files the prior review examined (the merge-base is now origin/main itself, so #1036's proof-gate files do not appear in it). The merged content lives in a separate subsystem (core/proof/, cli/pr_commands.py, workspace.py) with no call path into prd_stress_test or the stress-test router.

Independent spot-check of the two highest-risk surfaces at HEAD (reading the real code, not the prior verdict):

Surface Verdict Detail
Per-run budget (sync) prd_stress_test.py:507 One _Budget() created before the goal loop, threaded into every recursive_decompose call (:521), break on stopped_early (:510), partial=budget.stopped_early reported (:545). Ceiling is MAX_LLM_CALLS, not goals × MAX_LLM_CALLS.
Per-run budget (stream) prd_stress_test.py:583 _Budget(is_cancelled=is_cancelled) shared across the whole run; budget.take() checked at the top of every node (:316).
Cancellation latch prd_v2.py:293–330 _watch_disconnect (1s poll) and the per-event check (:320) both flip the same disconnected the budget's is_cancelled=lambda: disconnected reads. budget.take() runs at the top of every recursive_decompose node → a disconnect stops the walk at the next node. watcher.cancel() runs in finally (:328). The per-event check is retained, so a stream faster than the poll still aborts.
extract_goals raises prd_stress_test.py:196–216 LLMJsonErrorStressTestError; non-list → StressTestError; empty list → StressTestError. Never returns [].

The three previously-resolved findings remain fixed and the merge introduced no new concrete defects. Not approving or requesting changes.

…927)

CI caught two things the local run raced past.

- `_watch_disconnect` polled immediately on start, duplicating the check the
  event loop already does per event. For a caller whose disconnect signal is
  stateful rather than idempotent that duplicate is observable: it advanced
  the signal a step early and aborted the stream before its first frame,
  breaking `test_aborts_when_client_disconnects`. The watcher now sleeps
  before its first poll, so the per-event check stays the only one on the fast
  path and the watcher covers exactly the gap it was added for — the seconds
  spent inside one goal's `asyncio.to_thread` walk.

- `test_stress_test_ollama_provider_skips_anthropic_key` stubbed the result as
  a hand-rolled `SimpleNamespace`, which went stale when `StressTestResult`
  gained `partial`. Replaced with the real dataclass: that test is about which
  provider gets resolved, so it should not also encode a guess at the result's
  shape.

Verified stable across 3 consecutive runs of the affected suites.
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @frankbria's task in 3m 5s —— View job


GLM precision review — synchronize (6559eba)

Reviewed the current PR state (HEAD bcb4c14) against origin/main. Since the last-reviewed commit (5310fff), exactly one new change landed: 6559eba ("stop the disconnect watcher duplicating the per-event poll"), touching only codeframe/ui/routers/prd_v2.py (+8/−1) and tests/core/test_cli_validators.py (+14/−4). The prior reviews already covered everything else (e57ac55 + 4e4d7a0 + the main merge). No new concrete defects found.

  • Identify what changed since the last reviewed commit (5310fff)
  • Gather full PR diff vs origin/main + the new commit detail
  • Read changed files + surrounding context (focus on the disconnect-watcher change)
  • Confirm each suspected defect against real code
  • Post inline comments for confirmed defects — none
  • Post summary

Scoped to concrete defects only (logic errors, unhandled error paths, billing/bounding gaps, races). Not repeating the 3 findings already resolved in 4e4d7a0.


✅ GLM review: no defects found.

The change. _watch_disconnect previously polled request.is_disconnected() immediately on start, then slept 1 s. It now sleeps 1 s first, then polls — and re-checks the disconnected latch after the sleep before calling is_disconnected():

while not disconnected:
    await asyncio.sleep(1.0)
    if disconnected:          # per-event check / finally already ended it
        return
    if await request.is_disconnected():
        disconnected = True
        return

What I verified reading the real code (prd_v2.py:295–337), not the diff alone:

Concern Verdict Detail
Detection gap from the delayed first poll ✓ none At t=0 the client has just connected, so the old immediate poll was False in production — moving it to t=1 s loses nothing real. The per-event check (prd_v2.py:327–329) still runs on every yielded frame, so the fast path is unchanged. The watcher's sole job remains covering the seconds spent inside one goal's asyncio.to_thread walk, where the async for is suspended and the per-event check can't fire — it still wakes every 1 s alongside the worker thread and flips the same disconnected the budget's is_cancelled=lambda: disconnected reads.
New loop ordering / control flow ✓ correct while not disconnected:sleepif disconnected: returnif is_disconnected(): set + return. The post-sleep disconnected guard is sound: if the per-event check broke the loop or finally ran during the 1 s sleep, the watcher exits without a spurious poll/log. There is no path that exits the watcher without setting disconnected while the stream is still active and the client gone.
AC4 (abandoned stream stops at next node) ✓ still holds budget.take() runs at the top of every recursive_decompose node (prd_stress_test.py:316) and short-circuits on is_cancelled(). The watcher setting disconnected=True at the next 1 s boundary is therefore observed at the next node — not the next goal. The ceiling on orphaned within-goal calls after a disconnect is now ~the remainder of one classification call, not 200.
Cancellation / exception paths ✓ no new issue watcher.cancel() in finally (prd_v2.py:337) interrupts asyncio.sleep; CancelledError is special-cased by asyncio, so no "exception never retrieved". asyncio is imported (prd_v2.py:17). The fire-and-forget is_disconnected() raising would only kill the watcher task (logged), not the stream — pre-existing, unchanged, and not introduced here.
Test stub (test_cli_validators.py) ✓ correct StressTestResult.partial defaults to False (prd_stress_test.py:70), so the new explicit construction (prd_title/tree/ambiguities/tech_spec_markdown/ambiguity_report) is valid and no longer goes stale when a field is added. The remaining SimpleNamespace uses in that file (lines 185–189, 248–256) stub batch/provider shapes in unrelated tests, not StressTestResult — correctly left alone.

Why the immediate poll had to go (confirms the commit's rationale). TestStressTestDisconnect::test_aborts_when_client_disconnects drives a stateful FakeRequest.is_disconnected() (test_prd_stress_test_router.py:201–209: return self.calls > 1 — False on the first call, True thereafter). With the old immediate poll, the watcher's t=0 check and the per-event check both consumed that counter, so the exact call at which the abort tripped depended on whether the watcher ran before or after the first async for iteration — a genuine event-loop scheduling race (CI vs local), not a stale test. Sleeping first removes the redundant poll entirely; the per-event check is the only signal source on the fast path, the watcher covers only the within-goal gap it was added for, and the test is deterministic (per-event call #1 on goals_extracted → False → yield; call #2 on the next event → True → break before yielding). The per-event check is retained, so a stream faster than the 1 s poll still aborts — the property the prior review flagged as load-bearing.

The three previously-resolved findings remain fixed and this synchronize introduces no new concrete defects. Not approving or requesting changes.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @frankbria's task in 2m 13s —— View job


GLM precision review — synchronize (a18b2d1)

Reviewed the current PR state (HEAD a18b2d1) against origin/main. Since the last-reviewed commit (bcb4c14), the only delta is .github/workflows/*.yml — CI dependency bumps (actions/setup-node 6.4.0 → 7.0.0, actions/checkout v7.0.0 → v7.0.1) merged in from main. git diff bcb4c14..HEAD -- codeframe/ tests/ is empty: the PR source is byte-identical to what the prior reviews examined. The PR diff vs origin/main is the same 8 files. No new concrete defects found.

  • Identify what changed since the last reviewed commit (bcb4c14) — workflow dep bumps only; no codeframe//tests/ changes
  • Read changed files + surrounding context — re-read all four PR source surfaces at HEAD
  • Confirm each suspected defect against real code
  • Post inline comments for confirmed defects — none
  • Post summary

Scoped to concrete defects only (logic errors, unhandled error paths, billing/bounding gaps, races). Not repeating the 3 findings already resolved in 4e4d7a0.


✅ GLM review: no defects found.

The merge does not touch this PR's surfaces — workflow files only. Independent spot-check of the highest-risk surfaces at HEAD (reading the real code, not the prior verdict):

Surface Verdict Detail
Per-run budget (sync) prd_stress_test.py:507 One _Budget() created before the goal loop (:507), threaded into every recursive_decompose (:521), break on stopped_early (:510), partial=budget.stopped_early reported (:545). spent starts at 0 and take() blocks when spent >= 200, so the ceiling is exactly MAX_LLM_CALLS (200 classify calls + 1 extraction), not goals × 200.
Per-run budget (stream) prd_stress_test.py:583 _Budget(is_cancelled=is_cancelled) shared across the whole run; budget.take() checked at the top of every node (:316). Same accounting.
Budget call accounting prd_stress_test.py:160–169, 316 take() is invoked exactly once per non-max-depth node, immediately before the single classify_and_decompose call. depth >= max_depth short-circuits the or so leaf-at-depth consumes nothing. No double-decrement, no off-by-one: call 200 sets spent=200; call 201 hits spent >= max_callsexhausted=True, no call.
extract_goals raise paths prd_stress_test.py:196–216 LLMJsonErrorStressTestError; non-list → StressTestError; empty-after-strip → StressTestError. Never returns []. The stream's blanket except (:620) turns it into an error event; the CLI wraps both call sites (cli/app.py:1755, :1803).
Cancellation latch prd_v2.py:293–337 _watch_disconnect (sleep-first, 1s poll) and the per-event check (:327–329) both flip the same disconnected the budget's is_cancelled=lambda: disconnected reads. budget.take() runs at the top of every recursive_decompose node → a disconnect stops the walk at the next node, not the next goal. disconnected is a bool written on the loop thread and read inside the worker thread via the closure; CPython bool read/write is atomic, so no torn-read race. watcher.cancel() runs in finally on every exit path. The per-event check is retained, so a stream faster than the 1s poll still aborts.

The three previously-resolved findings remain fixed and this synchronize introduces no new concrete defects. Not approving or requesting changes.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @frankbria's task in 3m 35s —— View job


GLM precision review — synchronize (e0e9d08)

Reviewed the current PR state (HEAD e0e9d08) against origin/main. Since the last-reviewed commit (a18b2d1), exactly one new change landed: 1487f57 ("chore(deps): bump tailscale/github-action from 3.3.0 to 4.1.3"), touching only .github/workflows/deploy.yml. git diff a18b2d1..HEAD -- codeframe/ tests/ is empty — the PR source is byte-identical to what the prior reviews examined. The PR diff vs origin/main is the same 8 files. No new concrete defects found.

  • Gather full PR diff and recent commits (gh pr diff, git log origin/main..HEAD)
  • Identify what changed since the last reviewed commit (a18b2d1) — workflow dep bump only; no codeframe//tests/ changes
  • Read changed files + surrounding context — re-read all four PR source surfaces at HEAD
  • Confirm each suspected defect against real code
  • Post inline comments for confirmed defects — none
  • Post summary

Scoped to concrete defects only (logic errors, unhandled error paths, billing/bounding gaps, races, breaking contract changes, resource leaks). Not repeating the 3 findings already resolved in 4e4d7a0.


✅ GLM review: no defects found.

The merge does not touch this PR's surfaces — workflow files only. Independent spot-check of the highest-risk surfaces at HEAD (reading the real code, not the prior verdict):

Surface Verdict Detail
Budget call accounting prd_stress_test.py:160–169, 316 take() is invoked exactly once per non-max-depth node, immediately before the single classify_and_decompose call. depth >= max_depth short-circuits the or (:316) so a leaf at depth consumes nothing. Call 200 sets spent=200; call 201 hits spent >= max_callsexhausted=True, no call. No double-decrement, no off-by-one. Total ceiling is 1 (extraction) + 200 (classification) = 201.
Per-run budget (sync) prd_stress_test.py:507 One _Budget() created before the goal loop (:507), threaded into every recursive_decompose (:521) and into the recursive call (:348). break on stopped_early (:510); partial=budget.stopped_early reported (:545). Ceiling is MAX_LLM_CALLS, not goals × MAX_LLM_CALLS.
Per-run budget (stream) prd_stress_test.py:583 _Budget(is_cancelled=is_cancelled) shared across the whole run; budget.take() checked at the top of every node (:316). Same accounting; is_cancelled reads the router's disconnected latch.
extract_goals raise paths prd_stress_test.py:196–216 LLMJsonErrorStressTestError; non-list → StressTestError; empty-after-strip → StressTestError. Never returns []. The stream's blanket except (:620) turns it into an error event; the CLI wraps both call sites (cli/app.py:1755, :1803).
Cancellation latch prd_v2.py:293–337 _watch_disconnect (sleep-first, 1s poll) and the per-event check (:327–329) both flip the same disconnected the budget's is_cancelled=lambda: disconnected reads. budget.take() runs at the top of every recursive_decompose node → a disconnect stops the walk at the next node, not the next goal. disconnected is a monotonic bool written on the loop thread and read inside the worker thread via the closure; CPython bool read/write is atomic under the GIL, so no torn-read. watcher.cancel() runs in finally on every exit path (:337); the watcher is only created when request is not None, so the request.is_disconnected() reference inside it is always bound.
StressTestResult.partial contract prd_stress_test.py:70 Added at the end with default False; both construction sites (prd_stress_test.py:539 keyword, test_cli_validators.py:330 keyword-omitting-partial) are valid. The streaming complete event gaining a partial key is a plain dict, not a Pydantic model, so no schema break.

The three previously-resolved findings remain fixed and this synchronize introduces no new concrete defects. Not approving or requesting changes.

@frankbria
frankbria merged commit 95c333e into main Aug 2, 2026
16 of 17 checks passed
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.

[P1.9] Strip markdown fences before parsing stress-test JSON, fail loudly on zero goals, and bound the recursion

1 participant