mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-27 07:45:08 +08:00
roadmap: #266 filed
This commit is contained in:
27
ROADMAP.md
27
ROADMAP.md
@@ -16933,3 +16933,30 @@ Gap. This is an **event/log opacity gap at the CLI output layer**, distinct from
|
||||
Required fix shape: (a) add `CliOutputFormat::StreamJson` parsed from `--output-format stream-json` and documented in help; (b) add a `run_prompt_stream_json` dispatch path that emits JSON Lines with stable event names (`message_start`, `text_delta`, `tool_use`, `tool_result`, `usage_delta`, `prompt_cache`, `auto_compaction`, `turn_budget_warning`, `turn_budget_exhausted`, `message_stop`, `error`, `final_summary`); (c) ensure human Markdown rendering is disabled or explicitly separated when `stream-json` is active so stdout remains valid JSONL; (d) include stable sequence numbers and timestamps so consumers can reconstruct order without scraping; (e) add tests that `--output-format stream-json` is accepted, stdout is JSONL-only, tool-use and final-summary events both appear, and runtime errors are emitted as typed `error` events before non-zero exit. Acceptance: a downstream claw can run `claw --output-format stream-json -p "..."` and react to tool/budget/compaction/error events before the final assistant message, with no terminal-prose scraping.
|
||||
|
||||
**Status:** Open. No source code changed. Filed 2026-04-26 13:30 KST. Branch: feat/jobdori-168c-emission-routing. HEAD: `d5568eb` before filing. Cluster delta: CLI-event-stream-observability +1; prerequisite output lane for #260/#264 follow-up implementation. Concrete delta this cycle: ROADMAP-only pinpoint appended after live help/parse/source verification.
|
||||
|
||||
|
||||
## Pinpoint #266 — `RuntimeErrorKind` typed-error-kind enum is structurally absent: `RuntimeError` is a single-field `{ message: String }` newtype with zero typed discriminants, and the CLI compensates by reverse-engineering the discriminant downstream via a 22-branch substring-matching `classify_error_kind(message: &str) -> &'static str` function — the runtime throws away typed information at construction, then the CLI scrapes it back via prose-pattern-matching
|
||||
|
||||
Dogfooded 2026-04-26 13:35 KST on `feat/jobdori-168c-emission-routing` at HEAD `8975354` (post-rebase fast-forward onto gaebal-gajae's #265 `--output-format stream-json` lane-absent pinpoint). #264 audited the **Turn-budget primitive** runtime layer and noted in passing that `RuntimeError` lacks a `kind: RuntimeErrorKind` field and named `TurnBudgetExhausted` as a future variant alongside `ApiError`/`SessionError`/`HookError`. #266 is the **dedicated structural audit of that typed-error-taxonomy gap itself** — sister pinpoint to #264, founding the **Typed-error-kind-enumeration cluster** with #266 as solo founder.
|
||||
|
||||
Verified concrete surface (all paths absolute from `rust/crates/`): `RuntimeError` is defined at `runtime/src/conversation.rs:87-93` as `pub struct RuntimeError { message: String }` with a single `RuntimeError::new(impl Into<String>)` constructor at lines 91-97 and `Display`/`std::error::Error` impls at lines 100-106. There is **zero** `RuntimeErrorKind` enum, **zero** `RuntimeError::kind()` accessor, **zero** `kind: RuntimeErrorKind` field, **zero** typed discriminant, **zero** machine-readable reason, and **zero** structured payload (`iterations`, `max`, `path`, `operation`, `retryable`, etc.). `rg "pub enum RuntimeError\|RuntimeErrorKind" rust/crates/` returns no matches anywhere in the workspace. The sibling type `ToolError` at `conversation.rs:64-83` shares the same single-field `{ message: String }` shape — the typed-error gap is symmetric across both runtime error types.
|
||||
|
||||
Construction-site count: `rg 'RuntimeError::new' rust/crates/` returns **20 call sites** total (12 inside `runtime/src/conversation.rs`, 8 inside `rusty-claude-cli/src/main.rs`). Every single one passes a free-form `String` or `format!(...)` expression — no construction site emits a typed discriminant. Representative examples: `conversation.rs:324` (`format!("conversation loop exceeded the maximum number of iterations")`), `conversation.rs:740` (`"assistant stream produced no content"`), `main.rs:7976`/`7997`/`8007`/`8125` (`format_user_visible_api_error(...)` — API failures collapsed into prose), `main.rs:8000` (`"post-tool continuation nudge exhausted"`), `main.rs:8951`/`8968` (filesystem operations: `error.to_string()`).
|
||||
|
||||
Downstream counter-evidence — `classify_error_kind(message: &str) -> &'static str` at `rusty-claude-cli/src/main.rs:270-348` (78 lines, **22 substring-match branches**, called from `main.rs:215` (panic-handler tagging), `:243`/`:245`/`:249` (top-level error printer for both `text` and `json` modes), and `:2982` (mid-run error envelope). Branch enumeration: `missing_credentials`, `filesystem_io_error`, `missing_manifests`, `missing_worker_state`, `session_not_found`, `session_load_failed`, `no_managed_sessions`, `cli_parse` (×7 distinct substring patterns: `unrecognized argument`, `unknown option`, `prompt subcommand requires`, starts-with `empty prompt:`, `unsupported value for --`, `missing value for --`, `unsupported permission mode`, `invalid value for --`, `model string cannot be empty`, `unexpected extra arguments after \\`claw`), `slash_command_requires_repl`, `invalid_model_syntax`, `unsupported_command`, `unsupported_resumed_command`, `confirmation_required`, `api_http_error` (substring-OR over `api failed` / `api returned`), and a fall-through to `unknown`. Each branch carries inline comments referencing the historical pinpoint that motivated it (`// #169`, `// #170`, `// #171`, `// #247`, `// #130b`) — proving the function has accreted patterns one-pinpoint-at-a-time as new error prose shapes leaked in.
|
||||
|
||||
Downstream consumption: `--output-format json` envelope at `main.rs:215`/`:245`/`:249` emits `{ "type": "error", "error": "<bare prose>", "kind": "<classify_error_kind result>" }` where the `kind` field is recovered AT THE CLI BOUNDARY via the substring scrape, **not propagated from a typed runtime field**. The runtime never had the discriminant; the CLI invents it back. ROADMAP §4.44 (lines 758-785) and ROADMAP #130 (lines 4978-5122) both explicitly note this gap as a typed-error contract debt — #130's New evidence section at line 5120 calls out exactly this: "the typed-error contract is thus twice-broken on this path: (a) the io::ErrorKind information is discarded at the `?` in `run_export()`, AND (b) the flat `io::Error::Display` string is then fed to a classifier that has no patterns for filesystem errno strings." Neither §4.44 nor #130 audited the **`RuntimeErrorKind` enum itself** as the structurally-absent primitive; both treated the gap as classifier-pattern-missing rather than as **typed-discriminant-missing-at-the-source**.
|
||||
|
||||
Gap. The runtime treats the error class as a **stringly-typed value** rather than a typed enum. The classifier function is a **lossy reverse-decompiler** of information that should have been carried as a typed field from `RuntimeError` construction through the CLI emit. Two structural failures: (1) **forward-direction loss** — every `RuntimeError::new(...)` call site already knows the kind at the source (e.g., `conversation.rs:324` knows it's iteration-exhaustion, `main.rs:8000` knows it's post-tool-nudge-exhaustion, `main.rs:7976` knows it's API failure) but throws that knowledge away by collapsing into a `String`; (2) **reverse-direction fragility** — the CLI substring-scrape can mis-classify any error whose prose accidentally matches a pattern from a different error class (e.g., a legitimate API error containing the literal text `"unknown option"` would be misclassified as `cli_parse`), and silently degrades to `"unknown"` for any error class that has not yet been patched into the classifier. The 22-branch accretion is itself counter-evidence: every new error class needs both a `RuntimeError::new("<unique prose>")` site AND a corresponding `classify_error_kind` substring branch, with no compiler enforcement that the two stay in sync. New error classes that ship without a classifier branch silently fall through to `"unknown"`.
|
||||
|
||||
Cluster shape novelty: founds the **NEW Typed-error-kind-enumeration cluster** with #266 as solo founder. The cluster catalogues missing typed discriminants at the runtime-error-taxonomy axis: `RuntimeErrorKind` enum, `ToolErrorKind` enum, per-variant typed payloads (`TurnBudgetExhausted { iterations, max }`, `IterationBudgetExhausted { iterations, max }`, `ApiError { status, retryable }`, `SessionError { kind, path }`, `HookError { hook_name, exit_code }`, `FilesystemError { path, operation, errno }`, `ParseError { argv_index, raw }`), and the structural removal of `classify_error_kind` as a CLI-side reverse-decompiler in favor of typed field propagation. Sister to #264 (Turn-budget primitive cluster); the two pinpoints form a **second complementary-pinpoint-pair-bundle** following the #245+#250 (WebSearch client/server) and #262+#264 (turn-budget CLI-parse/runtime-primitive) pattern. #264 catalogues a single missing typed-event primitive; #266 catalogues the missing typed-discriminant-axis that ALL runtime errors (turn-budget, API, session, hook, filesystem, etc.) need in order to express themselves typedly. #266 is the **prerequisite layer** #264's `TurnBudgetExhausted` variant would land into.
|
||||
|
||||
Distinct from #260/#263/#265 (output-mode/help-text-contract gaps at the CLI output layer). Distinct from §4.44 (which proposed a typed envelope at the JSON boundary but did not audit the absent runtime enum at the source). Distinct from #130/#130b (which catalogued context-loss at filesystem `?` propagation and classifier-pattern-missing at the CLI, but did not catalogue the absent enum). Distinct from the silent-fallback family (#207/#208/#222/#231/#236/#246/#249/#258/#260/#262 — silent input/output mutation at boundaries; #266 is about the **type system itself missing a discriminant axis**).
|
||||
|
||||
Discovery-pattern continuation: this is the **third complementary-pinpoint-pair-bundle** in the dogfood corpus (#245+#250, #262+#264, now #264+#266), and the **second consecutive cycle pair-bundle** (#264 filed 13:05 KST, #266 filed 13:35 KST same cycle-day) — confirming complementary-pair-bundles as a stable discovery-pattern that systematically expands when a single-layer pinpoint is filed. Pair-bundle ratio: 3 of 67 pinpoints in the #200-range (≈4.5%) are bundled — small but consistent. Founds NEW cluster (Typed-error-kind-enumeration) rather than extending silent-fallback (which closes at 11 with #265).
|
||||
|
||||
Required fix shape: (a) introduce `pub enum RuntimeErrorKind { TurnBudgetExhausted { iterations: u32, max: u32 }, IterationBudgetExhausted { iterations: u32, max: u32 }, ApiError { status: Option<u16>, retryable: bool }, SessionError { kind: SessionErrorKind, path: Option<PathBuf> }, HookError { hook_name: String, exit_code: Option<i32> }, FilesystemError { path: PathBuf, operation: FilesystemOp, errno: Option<i32> }, ParseError { argv_index: Option<usize>, raw: Option<String> }, ToolStreamExhausted, EmptyAssistantStream, PostToolNudgeExhausted, ConfirmationRequired, Other }` at `runtime/src/conversation.rs`; (b) replace `RuntimeError { message: String }` with `RuntimeError { kind: RuntimeErrorKind, message: String }` adding `RuntimeError::kind(&self) -> &RuntimeErrorKind` accessor; (c) add typed constructors `RuntimeError::turn_budget_exhausted(iterations, max)`, `::api(status, retryable, message)`, `::session(kind, path, message)`, etc., and keep `RuntimeError::new(message)` as a `#[deprecated]` alias that constructs `kind: RuntimeErrorKind::Other` so existing call sites compile while the migration proceeds; (d) audit all 20 `RuntimeError::new` call sites and migrate each to a typed constructor — `conversation.rs:324` → `turn_budget_exhausted(...)`, `conversation.rs:740` → `tool_stream_exhausted()`, `conversation.rs:745` → `empty_assistant_stream()`, `main.rs:7976`/`:7997`/`:8007`/`:8125` → `api(...)`, `main.rs:8000` → `post_tool_nudge_exhausted()`, etc.; (e) extend `--output-format json` envelope to emit `error.kind` from `RuntimeError::kind()` with stable serde-renamed snake_case discriminant strings (`turn_budget_exhausted`, `api_error`, `session_error`, etc.), and emit per-variant typed fields (`error.iterations`, `error.max`, `error.path`, `error.retryable`, etc.); (f) replace `classify_error_kind(message: &str)` with `classify_error_kind(error: &RuntimeError) -> &'static str { error.kind().as_str() }` — the function survives as a serde-rename helper but no longer substring-scrapes prose; (g) add `RuntimeErrorKind::as_str()` and `FromStr` round-trip and golden-fixture tests for every variant proving the JSON envelope round-trips through `error.kind`; (h) deprecate the substring branches in `classify_error_kind` over a one-cycle window to give external consumers time to migrate from prose-scraping to typed-field-reading.
|
||||
|
||||
Acceptance: a downstream caller can pattern-match on `error.kind()` returning `RuntimeErrorKind::TurnBudgetExhausted { iterations, max }` instead of substring-matching `message.contains("conversation loop exceeded")`; `--output-format json` emits `{ "error": { "kind": "turn_budget_exhausted", "iterations": 33, "max": 32, "message": "…" }, … }` with typed payload fields per variant; the 22-branch substring classifier shrinks to a single `error.kind().as_str()` call; new error classes added to the runtime are compile-time-visible at every emit point because the enum requires exhaustive matching; a new error variant added without a classifier branch becomes a compiler error rather than silently degrading to `"unknown"`; #264's `TurnBudgetExhausted` variant has a typed home; #130/#130b's filesystem context-loss has a `RuntimeErrorKind::FilesystemError { path, operation, errno }` typed home rather than a classifier substring branch.
|
||||
|
||||
**Status:** Open. No source code changed. Filed 2026-04-26 13:35 KST. Branch: feat/jobdori-168c-emission-routing. HEAD: `8975354` before filing (post-rebase fast-forward onto gaebal-gajae's #265 `--output-format stream-json` lane-absent pinpoint). Cluster delta: Typed-error-kind-enumeration cluster 0→1 (founder, NEW SOLO CLUSTER); complementary-pinpoint-pair-bundle discovery-pattern extended to 3 bundles total (#245+#250 WebSearch, #262+#264 turn-budget, #264+#266 turn-budget-runtime/typed-error-axis). Smaller-scope by design (matches #253/#254/#257/#258/#260/#261/#262/#263/#264/#265 context-budget discipline). Sister: #264 (Turn-budget primitive runtime-side; #264 names `RuntimeErrorKind` as a future variant; #266 is the dedicated structural audit of that absent enum itself). Distinct from §4.44 typed-error contract proposal (which targets the JSON envelope boundary; #266 targets the runtime enum that the envelope would serialize FROM). Distinct from #130/#130b classifier-pattern-missing (which is downstream of the absent enum; #266 catalogues the upstream root cause). Concurrent-dogfood-rebase parity will be confirmed local==origin==fork at HEAD `8975354+#266` after push.
|
||||
|
||||
Reference in New Issue
Block a user