ROADMAP #249: resumed-session slash command error envelopes omit kind field

Cycle #37 dogfood finding post-#247 merge. Two Err arms in the resumed-session
JSON path at main.rs:2747 and main.rs:2783 emit error envelopes WITHOUT the
`kind` field required by the §4.44 typed-envelope contract.

## The Pinpoint

Probed resumed-session slash command JSON path:

  $ claw --output-format json --resume latest /session
  {"command":"/session","error":"unsupported resumed slash command","type":"error"}
  # no kind field

  $ claw --output-format json --resume latest /xyz-unknown
  {"command":"/xyz-unknown","error":"Unknown slash command: /xyz-unknown\n  Help             /help lists available slash commands","type":"error"}
  # no kind field AND multi-line error without split hint

Compare to happy path which DOES include kind:
  $ claw --output-format json --resume latest /session list
  {"active":"...","kind":"session_list",...}

Contract awareness exists. It's just not applied in the Err arms.

## Scope

Two atomic fixes in main.rs:
- Line 2747: SlashCommand::parse() Err → add kind via classify_error_kind()
- Line 2783: run_resume_command() Err → add kind + call split_error_hint()

~15 lines Rust total. Bounded.

## Family Classification

§4.44 typed-envelope contract sweep:
- #179 (parse-error real message quality) — closed
- #181 (envelope exit_code matches process exit) — closed
- #247 (classify_error_kind misses prompt-patterns) — closed
- #248 (verb-qualified unknown option errors) — in-flight (another agent)
- **#249 (resumed-session slash error envelopes omit kind) — filed**

Natural bundle #247+#248+#249: classifier/envelope completeness across all
three CLI paths (top-level parse, subcommand options, resumed-session slash).

## Discipline

Per cycle #24 calibration:
- Red-state bug? ✗ (errors surfaced, exit codes correct)
- Real friction? ✓ (typed-error contract violation; claws dispatching on
  error.kind get undefined for all resumed slash-command errors)
- Evidence-backed? ✓ (dogfood probe + code trace identified both Err arms)
- Implementation cost? ~15 lines (bounded)
- Same-cycle fix? ✗ (Rust change, deferred per file-not-fix discipline)

## Not Implementing This Cycle

Per the boundary discipline established in cycle #36: I don't touch another
agent's in-flight work, and I don't implement a Rust fix same-cycle when
the pattern is "file + document + let owner/maintainer decide."

Filing with concrete fix shape is the correct output. If demand or red-state
symptoms arrive, implementation can follow the same path as #247: file →
fix in branch → review → merge.

Source: Jobdori cycle #37 proactive dogfood in response to Clawhip pinpoint
nudge at msg 1496518474019639408.
This commit is contained in:
YeonGyu-Kim 2026-04-22 23:33:50 +09:00
parent 84466bbb6c
commit 5f8d1b92a6

View File

@ -6670,3 +6670,103 @@ Two atomic changes:
- #179 (parse-error real message quality) — claws consuming envelope expect truthful error - #179 (parse-error real message quality) — claws consuming envelope expect truthful error
- #181 (envelope.exit_code matches process exit) — cross-channel truthfulness - #181 (envelope.exit_code matches process exit) — cross-channel truthfulness
- #30 (cycle #30: OPT_OUT rejection tests) — classification contracts deserve regression tests - #30 (cycle #30: OPT_OUT rejection tests) — classification contracts deserve regression tests
---
## Pinpoint #249. Resumed-session slash command error envelopes omit `kind` field — typed-error contract violation at `main.rs:2747` and `main.rs:2783`
**Gap.** The typed-error envelope contract (§4.44) specifies every error envelope MUST include a `kind` field so claws can dispatch without regex-scraping prose. The `--output-format json` path for resumed-session slash commands has TWO branches that emit error envelopes WITHOUT `kind`:
1. **`main.rs:2747-2760`** (`SlashCommand::parse()` Err arm) — triggered when the raw command string is malformed or references an invalid slash structure. Fires for inputs like `claw --resume latest /session` (valid name, missing required subcommand arg).
2. **`main.rs:2783-2793`** (`run_resume_command()` Err arm) — triggered when the slash command dispatch returns an error (including `SlashCommand::Unknown`). Fires for inputs like `claw --resume latest /xyz-unknown`.
Both arms emit JSON envelopes of shape `{type, error, command}` but NOT `kind`, defeating typed-error dispatch for any claw routing on `error.kind`.
Also observed: the `/xyz-unknown` path embeds a multi-line error string (`Unknown slash command: /xyz-unknown\n Help ...`) directly into the `error` field without splitting the runbook hint into a separate `hint` field (per #77 `split_error_hint()` convention). JSON consumers get embedded newlines in the error string.
**Repro.** Dogfooded 2026-04-22 on main HEAD `84466bb` (cycle #37, post-#247 merge):
```bash
$ cd /Users/yeongyu/clawd/claw-code/rust
$ ./target/debug/claw --output-format json --resume latest /session
{"command":"/session","error":"unsupported resumed slash command","type":"error"}
# Observation: no `kind` field. Claws dispatching on error.kind get undefined.
$ ./target/debug/claw --output-format json --resume latest /xyz-unknown
{"command":"/xyz-unknown","error":"Unknown slash command: /xyz-unknown
Help /help lists available slash commands","type":"error"}
# Observation: no `kind` field AND multi-line error without split hint.
$ ./target/debug/claw --output-format json --resume latest /session list
{"active":"session-...","kind":"session_list",...}
# Comparison: happy path DOES include kind field. Only the error path omits it.
```
Contrast with the `Ok(None)` arm at `main.rs:2735-2742` which DOES include `kind: "unsupported_resumed_command"` — proving the contract awareness exists, just not applied consistently across all Err arms.
**Impact.**
1. **Typed-error dispatch broken for slash-command errors.** A claw reading `{"type":"error", "error":"..."}` and switching on `error.kind` gets `undefined` for any resumed slash-command error. Must fall back to substring matching the `error` field, defeating the point of typed errors.
2. **Family-internal inconsistency.** The same error path (`eprintln!` → exit(2)) has three arms: `Ok(None)` sets kind, `Err(error)` (parse) doesn't, `Err(error)` (dispatch) doesn't. Random omission is worse than uniform absence because claws can't tell whether they're hitting a kind-less arm or an untyped category.
3. **Hint embedded in error field.** The `/xyz-unknown` path gets its runbook text inside the `error` string instead of a separate `hint` field, forcing consumers to post-process the message.
**Recommended fix shape.**
Two small, atomic edits in `main.rs`:
1. **Parse-error envelope** (line 2747): Add `"kind": "cli_parse"` to the JSON object. Optionally call `classify_error_kind(&error.to_string())` to get a more specific kind.
2. **Dispatch-error envelope** (line 2783): Same treatment. Classify using `classify_error_kind()`. Additionally, call `split_error_hint()` on `error.to_string()` to separate the short reason from any embedded hint (matches #77 convention used elsewhere).
```rust
// Before (line 2747):
serde_json::json!({
"type": "error",
"error": error.to_string(),
"command": raw_command,
})
// After:
let message = error.to_string();
let kind = classify_error_kind(&message);
let (short_reason, hint) = split_error_hint(&message);
serde_json::json!({
"type": "error",
"error": short_reason,
"hint": hint,
"kind": kind,
"command": raw_command,
})
```
**Regression coverage.** Add integration tests in `tests/output_format_contract.rs`:
- `resumed_session_bare_slash_name_emits_kind_field_249``/session` without subcommand
- `resumed_session_unknown_slash_emits_kind_field_249``/xyz-unknown`
- `resumed_session_unknown_slash_splits_hint_249` — multi-line error gets hint split
- Regression guard: `resumed_session_happy_path_session_list_unchanged_249` — confirm `/session list` JSON unchanged
**Blocker.** None. ~15 lines Rust, bounded.
**Priority.** Medium. Not red-state (errors ARE surfaced, exit code IS 2), but typed-error contract violation. Any claw doing `error.kind` dispatch on slash-command paths currently falls through to `undefined`.
**Source.** Jobdori cycle #37 proactive dogfood 2026-04-22 23:15 KST in response to Clawhip pinpoint nudge. Probed slash-command JSON error envelopes post-#247 merge; found two Err arms emitting envelopes without `kind`. Joins §4.44 typed-envelope family:
- #179 (parse-error real message quality) — closed
- #181 (envelope exit_code matches process exit) — closed
- #247 (classify_error_kind misses prompt-patterns + hint drop) — closed (cycle #34/#36)
- **#248 (verb-qualified unknown option errors misclassified) — in-flight (another agent)**
- **#249 (this: resumed-session slash command envelopes omit kind) — filed**
Natural bundle: **#247 + #248 + #249** — classifier/envelope completeness sweep. All three fix the same kind of drift: typed-error envelopes missing or mis-set `kind` field on specific CLI paths. When all three land, the typed-envelope contract is uniformly applied across:
- Top-level CLI argument parsing (#247)
- Subcommand option parsing (#248)
- Resumed-session slash command dispatch (#249)
**Related prior work.**
- §4.44 typed error envelope contract (2026-04-20)
- #77 split_error_hint() — should be applied to slash-command error path too
- #247 (model: add classifier branches + ensure envelope carries them)