From b8f066347be61388715f00d3c344c8b669a7c240 Mon Sep 17 00:00:00 2001 From: bellman Date: Fri, 5 Jun 2026 05:29:27 +0900 Subject: [PATCH] fix: structured bootstrap-plan phases JSON (#412) bootstrap-plan --output-format json now returns phases as structured objects with id, label, description, and order fields instead of raw Rust enum variant name strings. Also exposes total_phases count. Generated with https://github.com/Yeachan-Heo/gajae-code Co-authored-by: Gajae Code --- ROADMAP.md | 2 +- rust/crates/rusty-claude-cli/src/main.rs | 100 +++++++++++++++++++---- 2 files changed, 85 insertions(+), 17 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 30446a67..b48525ee 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6317,7 +6317,7 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed) 411. **`plugins enable/disable --output-format json` always emits `reload_runtime:true` regardless of whether state actually changed, and omits `previous_status`, `changed`, `version`, and `source` fields — automation cannot tell if a reload is necessary or if the mutation was a no-op** — dogfooded 2026-04-30 by Jobdori on `e939777f`. Running `claw plugins enable example-bundled --output-format json` on an already-enabled plugin returns `{"action":"enable","kind":"plugin","message":"…","reload_runtime":true,"target":"example-bundled"}` — `reload_runtime:true` every time, even on a no-op re-enable. The same applies to idempotent `disable`. Structured fields present: `action`, `kind`, `message`, `reload_runtime`, `target`. Structured fields absent: `previous_status`, `status`, `changed`, `version`, `source`. The actual plugin name, version, and new status are embedded only in the prose `message` field (`"Result enabled example-bundled@bundled\n Name example-bundled\n Version 0.1.0\n Status enabled"`), requiring callers to scrape column-aligned text to extract the post-mutation state. A no-op mutation emitting `reload_runtime:true` forces orchestration to trigger an expensive runtime reload even when no config change occurred. **Required fix shape:** (a) add `changed:bool` so callers can skip runtime reload when `changed:false`; (b) add `previous_status` and `status` fields (enums: `enabled`/`disabled`) so pre/post state is machine-readable without parsing `message`; (c) add `version` and `source` fields at the mutation response level, consistent with `plugins list` entry shape; (d) emit `reload_runtime:false` when `changed:false`; (e) add regression coverage proving idempotent enable/disable sets `changed:false` and `reload_runtime:false`. **Why this matters:** plugin lifecycle is a hot path for automation that conditionally enables plugins before running sessions. If every enable emits `reload_runtime:true` and no `changed` field exists, orchestration must reload unconditionally or maintain external state — both brittle patterns. Source: Jobdori live dogfood, `e939777f`, 2026-04-30. -412. **`bootstrap-plan --output-format json` returns `phases: string[]` of raw Rust enum variant names with no description, steps, duration, or dependency metadata — unusable by automation** — dogfooded 2026-04-30 by Jobdori on `e939777f`. Running `claw bootstrap-plan --output-format json` returns `{"kind":"bootstrap-plan","phases":["CliEntry","FastPathVersion","StartupProfiler","SystemPromptFastPath","ChromeMcpFastPath","DaemonWorkerFastPath","BridgeFastPath","DaemonFastPath","BackgroundSessionFastPath","TemplateFastPath","EnvironmentRunnerFastPath","MainRuntime"]}`. The envelope has only two keys: `kind` and `phases`. The `phases` array contains 12 raw Rust enum variant name strings — opaque identifiers with no `description`, no `label`, no `steps[]`, no `estimated_ms`, no `dependencies[]`, no `optional:bool`, and no `status` (enabled/disabled/skipped). Automation that calls `bootstrap-plan` to understand startup costs or profile initialization paths receives 12 name strings that reveal nothing about what each phase does, how long it takes, whether it depends on credentials/network/MCP, or which ones can be skipped. **Required fix shape:** (a) replace `phases: string[]` with `phases: [{id, label, description, optional, estimated_ms?, dependencies?, status?}]`; (b) add a top-level `total_phases` count; (c) mark network/credential-dependent phases with a `requires_auth:bool` or `deps:["network","credentials","mcp"]` field so automation can plan for unavailability; (d) add regression coverage proving each phase entry has at least `id`, `label`, and `description` fields and that the count matches the phases array length. **Why this matters:** bootstrap-plan is the startup-cost introspection surface. If its JSON output is 12 opaque variant name strings, automation cannot profile startup, identify slow phases, skip optional phases, or present meaningful startup diagnostics — the entire command serves only as a list of internal identifiers. Source: Jobdori live dogfood, `e939777f`, 2026-04-30. +412. **DONE — `bootstrap-plan --output-format json` returns `phases: string[]` of raw Rust enum variant names with no description, steps, duration, or dependency metadata — unusable by automation** — dogfooded 2026-04-30 by Jobdori on `e939777f`. Running `claw bootstrap-plan --output-format json` returns `{"kind":"bootstrap-plan","phases":["CliEntry","FastPathVersion","StartupProfiler","SystemPromptFastPath","ChromeMcpFastPath","DaemonWorkerFastPath","BridgeFastPath","DaemonFastPath","BackgroundSessionFastPath","TemplateFastPath","EnvironmentRunnerFastPath","MainRuntime"]}`. The envelope has only two keys: `kind` and `phases`. The `phases` array contains 12 raw Rust enum variant name strings — opaque identifiers with no `description`, no `label`, no `steps[]`, no `estimated_ms`, no `dependencies[]`, no `optional:bool`, and no `status` (enabled/disabled/skipped). Automation that calls `bootstrap-plan` to understand startup costs or profile initialization paths receives 12 name strings that reveal nothing about what each phase does, how long it takes, whether it depends on credentials/network/MCP, or which ones can be skipped. **Required fix shape:** (a) replace `phases: string[]` with `phases: [{id, label, description, optional, estimated_ms?, dependencies?, status?}]`; (b) add a top-level `total_phases` count; (c) mark network/credential-dependent phases with a `requires_auth:bool` or `deps:["network","credentials","mcp"]` field so automation can plan for unavailability; (d) add regression coverage proving each phase entry has at least `id`, `label`, and `description` fields and that the count matches the phases array length. **Why this matters:** bootstrap-plan is the startup-cost introspection surface. If its JSON output is 12 opaque variant name strings, automation cannot profile startup, identify slow phases, skip optional phases, or present meaningful startup diagnostics — the entire command serves only as a list of internal identifiers. Source: Jobdori live dogfood, `e939777f`, 2026-04-30. 413. **DONE — ACP JSON no longer leaks tracking IDs** — verified 2026-06-04: `acp --output-format json` has no `tracking` or `discoverability_tracking` fields. Status is `not_implemented`. diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index cc2021c1..405c5c0d 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -4759,30 +4759,98 @@ fn build_rust_resolver_manifest(workspace_dir: &Path) -> Result Result<(), Box> { - let phases = runtime::BootstrapPlan::claude_code_default() - .phases() - .iter() - .map(|phase| format!("{phase:?}")) - .collect::>(); + let phases = runtime::BootstrapPlan::claude_code_default(); match output_format { CliOutputFormat::Text => { - for phase in &phases { - println!("- {phase}"); + for phase in phases.phases() { + println!("- {phase:?}"); } } - CliOutputFormat::Json => println!( - "{}", - serde_json::to_string_pretty(&json!({ - "kind": "bootstrap-plan", - "action": "show", - "status": "ok", - "phases": phases, - }))? - ), + CliOutputFormat::Json => { + // #412: emit structured phase objects with label and description + let phase_objects: Vec = phases + .phases() + .iter() + .enumerate() + .map(|(i, phase)| { + let (label, description) = bootstrap_phase_metadata(phase); + json!({ + "id": format!("{phase:?}"), + "label": label, + "description": description, + "order": i, + }) + }) + .collect(); + println!( + "{}", + serde_json::to_string_pretty(&json!({ + "kind": "bootstrap-plan", + "action": "show", + "status": "ok", + "total_phases": phases.phases().len(), + "phases": phase_objects, + }))? + ); + } } Ok(()) } +fn bootstrap_phase_metadata(phase: &runtime::BootstrapPhase) -> (&'static str, &'static str) { + use runtime::BootstrapPhase::*; + match phase { + CliEntry => ( + "CLI Entry", + "Command-line argument parsing and global flag resolution", + ), + FastPathVersion => ( + "Fast-Path Version", + "Short-circuit version/help requests before full startup", + ), + StartupProfiler => ( + "Startup Profiler", + "Instrument startup timing for diagnostics", + ), + SystemPromptFastPath => ( + "System Prompt Fast-Path", + "Serve system-prompt requests without provider init", + ), + ChromeMcpFastPath => ( + "Chrome MCP Fast-Path", + "Serve Chrome MCP requests without full runtime", + ), + DaemonWorkerFastPath => ( + "Daemon Worker Fast-Path", + "Handle daemon worker requests without full init", + ), + BridgeFastPath => ( + "Bridge Fast-Path", + "Bridge/sibling process communication without full init", + ), + DaemonFastPath => ( + "Daemon Fast-Path", + "Daemon lifecycle management without full runtime", + ), + BackgroundSessionFastPath => ( + "Background Session Fast-Path", + "Resume/list background sessions without full init", + ), + TemplateFastPath => ( + "Template Fast-Path", + "Template rendering without full runtime", + ), + EnvironmentRunnerFastPath => ( + "Environment Runner Fast-Path", + "Environment/runner dispatch without full init", + ), + MainRuntime => ( + "Main Runtime", + "Full interactive REPL or one-shot prompt execution", + ), + } +} + fn print_system_prompt( cwd: PathBuf, date: String,