Skip to content

Pass Claude's structured answer to the plan parser unchanged (#166) - #169

Merged
mchwang merged 10 commits into
mainfrom
feat/166-claude-structured-parse
Oct 19, 2026
Merged

mchwang merged 10 commits into
mainfrom
feat/166-claude-structured-parse

Conversation

@mchwang

@mchwang mchwang commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Closes #166.

Built on #168 (merged as dd35566), which added the agent-adapter test that described the old re-encoding; this PR replaces it.

Summary

parseClaudeOutput read Claude's envelope with JSON.parse and returned JSON.stringify(structured_output). That round trip resolved duplicate keys (last wins) and rounded inexact integers before parseV1 saw the text, which bypassed the v1 parsing contract (schema/versions/1/semantics.md, "Deterministic input parsing").

  • agents/adapters/claude.ts: after JSON.parse validates the envelope, topLevelMembers cuts the exact source text of each top-level member. Planning answers return the exact text of structured_output, so parseV1 refuses a duplicate key or an inexact integer the envelope holds.
  • A planning envelope that repeats a top-level key is refused as malformed. The check runs before classification, so a repeated is_error cannot turn a refusal into a provider failure.
  • Every scanner loop is bounded by the input length. Non-object envelopes never reach the scanner, and could not hang it if they did.
  • The 1 MiB parser limit now applies to the bytes of structured_output in the envelope, not to a re-encoding.
  • Unchanged: question (non-schema) answers, provider-failure handling for well-formed envelopes, the supervisor's decoded-size checks (the slice is never longer than the raw output).

Limit

The Claude API and CLI may resolve duplicates or number spellings before writing the envelope. That is not verified: the real-CLI probes need credentials and do not run in CI. The docs (docs/implementation/agent-isolation.md) and the code comment say so. This PR guarantees that codeboost itself no longer normalizes the answer.

The recorded planning fixtures need no re-recording: their staleness hash covers the recorded repository files and argv only, and their text remains valid validator input.

Validation

