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: idle → action: waiting-on-meatbag (observed on wave-status waiting) — see if that transition's success-response builder differs from others
Implementation Steps
-
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.
-
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.
-
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).
-
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).
-
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."
-
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.
-
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
Acceptance Criteria
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
Summary
Multiple
wave-statusCLI subcommands consistently fail withUnexpected 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 surfacesok:falsedespite 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 startwave-status close-issue <N>→ errored after marking the issue closed in state.jsonwave-status record-mr <N> <url>→ errored after recording the MR URLwave-status flight-done <N>→ errored after marking the flight completewave-status waiting '<reason>'→ errored after setting action towaiting-on-meatbagwith the reason persistedwave-status wavemachine-stop→ errored (thoughwavemachine_activewas alreadyNoneby that point)In each case: inspect
.claude/status/state.jsonbefore 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 normalizersrc/wave_status/state.py— anysave_jsonwrapper that returns a serialized string instead of the new state dictaction: idle→action: waiting-on-meatbag(observed onwave-status waiting) — see if that transition's success-response builder differs from othersImplementation Steps
Locate the response-shaping gap (src/wave_status/main.py, lines 141–247):
flight(args.n, root)but discards the return value; line 145 immediately calls_regenerate_dashboard(root)instead of printing the response.Extract the expected response shape (src/wave_status/state.py):
flight(),flight_done(),close_issue(),record_mr(),waiting(), andwavemachine_stop()all returndictobjects representing the updated state.Identify the response envelope format:
defer,defer-accept,set-current,set-kahuna-branch) handle their responses.{"ok": true, "state": {...}}or similar).Apply response-printing to each affected subcommand handler:
_cmd_flight,_cmd_flight_done,_cmd_close_issue,_cmd_record_mr,_cmd_waiting,_cmd_wavemachine_stop(and optionally defer-related handlers for consistency).print(json.dumps({"ok": True, "state": result_dict}))(or match existing envelope shape).Guard against dashboard regeneration crashes:
{ok: true}even if post-write dashboard generation fails, matching the hypothesis that "write succeeds, envelope crashes."Verify no .get() calls on strings (src/wave_status/main.py, state.py):
response.get()orresult.get()where the variable might be a string.'str' object has no attribute 'get'suggests code calling.get()on a non-dict return value.response.get(key)will crash.Test coverage:
Tests
Acceptance Criteria
wave-status flight,close-issue,record-mr,flight-done,waiting,wavemachine-stopall return exit code 0 on successful state mutationwave_flight,wave_close_issue, etc.) surface{ok: true, ...}to callers when state.json was updated successfullyDependencies
None.
Metadata
/wavemachineexecution of waves pa-4 / pa-5 / pa-6 on 2026-04-26