Skip to content

fix(claude-print): accept the event-array envelope claude CLI 2.x emits - #493

Open
fedorovvvv wants to merge 1 commit into
ForgePlan:mainfrom
fedorovvvv:fix/claude-print-event-array-envelope
Open

fedorovvvv wants to merge 1 commit into
ForgePlan:mainfrom
fedorovvvv:fix/claude-print-event-array-envelope

Conversation

@fedorovvvv

Copy link
Copy Markdown

Fixes #492.

The bug

claude --print --output-format json on claude CLI 2.x returns a JSON array of stream events, with the envelope as the final {"type": "result", ...} entry. parse_envelope decoded stdout straight into ClaudePrintResponse, and serde deserialises a JSON array into a struct positionally — so the first event ({"type": "system", "subtype": "init", ...}) was fed to is_error: bool:

invalid type: map, expected a boolean at line 1 column 1

Both callers report that as an authentication failure. The claude-code provider appends "Is claude logged in? Try claude login.", so a correctly configured, logged-in session looks like a broken login, and forgeplan reason — with every Standard+ FPF ADI gate behind it — is dead.

The change

parse_envelope now parses stdout into serde_json::Value first and unwraps an array to its last type: "result" event before decoding. The bare-object path is untouched, so older CLI versions keep working — the array branch is additive.

An array carrying no result event returns a named error rather than positionally mis-parsing some other event:

`claude --print --output-format json` returned an event array with no `type: "result"` event

One function changed. parse_envelope is pub(crate) with two consumers, so this fixes both the claude-code LLM provider (llm/mod.rs:247, the reported path) and playbook::dispatch's AgentDispatcher / PluginDispatcher (claude_print::invoke).

Tests

Five added, all failing before the change:

Test Covers
parses_event_array_envelope the CLI 2.x shape — the regression itself
parses_bare_object_envelope_via_helper the old shape still decodes
event_array_preserves_in_band_error is_error / api_error_status read off the result event, not event 0
event_array_without_result_event_errors_clearly named error, no silent mis-parse
claude_code_accepts_event_array_envelope end-to-end through the provider's mock-claude harness

The existing fixtures could not have caught this: json_response() and write_mock_claude() both emit the bare-object shape the CLI produced when #382 landed. The end-to-end test reuses write_mock_claude with an array payload, so the harness now covers the shape real users hit.

cargo fmt --check clean, cargo clippy -p forgeplan-core --lib clean, cargo test -p forgeplan-core --lib claude_print 35 passed / llm:: 34 passed.

Verified against the real CLI

forgeplan 0.37.0, claude CLI 2.1.266, macOS arm64 — same workspace, same artifact, same logged-in session:

# released 0.37.0
$ forgeplan reason RFC-001
Error: ADI reasoning failed: claude-code provider: failed to decode `claude --print` JSON envelope:
invalid type: map, expected a boolean at line 1 column 1. Is `claude` logged in? Try `claude login`.

# patched
$ ./target/debug/forgeplan reason RFC-001
  Analyzing RFC-001 with ADI cycle (claude-code/claude-sonnet-5)...
  { "hypotheses": [ { "id": "H1", ... } ] }     # full ADI cycle completes

Reproduced identically on 0.34.0, so this is not a 0.37.0 regression — it is the CLI's output shape moving out from under the parser.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YB7Md4NFwfDXiCcsy49PvX

`parse_envelope` decoded stdout as a bare `ClaudePrintResponse` object.
claude CLI 2.x returns `--output-format json` as a JSON *array* of stream
events whose final `{"type": "result", ...}` entry carries the envelope.

serde deserialises a JSON array into a struct positionally, so the first
event (`{"type": "system", "subtype": "init", ...}`) landed in `is_error`
and the decode failed with:

    invalid type: map, expected a boolean at line 1 column 1

Both callers surface that as an auth problem — the `claude-code` LLM
provider appends "Is `claude` logged in? Try `claude login`." — so
`forgeplan reason` (and every Standard+ FPF ADI gate behind it) is dead
on a correctly configured, logged-in session, with remediation text
pointing at the wrong thing.

Unwrap the array to its last `type: "result"` event before decoding, and
keep the bare-object path for older CLI versions. An array with no result
event is a distinct, named error rather than a positional mis-parse of
some other event.

Tests: four unit tests over `parse_envelope` (array, bare object, in-band
error inside the array, array without a result event) plus an end-to-end
test through the mock-`claude` harness in the `claude-code` provider,
which is where the failure was reported.

Verified against claude CLI 2.1.266 and forgeplan 0.37.0: `forgeplan
reason RFC-001` fails on the released binary with the message above and
completes an ADI cycle on the patched one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YB7Md4NFwfDXiCcsy49PvX
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.

claude-code provider cannot decode claude --print output: CLI 2.x returns an event array, parse_envelope expects an object

1 participant