What happens
forgeplan reason fails on a correctly configured, logged-in claude-code provider:
$ forgeplan reason RFC-001
Analyzing RFC-001 with ADI cycle (claude-code/claude-sonnet-5)...
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`.
Error: LLM call failed
The remediation text points at authentication, which sends you to claude login, claude auth, and a re-check of .forgeplan/secrets.env. None of it is the problem — the session is logged in and claude --print works fine on its own.
Root cause
claude --print --output-format json on claude CLI 2.x returns a JSON array of stream events. The envelope is the final {"type": "result", ...} entry:
parse_envelope (crates/forgeplan-core/src/playbook/dispatch/claude_print.rs:485) decodes stdout straight into ClaudePrintResponse:
pub(crate) fn parse_envelope(stdout: &[u8]) -> Result<ClaudePrintResponse, serde_json::Error> {
let s = String::from_utf8_lossy(stdout);
serde_json::from_str(s.trim())
}
serde deserialises a JSON array into a struct positionally. The first field of ClaudePrintResponse is is_error: bool, so element 0 — {"type": "system", ...} — is fed to it, giving invalid type: map, expected a boolean at line 1 column 1. The column-1 position is what makes it look like malformed output rather than a shape mismatch.
Blast radius
parse_envelope is pub(crate) and has two consumers, so both paths are affected:
crate::llm — the claude-code provider (llm/mod.rs:247). This is the reported symptom: forgeplan reason is dead, and with it every Standard+ FPF ADI gate that runs before activation.
playbook::dispatch — AgentDispatcher and PluginDispatcher (claude_print::invoke, line 683).
The existing tests do not catch it because every fixture uses the bare-object shape (json_response() in claude_print::tests, write_mock_claude() in llm::claude_code_tests) — the shape the CLI emitted when #382 landed.
Repro
# 1. see the actual shape
echo hi | claude --print --output-format json | jq 'type' # => "array"
# 2. configure the provider — no API key needed, it reuses `claude login`
cat >> .forgeplan/config.yaml <<'YAML'
llm:
provider: claude-code
model: claude-sonnet-5
YAML
# 3. any artifact
forgeplan reason <ID>
Workaround
Switch to a provider that talks HTTP directly (claude + ANTHROPIC_API_KEY, gemini, openai). That reintroduces the separate API key and billing that #382 set out to avoid.
Environment
|
|
| forgeplan |
0.34.0 and 0.37.0 — identical failure on both |
| claude CLI |
2.1.266 |
| OS |
macOS 25.5.0 (arm64) |
Fix
PR follows: unwrap the array to its last type: "result" event before decoding, keep the bare-object path for older CLI versions, and make "array with no result event" a named error instead of a positional mis-parse.
What happens
forgeplan reasonfails on a correctly configured, logged-inclaude-codeprovider:The remediation text points at authentication, which sends you to
claude login,claude auth, and a re-check of.forgeplan/secrets.env. None of it is the problem — the session is logged in andclaude --printworks fine on its own.Root cause
claude --print --output-format jsonon claude CLI 2.x returns a JSON array of stream events. The envelope is the final{"type": "result", ...}entry:[ {"type": "system", "subtype": "init", "session_id": "...", "tools": [...]}, {"type": "assistant", "message": {...}}, {"type": "result", "subtype": "success", "is_error": false, "result": "...", "session_id": "..."} ]parse_envelope(crates/forgeplan-core/src/playbook/dispatch/claude_print.rs:485) decodes stdout straight intoClaudePrintResponse:serde deserialises a JSON array into a struct positionally. The first field of
ClaudePrintResponseisis_error: bool, so element 0 —{"type": "system", ...}— is fed to it, givinginvalid type: map, expected a boolean at line 1 column 1. The column-1 position is what makes it look like malformed output rather than a shape mismatch.Blast radius
parse_envelopeispub(crate)and has two consumers, so both paths are affected:crate::llm— theclaude-codeprovider (llm/mod.rs:247). This is the reported symptom:forgeplan reasonis dead, and with it every Standard+ FPF ADI gate that runs before activation.playbook::dispatch—AgentDispatcherandPluginDispatcher(claude_print::invoke, line 683).The existing tests do not catch it because every fixture uses the bare-object shape (
json_response()inclaude_print::tests,write_mock_claude()inllm::claude_code_tests) — the shape the CLI emitted when #382 landed.Repro
Workaround
Switch to a provider that talks HTTP directly (
claude+ANTHROPIC_API_KEY,gemini,openai). That reintroduces the separate API key and billing that #382 set out to avoid.Environment
Fix
PR follows: unwrap the array to its last
type: "result"event before decoding, keep the bare-object path for older CLI versions, and make "array with no result event" a named error instead of a positional mis-parse.