Base main dd3556640b005bac0d47fa94f4ab55bb1bc4fdb9 (#168's squash; its tree equals #168's final head 6b42931, so e9ba497 merged it with -s ours after checking), head e9ba497bcafd6b14491974467808d141c04296e1 (current head). The tree of e9ba497 is identical to 952a200's, where the full suite ran.

Gate Result
Typecheck Pass
Full suite without Docker (vitest --project parallel) 2369/2369 (81 files) on 952a200, the same tree as e9ba497
Docker suites Not affected by this diff; not run locally. CI real-docker is the gate
CI test, real-docker (required on main) pass on e9ba497

Five tests fail against the base decoder: exact text, duplicate key, inexact integer, repeated top-level key, and the 1 MiB limit on the raw bytes of structured_output. Deliberate breaks of escape skipping, string skipping in nested values, the repeat refusal, its ordering before classification, key decoding, whitespace handling and the non-object guard each fail a test, within seconds: comparisons of megabyte answers check the length and then use ===, so a broken decoder never stalls in a long vitest diff.

Earlier validated head 083d916 on base f9c2e6b: typecheck pass; 2034/2034 without Docker.

Load-related failure, separate from product results: one full run at load average ~150 had a timing failure in pre-merge.test.ts ("stops an active command check when the operation-wide deadline expires"), a file this PR does not touch. It passed 3/3 alone and in the full rerun.

Review record

Round Reviewer Pair Findings Changed / declined
1 Independent agent (20,000-case fuzz) f9c2e6b / uncommitted Repeated is_error classified before the repeat check; untested spacing, key spellings and raw-byte limit; assertion wording; doc precision; long lines Fixed
2 Independent agent (200,000-case fuzz) same Doc wrapping Fixed. Declined: the TypeError on a null envelope (unchanged from the base); question answers keeping last-wins (plain text, never parsed by parseV1)
3 Independent agent same Non-object guard untested and the scanner could hang without it; "exact text Claude wrote" overclaims what the CLI preserves Loops bounded by length; non-object tests added; claims narrowed to the envelope, and the API/CLI behaviour documented as unverified
4 Independent agent same Only inexact integers are refused, not any inexact number; one non-object case did not exercise the guard Fixed
5 Independent agent f9c2e6b / tree committed as 083d916 None Clean pass
merge #168 — 6b42931 / f2a5bcb Merged #168's head (which now includes main 7fd4dfe) with no conflicts —
6 Independent agent 6b42931 / f2a5bcb A megabyte comparison made a broken decoder hang in a vitest diff instead of failing; a repeated key with an error is refused as malformed (accepted: documented) b3b028b (diff-free comparison)
7 Independent agent 6b42931 / b3b028b One more megabyte comparison with a diff; the change also hit a small test; a misleading comment 952a200
8 Independent agent (50,000-case fuzz in round 7) 6b42931 / 952a200 Clean; the "four core tests" count corrected to five here —
merge main — dd35566 / e9ba497 #168 squash-merged; main's tree equals 6b42931, so merged with -s ours; PR diff unchanged —
9 Independent agent dd35566 / e9ba497 Clean (125/125 in 4 files; 8 mutations each caught) —
10 Independent agent (100,000-case differential fuzz) dd35566 / e9ba497 Clean; every caller checked (only planning uses the scanner) —
Copilot 1 copilot-pull-request-reviewer dd35566 / e9ba497 Requested after rounds 9–10 were clean and CI passed. No open issues; approval recommended —

Review-lesson audit

  • Overclaiming wording in docs and test titles: covered by CLAUDE.md "Treat every behavioural claim … as something to verify".
  • A loop that can hang only when a guard is removed: one-off defensive hardening of this scanner.
  • Unverifiable upstream behaviour: recorded as untested in the docs (CLAUDE.md, behavioural claims rule).
  • A test that hung, instead of failing, when the feature broke (a vitest diff of megabyte strings): covered by the Design system and plan format #1 rule of tests (a test must fail when its feature breaks) and recorded here; one-off.
    No new AGENTS.md rule is needed.

🤖 Generated with Claude Code

mchwang and others added 9 commits October 12, 2026 09:24
Every version in schema/versions.json is now compiled from its retained
snapshots, and plans and edit replies are checked by the version they
declare. The loader refuses a registry that disagrees with the snapshots
or validators, and treats a version with no validator as unsupported.

Pin the released v1 files by SHA-256, keep them LF on checkout, and add
the remaining #6 tests: path identity and Git mode mapping, hostile text
in every authoring prompt, and output limits at the exact byte.

Runtime gaps move to #162-#166.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parseClaudeOutput re-encoded structured_output with JSON.parse and
JSON.stringify, so a duplicate key or an inexact integer reached
parseV1 already resolved. Cut the exact text of structured_output from
the envelope with a length-bounded scanner instead, and refuse a
planning envelope that repeats a top-level key before classifying it.

Whether the Claude API or CLI resolves duplicates before writing the
envelope is not verified; the docs say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves the test/review.test.ts import conflict by keeping both imports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ctured-parse

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s at once

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mchwang
mchwang changed the base branch from feat/6-plan-library-alignment to main October 19, 2026 07:47
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI 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.

🟢 Approval recommended

The bounded extraction and targeted regressions support the parsing contract, with no unresolved findings.

No open issues 🎉

What changed in this PR

Fixes #166 by preserving Claude’s structured answer unchanged so the existing plan parser can reject duplicate keys and inexact integers.

Changes:

  • Extracts exact structured_output text and rejects repeated envelope keys.
  • Adds parsing and raw-byte limit regressions.
  • Documents preservation guarantees and unverified upstream behavior.
File Description
test/​planning-extraction.test.ts Avoids expensive diffs for megabyte assertions.
test/​agent-adapter.test.ts Covers exact extraction, malformed envelopes, and parser limits.
docs/​implementation/​plan-library-alignment.md Records the #166 fix and updated limit behavior.
docs/​implementation/​agent-isolation.md Clarifies extraction guarantees and upstream limitations.
agents/​adapters/​claude.ts Preserves structured answer text without re-encoding.

Review effort: Balanced

@mchwang
mchwang marked this pull request as ready for review October 19, 2026 15:28
@mchwang
mchwang merged commit 7cd81a8 into main Oct 19, 2026
3 checks passed
@mchwang
mchwang deleted the feat/166-claude-structured-parse branch October 19, 2026 15:28
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.

Parse Claude planning answers under the v1 parsing contract

2 participants