Skip to content

bug(wave-status): post-write response-shaping crashes with 'str object has no attribute .get()' on multiple CLI subcommands #495

@bakeb7j0

Description

@bakeb7j0

Summary

Multiple wave-status CLI subcommands consistently fail with Unexpected error: 'str' object has no attribute 'get' AFTER performing their underlying write/state mutation. The state.json persists correctly — only the response envelope shaping crashes. But because the CLI exits non-zero, any MCP tool that shells out to it surfaces ok:false despite the operation having actually succeeded.

Impact

Observed during wavemachine runs of waves pa-4 / pa-5 / pa-6 in mcp-server-sdlc on 2026-04-26:

  • wave-status flight 1 → errored after registering the flight start
  • wave-status close-issue <N> → errored after marking the issue closed in state.json
  • wave-status record-mr <N> <url> → errored after recording the MR URL
  • wave-status flight-done <N> → errored after marking the flight complete
  • wave-status waiting '<reason>' → errored after setting action to waiting-on-meatbag with the reason persisted
  • wave-status wavemachine-stop → errored (though wavemachine_active was already None by that point)

In each case: inspect .claude/status/state.json before and after the call — the intended state change is present. The failure is strictly in what the CLI returns to stdout/stderr.

Downstream effect: the sdlc-server MCP tools (mcp__sdlc-server__wave_flight, wave_close_issue, wave_record_mr, wave_flight_done, wave_waiting) all propagate {ok:false, error:"Command failed: wave-status ...\nUnexpected error: 0"} (or similar) to callers. Prime(post-flight) and Prime(post-wave) sub-agents see these as failures and have to do exception-handling gymnastics to proceed. The Orchestrator skill docs describe these tools as transactional, but the reality is "write succeeds, envelope crashes."

Hypothesis (unverified)

The error text 'str' object has no attribute 'get' suggests the response-shaping code calls .get(key) on a value that is supposed to be a dict but is actually a string. One candidate: some code path returns "ok" (a string) where the caller expects {"ok": True, ...} (a dict). Specific candidate paths worth spot-checking:

  • src/wave_status/cli.py — the command dispatch layer's response normalizer
  • src/wave_status/state.py — any save_json wrapper that returns a serialized string instead of the new state dict
  • The transition from action: idleaction: waiting-on-meatbag (observed on wave-status waiting) — see if that transition's success-response builder differs from others

Implementation Steps

  1. Locate the response-shaping gap (src/wave_status/main.py, lines 141–247):

    • Each subcommand handler (_cmd_flight, _cmd_flight_done, _cmd_close_issue, _cmd_record_mr, _cmd_waiting, _cmd_wavemachine_stop, etc.) calls its state mutation function but does NOT capture or return the resulting dict.
    • Example: line 144 calls flight(args.n, root) but discards the return value; line 145 immediately calls _regenerate_dashboard(root) instead of printing the response.
  2. Extract the expected response shape (src/wave_status/state.py):

    • Confirm that functions flight(), flight_done(), close_issue(), record_mr(), waiting(), and wavemachine_stop() all return dict objects representing the updated state.
    • Each dict is the mutated state_data or flights data; consistency check: all functions at lines 798, 852, 934, 1009, 692, 781 should return a dict, not a string or None.
  3. Identify the response envelope format:

    • Check how other "mutation" CLI subcommands (e.g., defer, defer-accept, set-current, set-kahuna-branch) handle their responses.
    • These do NOT print JSON either (lines 199–233), meaning the response envelope convention may not yet be established. Check test fixtures or MCP tool wrappers in mcp-server-sdlc to infer the expected shape (likely {"ok": true, "state": {...}} or similar).
  4. Apply response-printing to each affected subcommand handler:

    • Modify _cmd_flight, _cmd_flight_done, _cmd_close_issue, _cmd_record_mr, _cmd_waiting, _cmd_wavemachine_stop (and optionally defer-related handlers for consistency).
    • Capture the return value from the state mutation function.
    • Print it as JSON to stdout AFTER _regenerate_dashboard() completes (to avoid serializing incompletely-updated state).
    • Pattern: print(json.dumps({"ok": True, "state": result_dict})) (or match existing envelope shape).
  5. Guard against dashboard regeneration crashes:

    • Wrap _regenerate_dashboard() calls in try/except; if it fails, still output the JSON response (the state.json was already persisted successfully by save_json()).
    • This ensures MCP tools see {ok: true} even if post-write dashboard generation fails, matching the hypothesis that "write succeeds, envelope crashes."
  6. Verify no .get() calls on strings (src/wave_status/main.py, state.py):

    • Search for patterns like response.get() or result.get() where the variable might be a string.
    • The error 'str' object has no attribute 'get' suggests code calling .get() on a non-dict return value.
    • Likely culprit: if a function accidentally returns the JSON-stringified version instead of the dict, a later response.get(key) will crash.
  7. Test coverage:

    • Write contract tests invoking each subcommand via subprocess; assert exit code 0 and valid JSON on stdout.
    • Verify state.json persists correctly even if the CLI crashes during dashboard regeneration.

Tests

  • Unit test: every subcommand returns a parseable JSON object to stdout on success, not a string or None
  • Contract test: invoke each subcommand from subprocess; assert exit code 0 and parseable JSON response
  • Regression test: the specific state transitions that crashed above (flight-start, close-issue, record-mr, flight-done, waiting)

Acceptance Criteria

  • wave-status flight, close-issue, record-mr, flight-done, waiting, wavemachine-stop all return exit code 0 on successful state mutation
  • Each returns a parseable JSON envelope (shape TBD — match whatever other CLI subcommands already return)
  • Downstream sdlc-server MCP tool wrappers (wave_flight, wave_close_issue, etc.) surface {ok: true, ...} to callers when state.json was updated successfully
  • The existing post-write state mutation remains unchanged — this is strictly a response-shaping fix

Dependencies

None.

Metadata

  • Phase: post-Phase-1 retrofit observability follow-up
  • Source: Observed during /wavemachine execution of waves pa-4 / pa-5 / pa-6 on 2026-04-26
  • Repository: Wave-Engineering/claudecode-workflow

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions