Close the clawability backlog with deterministic CLI output and lane lineage

Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage.

Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility
Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone
Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output
Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata
Confidence: medium
Scope-risk: moderate
Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths
Tested: python .github/scripts/check_doc_source_of_truth.py
Tested: cd rust && cargo fmt --all --check
Tested: cd rust && cargo test --workspace
Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings
Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
This commit is contained in:
Yeachan-Heo
2026-04-05 18:40:33 +00:00
parent 93e979261e
commit 163cf00650
14 changed files with 954 additions and 138 deletions

View File

@@ -2466,6 +2466,7 @@ fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, P
roots
}
#[allow(clippy::too_many_lines)]
fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
let mut roots = Vec::new();

View File

@@ -0,0 +1,144 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BranchLockIntent {
#[serde(rename = "laneId")]
pub lane_id: String,
pub branch: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub worktree: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub modules: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BranchLockCollision {
pub branch: String,
pub module: String,
#[serde(rename = "laneIds")]
pub lane_ids: Vec<String>,
}
#[must_use]
pub fn detect_branch_lock_collisions(intents: &[BranchLockIntent]) -> Vec<BranchLockCollision> {
let mut collisions = Vec::new();
for (index, left) in intents.iter().enumerate() {
for right in &intents[index + 1..] {
if left.branch != right.branch {
continue;
}
for module in overlapping_modules(&left.modules, &right.modules) {
collisions.push(BranchLockCollision {
branch: left.branch.clone(),
module,
lane_ids: vec![left.lane_id.clone(), right.lane_id.clone()],
});
}
}
}
collisions.sort_by(|a, b| {
a.branch
.cmp(&b.branch)
.then(a.module.cmp(&b.module))
.then(a.lane_ids.cmp(&b.lane_ids))
});
collisions.dedup();
collisions
}
fn overlapping_modules(left: &[String], right: &[String]) -> Vec<String> {
let mut overlaps = Vec::new();
for left_module in left {
for right_module in right {
if modules_overlap(left_module, right_module) {
overlaps.push(shared_scope(left_module, right_module));
}
}
}
overlaps.sort();
overlaps.dedup();
overlaps
}
fn modules_overlap(left: &str, right: &str) -> bool {
left == right
|| left.starts_with(&format!("{right}/"))
|| right.starts_with(&format!("{left}/"))
}
fn shared_scope(left: &str, right: &str) -> String {
if left.starts_with(&format!("{right}/")) || left == right {
right.to_string()
} else {
left.to_string()
}
}
#[cfg(test)]
mod tests {
use super::{detect_branch_lock_collisions, BranchLockIntent};
#[test]
fn detects_same_branch_same_module_collisions() {
let collisions = detect_branch_lock_collisions(&[
BranchLockIntent {
lane_id: "lane-a".to_string(),
branch: "feature/lock".to_string(),
worktree: Some("wt-a".to_string()),
modules: vec!["runtime/mcp".to_string()],
},
BranchLockIntent {
lane_id: "lane-b".to_string(),
branch: "feature/lock".to_string(),
worktree: Some("wt-b".to_string()),
modules: vec!["runtime/mcp".to_string()],
},
]);
assert_eq!(collisions.len(), 1);
assert_eq!(collisions[0].branch, "feature/lock");
assert_eq!(collisions[0].module, "runtime/mcp");
}
#[test]
fn detects_nested_module_scope_collisions() {
let collisions = detect_branch_lock_collisions(&[
BranchLockIntent {
lane_id: "lane-a".to_string(),
branch: "feature/lock".to_string(),
worktree: None,
modules: vec!["runtime".to_string()],
},
BranchLockIntent {
lane_id: "lane-b".to_string(),
branch: "feature/lock".to_string(),
worktree: None,
modules: vec!["runtime/mcp".to_string()],
},
]);
assert_eq!(collisions[0].module, "runtime");
}
#[test]
fn ignores_different_branches() {
let collisions = detect_branch_lock_collisions(&[
BranchLockIntent {
lane_id: "lane-a".to_string(),
branch: "feature/a".to_string(),
worktree: None,
modules: vec!["runtime/mcp".to_string()],
},
BranchLockIntent {
lane_id: "lane-b".to_string(),
branch: "feature/b".to_string(),
worktree: None,
modules: vec!["runtime/mcp".to_string()],
},
]);
assert!(collisions.is_empty());
}
}

View File

@@ -76,6 +76,20 @@ pub struct LaneEventBlocker {
pub detail: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LaneCommitProvenance {
pub commit: String,
pub branch: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub worktree: Option<String>,
#[serde(rename = "canonicalCommit", skip_serializing_if = "Option::is_none")]
pub canonical_commit: Option<String>,
#[serde(rename = "supersededBy", skip_serializing_if = "Option::is_none")]
pub superseded_by: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub lineage: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LaneEvent {
pub event: LaneEventName,
@@ -122,6 +136,36 @@ impl LaneEvent {
.with_optional_detail(detail)
}
#[must_use]
pub fn commit_created(
emitted_at: impl Into<String>,
detail: Option<String>,
provenance: LaneCommitProvenance,
) -> Self {
Self::new(
LaneEventName::CommitCreated,
LaneEventStatus::Completed,
emitted_at,
)
.with_optional_detail(detail)
.with_data(serde_json::to_value(provenance).expect("commit provenance should serialize"))
}
#[must_use]
pub fn superseded(
emitted_at: impl Into<String>,
detail: Option<String>,
provenance: LaneCommitProvenance,
) -> Self {
Self::new(
LaneEventName::Superseded,
LaneEventStatus::Superseded,
emitted_at,
)
.with_optional_detail(detail)
.with_data(serde_json::to_value(provenance).expect("commit provenance should serialize"))
}
#[must_use]
pub fn blocked(emitted_at: impl Into<String>, blocker: &LaneEventBlocker) -> Self {
Self::new(LaneEventName::Blocked, LaneEventStatus::Blocked, emitted_at)
@@ -161,11 +205,54 @@ impl LaneEvent {
}
}
#[must_use]
pub fn dedupe_superseded_commit_events(events: &[LaneEvent]) -> Vec<LaneEvent> {
let mut keep = vec![true; events.len()];
let mut latest_by_key = std::collections::BTreeMap::<String, usize>::new();
for (index, event) in events.iter().enumerate() {
if event.event != LaneEventName::CommitCreated {
continue;
}
let Some(data) = event.data.as_ref() else {
continue;
};
let key = data
.get("canonicalCommit")
.or_else(|| data.get("commit"))
.and_then(serde_json::Value::as_str)
.map(str::to_string);
let superseded = data
.get("supersededBy")
.and_then(serde_json::Value::as_str)
.is_some();
if superseded {
keep[index] = false;
continue;
}
if let Some(key) = key {
if let Some(previous) = latest_by_key.insert(key, index) {
keep[previous] = false;
}
}
}
events
.iter()
.cloned()
.zip(keep)
.filter_map(|(event, retain)| retain.then_some(event))
.collect()
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{LaneEvent, LaneEventBlocker, LaneEventName, LaneEventStatus, LaneFailureClass};
use super::{
dedupe_superseded_commit_events, LaneCommitProvenance, LaneEvent, LaneEventBlocker,
LaneEventName, LaneEventStatus, LaneFailureClass,
};
#[test]
fn canonical_lane_event_names_serialize_to_expected_wire_values() {
@@ -240,4 +327,56 @@ mod tests {
assert_eq!(failed.status, LaneEventStatus::Failed);
assert_eq!(failed.detail.as_deref(), Some("broken server"));
}
#[test]
fn commit_events_can_carry_worktree_and_supersession_metadata() {
let event = LaneEvent::commit_created(
"2026-04-04T00:00:00Z",
Some("commit created".to_string()),
LaneCommitProvenance {
commit: "abc123".to_string(),
branch: "feature/provenance".to_string(),
worktree: Some("wt-a".to_string()),
canonical_commit: Some("abc123".to_string()),
superseded_by: None,
lineage: vec!["abc123".to_string()],
},
);
let event_json = serde_json::to_value(&event).expect("lane event should serialize");
assert_eq!(event_json["event"], "lane.commit.created");
assert_eq!(event_json["data"]["branch"], "feature/provenance");
assert_eq!(event_json["data"]["worktree"], "wt-a");
}
#[test]
fn dedupes_superseded_commit_events_by_canonical_commit() {
let retained = dedupe_superseded_commit_events(&[
LaneEvent::commit_created(
"2026-04-04T00:00:00Z",
Some("old".to_string()),
LaneCommitProvenance {
commit: "old123".to_string(),
branch: "feature/provenance".to_string(),
worktree: Some("wt-a".to_string()),
canonical_commit: Some("canon123".to_string()),
superseded_by: Some("new123".to_string()),
lineage: vec!["old123".to_string(), "new123".to_string()],
},
),
LaneEvent::commit_created(
"2026-04-04T00:00:01Z",
Some("new".to_string()),
LaneCommitProvenance {
commit: "new123".to_string(),
branch: "feature/provenance".to_string(),
worktree: Some("wt-b".to_string()),
canonical_commit: Some("canon123".to_string()),
superseded_by: None,
lineage: vec!["old123".to_string(), "new123".to_string()],
},
),
]);
assert_eq!(retained.len(), 1);
assert_eq!(retained[0].detail.as_deref(), Some("new"));
}
}

View File

@@ -7,6 +7,7 @@
mod bash;
pub mod bash_validation;
mod bootstrap;
pub mod branch_lock;
mod compact;
mod config;
mod conversation;
@@ -46,6 +47,7 @@ pub mod worker_boot;
pub use bash::{execute_bash, BashCommandInput, BashCommandOutput};
pub use bootstrap::{BootstrapPhase, BootstrapPlan};
pub use branch_lock::{detect_branch_lock_collisions, BranchLockCollision, BranchLockIntent};
pub use compact::{
compact_session, estimate_session_tokens, format_compact_summary,
get_compact_continuation_message, should_compact, CompactionConfig, CompactionResult,
@@ -72,7 +74,8 @@ pub use hooks::{
HookAbortSignal, HookEvent, HookProgressEvent, HookProgressReporter, HookRunResult, HookRunner,
};
pub use lane_events::{
LaneEvent, LaneEventBlocker, LaneEventName, LaneEventStatus, LaneFailureClass,
dedupe_superseded_commit_events, LaneCommitProvenance, LaneEvent, LaneEventBlocker,
LaneEventName, LaneEventStatus, LaneFailureClass,
};
pub use mcp::{
mcp_server_signature, mcp_tool_name, mcp_tool_prefix, normalize_name_for_mcp,

View File

@@ -2652,8 +2652,37 @@ mod tests {
});
}
fn write_initialize_disconnect_script() -> PathBuf {
let root = temp_dir();
fs::create_dir_all(&root).expect("temp dir");
let script_path = root.join("initialize-disconnect.py");
let script = [
"#!/usr/bin/env python3",
"import sys",
"header = b''",
r"while not header.endswith(b'\r\n\r\n'):",
" chunk = sys.stdin.buffer.read(1)",
" if not chunk:",
" raise SystemExit(1)",
" header += chunk",
"length = 0",
r"for line in header.decode().split('\r\n'):",
r" if line.lower().startswith('content-length:'):",
r" length = int(line.split(':', 1)[1].strip())",
"if length:",
" sys.stdin.buffer.read(length)",
"raise SystemExit(0)",
"",
]
.join("\n");
fs::write(&script_path, script).expect("write script");
let mut permissions = fs::metadata(&script_path).expect("metadata").permissions();
permissions.set_mode(0o755);
fs::set_permissions(&script_path, permissions).expect("chmod");
script_path
}
#[test]
#[ignore = "flaky: intermittent timing issues in CI, see ROADMAP P2.15"]
fn manager_discovery_report_keeps_healthy_servers_when_one_server_fails() {
let runtime = Builder::new_current_thread()
.enable_all()
@@ -2663,6 +2692,7 @@ mod tests {
let script_path = write_manager_mcp_server_script();
let root = script_path.parent().expect("script parent");
let alpha_log = root.join("alpha.log");
let broken_script_path = write_initialize_disconnect_script();
let servers = BTreeMap::from([
(
"alpha".to_string(),
@@ -2673,8 +2703,8 @@ mod tests {
ScopedMcpServerConfig {
scope: ConfigSource::Local,
config: McpServerConfig::Stdio(McpStdioServerConfig {
command: "python3".to_string(),
args: vec!["-c".to_string(), "import sys; sys.exit(0)".to_string()],
command: broken_script_path.display().to_string(),
args: Vec::new(),
env: BTreeMap::new(),
tool_call_timeout_ms: None,
}),
@@ -2737,6 +2767,7 @@ mod tests {
manager.shutdown().await.expect("shutdown");
cleanup_script(&script_path);
cleanup_script(&broken_script_path);
});
}

View File

@@ -31,3 +31,4 @@ workspace = true
mock-anthropic-service = { path = "../mock-anthropic-service" }
serde_json.workspace = true
tokio = { version = "1", features = ["rt-multi-thread"] }

View File

@@ -89,6 +89,10 @@ const CLI_OPTION_SUGGESTIONS: &[&str] = &[
];
type AllowedToolSet = BTreeSet<String>;
type RuntimePluginStateBuildOutput = (
Option<Arc<Mutex<RuntimeMcpState>>>,
Vec<RuntimeToolDefinition>,
);
fn main() {
if let Err(error) = run() {
@@ -109,9 +113,12 @@ Run `claw --help` for usage."
fn run() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().skip(1).collect();
match parse_args(&args)? {
CliAction::DumpManifests => dump_manifests(),
CliAction::BootstrapPlan => print_bootstrap_plan(),
CliAction::Agents { args } => LiveCli::print_agents(args.as_deref())?,
CliAction::DumpManifests { output_format } => dump_manifests(output_format)?,
CliAction::BootstrapPlan { output_format } => print_bootstrap_plan(output_format)?,
CliAction::Agents {
args,
output_format,
} => LiveCli::print_agents(args.as_deref(), output_format)?,
CliAction::Mcp {
args,
output_format,
@@ -120,12 +127,17 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
args,
output_format,
} => LiveCli::print_skills(args.as_deref(), output_format)?,
CliAction::PrintSystemPrompt { cwd, date } => print_system_prompt(cwd, date),
CliAction::Version => print_version(),
CliAction::PrintSystemPrompt {
cwd,
date,
output_format,
} => print_system_prompt(cwd, date, output_format)?,
CliAction::Version { output_format } => print_version(output_format)?,
CliAction::ResumeSession {
session_path,
commands,
} => resume_session(&session_path, &commands),
output_format,
} => resume_session(&session_path, &commands, output_format),
CliAction::Status {
model,
permission_mode,
@@ -140,27 +152,32 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
permission_mode,
} => LiveCli::new(model, true, allowed_tools, permission_mode)?
.run_turn_with_output(&prompt, output_format)?,
CliAction::Login => run_login()?,
CliAction::Logout => run_logout()?,
CliAction::Doctor => run_doctor()?,
CliAction::Init => run_init()?,
CliAction::Login { output_format } => run_login(output_format)?,
CliAction::Logout { output_format } => run_logout(output_format)?,
CliAction::Doctor { output_format } => run_doctor(output_format)?,
CliAction::Init { output_format } => run_init(output_format)?,
CliAction::Repl {
model,
allowed_tools,
permission_mode,
} => run_repl(model, allowed_tools, permission_mode)?,
CliAction::HelpTopic(topic) => print_help_topic(topic),
CliAction::Help => print_help(),
CliAction::Help { output_format } => print_help(output_format)?,
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum CliAction {
DumpManifests,
BootstrapPlan,
DumpManifests {
output_format: CliOutputFormat,
},
BootstrapPlan {
output_format: CliOutputFormat,
},
Agents {
args: Option<String>,
output_format: CliOutputFormat,
},
Mcp {
args: Option<String>,
@@ -173,11 +190,15 @@ enum CliAction {
PrintSystemPrompt {
cwd: PathBuf,
date: String,
output_format: CliOutputFormat,
},
Version {
output_format: CliOutputFormat,
},
Version,
ResumeSession {
session_path: PathBuf,
commands: Vec<String>,
output_format: CliOutputFormat,
},
Status {
model: String,
@@ -194,10 +215,18 @@ enum CliAction {
allowed_tools: Option<AllowedToolSet>,
permission_mode: PermissionMode,
},
Login,
Logout,
Doctor,
Init,
Login {
output_format: CliOutputFormat,
},
Logout {
output_format: CliOutputFormat,
},
Doctor {
output_format: CliOutputFormat,
},
Init {
output_format: CliOutputFormat,
},
Repl {
model: String,
allowed_tools: Option<AllowedToolSet>,
@@ -205,7 +234,9 @@ enum CliAction {
},
HelpTopic(LocalHelpTopic),
// prompt-mode formatting is only supported for non-interactive runs
Help,
Help {
output_format: CliOutputFormat,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -346,11 +377,11 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
}
if wants_help {
return Ok(CliAction::Help);
return Ok(CliAction::Help { output_format });
}
if wants_version {
return Ok(CliAction::Version);
return Ok(CliAction::Version { output_format });
}
let allowed_tools = normalize_allowed_tools(&allowed_tool_values)?;
@@ -364,22 +395,25 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
});
}
if rest.first().map(String::as_str) == Some("--resume") {
return parse_resume_args(&rest[1..]);
return parse_resume_args(&rest[1..], output_format);
}
if let Some(action) = parse_local_help_action(&rest) {
return action;
}
if let Some(action) = parse_single_word_command_alias(&rest, &model, permission_mode_override, output_format) {
if let Some(action) =
parse_single_word_command_alias(&rest, &model, permission_mode_override, output_format)
{
return action;
}
let permission_mode = permission_mode_override.unwrap_or_else(default_permission_mode);
match rest[0].as_str() {
"dump-manifests" => Ok(CliAction::DumpManifests),
"bootstrap-plan" => Ok(CliAction::BootstrapPlan),
"dump-manifests" => Ok(CliAction::DumpManifests { output_format }),
"bootstrap-plan" => Ok(CliAction::BootstrapPlan { output_format }),
"agents" => Ok(CliAction::Agents {
args: join_optional_args(&rest[1..]),
output_format,
}),
"mcp" => Ok(CliAction::Mcp {
args: join_optional_args(&rest[1..]),
@@ -389,10 +423,10 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
args: join_optional_args(&rest[1..]),
output_format,
}),
"system-prompt" => parse_system_prompt_args(&rest[1..]),
"login" => Ok(CliAction::Login),
"logout" => Ok(CliAction::Logout),
"init" => Ok(CliAction::Init),
"system-prompt" => parse_system_prompt_args(&rest[1..], output_format),
"login" => Ok(CliAction::Login { output_format }),
"logout" => Ok(CliAction::Logout { output_format }),
"init" => Ok(CliAction::Init { output_format }),
"prompt" => {
let prompt = rest[1..].join(" ");
if prompt.trim().is_empty() {
@@ -446,15 +480,15 @@ fn parse_single_word_command_alias(
}
match rest[0].as_str() {
"help" => Some(Ok(CliAction::Help)),
"version" => Some(Ok(CliAction::Version)),
"help" => Some(Ok(CliAction::Help { output_format })),
"version" => Some(Ok(CliAction::Version { output_format })),
"status" => Some(Ok(CliAction::Status {
model: model.to_string(),
permission_mode: permission_mode_override.unwrap_or_else(default_permission_mode),
output_format,
})),
"sandbox" => Some(Ok(CliAction::Sandbox { output_format })),
"doctor" => Some(Ok(CliAction::Doctor)),
"doctor" => Some(Ok(CliAction::Doctor { output_format })),
other => bare_slash_command_guidance(other).map(Err),
}
}
@@ -502,8 +536,11 @@ fn parse_direct_slash_cli_action(
) -> Result<CliAction, String> {
let raw = rest.join(" ");
match SlashCommand::parse(&raw) {
Ok(Some(SlashCommand::Help)) => Ok(CliAction::Help),
Ok(Some(SlashCommand::Agents { args })) => Ok(CliAction::Agents { args }),
Ok(Some(SlashCommand::Help)) => Ok(CliAction::Help { output_format }),
Ok(Some(SlashCommand::Agents { args })) => Ok(CliAction::Agents {
args,
output_format,
}),
Ok(Some(SlashCommand::Mcp { action, target })) => Ok(CliAction::Mcp {
args: match (action, target) {
(None, None) => None,
@@ -727,7 +764,10 @@ fn filter_tool_specs(
tool_registry.definitions(allowed_tools)
}
fn parse_system_prompt_args(args: &[String]) -> Result<CliAction, String> {
fn parse_system_prompt_args(
args: &[String],
output_format: CliOutputFormat,
) -> Result<CliAction, String> {
let mut cwd = env::current_dir().map_err(|error| error.to_string())?;
let mut date = DEFAULT_DATE.to_string();
let mut index = 0;
@@ -752,10 +792,14 @@ fn parse_system_prompt_args(args: &[String]) -> Result<CliAction, String> {
}
}
Ok(CliAction::PrintSystemPrompt { cwd, date })
Ok(CliAction::PrintSystemPrompt {
cwd,
date,
output_format,
})
}
fn parse_resume_args(args: &[String]) -> Result<CliAction, String> {
fn parse_resume_args(args: &[String], output_format: CliOutputFormat) -> Result<CliAction, String> {
let (session_path, command_tokens): (PathBuf, &[String]) = match args.first() {
None => (PathBuf::from(LATEST_SESSION_REFERENCE), &[]),
Some(first) if looks_like_slash_command_token(first) => {
@@ -795,6 +839,7 @@ fn parse_resume_args(args: &[String]) -> Result<CliAction, String> {
Ok(CliAction::ResumeSession {
session_path,
commands,
output_format,
})
}
@@ -930,9 +975,21 @@ fn render_doctor_report() -> Result<DoctorReport, Box<dyn std::error::Error>> {
})
}
fn run_doctor() -> Result<(), Box<dyn std::error::Error>> {
fn run_doctor(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
let report = render_doctor_report()?;
println!("{}", report.render());
let message = report.render();
match output_format {
CliOutputFormat::Text => println!("{message}"),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "doctor",
"message": message,
"report": message,
"has_failures": report.has_failures(),
}))?
),
}
if report.has_failures() {
return Err("doctor found failing checks".into());
}
@@ -1212,26 +1269,54 @@ fn looks_like_slash_command_token(token: &str) -> bool {
.any(|spec| spec.name == name || spec.aliases.contains(&name))
}
fn dump_manifests() {
fn dump_manifests(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
let workspace_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let paths = UpstreamPaths::from_workspace_dir(&workspace_dir);
match extract_manifest(&paths) {
Ok(manifest) => {
println!("commands: {}", manifest.commands.entries().len());
println!("tools: {}", manifest.tools.entries().len());
println!("bootstrap phases: {}", manifest.bootstrap.phases().len());
}
Err(error) => {
eprintln!("failed to extract manifests: {error}");
std::process::exit(1);
match output_format {
CliOutputFormat::Text => {
println!("commands: {}", manifest.commands.entries().len());
println!("tools: {}", manifest.tools.entries().len());
println!("bootstrap phases: {}", manifest.bootstrap.phases().len());
}
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "dump-manifests",
"commands": manifest.commands.entries().len(),
"tools": manifest.tools.entries().len(),
"bootstrap_phases": manifest.bootstrap.phases().len(),
}))?
),
}
Ok(())
}
Err(error) => Err(format!("failed to extract manifests: {error}").into()),
}
}
fn print_bootstrap_plan() {
for phase in runtime::BootstrapPlan::claude_code_default().phases() {
println!("- {phase:?}");
fn print_bootstrap_plan(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
let phases = runtime::BootstrapPlan::claude_code_default()
.phases()
.iter()
.map(|phase| format!("{phase:?}"))
.collect::<Vec<_>>();
match output_format {
CliOutputFormat::Text => {
for phase in &phases {
println!("- {phase}");
}
}
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "bootstrap-plan",
"phases": phases,
}))?
),
}
Ok(())
}
fn default_oauth_config() -> OAuthConfig {
@@ -1249,7 +1334,7 @@ fn default_oauth_config() -> OAuthConfig {
}
}
fn run_login() -> Result<(), Box<dyn std::error::Error>> {
fn run_login(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
let cwd = env::current_dir()?;
let config = ConfigLoader::default_for(&cwd).load()?;
let default_oauth = default_oauth_config();
@@ -1262,8 +1347,10 @@ fn run_login() -> Result<(), Box<dyn std::error::Error>> {
OAuthAuthorizationRequest::from_config(oauth, redirect_uri.clone(), state.clone(), &pkce)
.build_url();
println!("Starting Claude OAuth login...");
println!("Listening for callback on {redirect_uri}");
if output_format == CliOutputFormat::Text {
println!("Starting Claude OAuth login...");
println!("Listening for callback on {redirect_uri}");
}
if let Err(error) = open_browser(&authorize_url) {
eprintln!("warning: failed to open browser automatically: {error}");
println!("Open this URL manually:\n{authorize_url}");
@@ -1287,8 +1374,13 @@ fn run_login() -> Result<(), Box<dyn std::error::Error>> {
}
let client = AnthropicClient::from_auth(AuthSource::None).with_base_url(api::read_base_url());
let exchange_request =
OAuthTokenExchangeRequest::from_config(oauth, code, state, pkce.verifier, redirect_uri);
let exchange_request = OAuthTokenExchangeRequest::from_config(
oauth,
code,
state,
pkce.verifier,
redirect_uri.clone(),
);
let runtime = tokio::runtime::Runtime::new()?;
let token_set = runtime.block_on(client.exchange_oauth_code(oauth, &exchange_request))?;
save_oauth_credentials(&runtime::OAuthTokenSet {
@@ -1297,13 +1389,33 @@ fn run_login() -> Result<(), Box<dyn std::error::Error>> {
expires_at: token_set.expires_at,
scopes: token_set.scopes,
})?;
println!("Claude OAuth login complete.");
match output_format {
CliOutputFormat::Text => println!("Claude OAuth login complete."),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "login",
"callback_port": callback_port,
"redirect_uri": redirect_uri,
"message": "Claude OAuth login complete.",
}))?
),
}
Ok(())
}
fn run_logout() -> Result<(), Box<dyn std::error::Error>> {
fn run_logout(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
clear_oauth_credentials()?;
println!("Claude OAuth credentials cleared.");
match output_format {
CliOutputFormat::Text => println!("Claude OAuth credentials cleared."),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "logout",
"message": "Claude OAuth credentials cleared.",
}))?
),
}
Ok(())
}
@@ -1361,21 +1473,50 @@ fn wait_for_oauth_callback(
Ok(callback)
}
fn print_system_prompt(cwd: PathBuf, date: String) {
match load_system_prompt(cwd, date, env::consts::OS, "unknown") {
Ok(sections) => println!("{}", sections.join("\n\n")),
Err(error) => {
eprintln!("failed to build system prompt: {error}");
std::process::exit(1);
}
fn print_system_prompt(
cwd: PathBuf,
date: String,
output_format: CliOutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let sections = load_system_prompt(cwd, date, env::consts::OS, "unknown")?;
let message = sections.join(
"
",
);
match output_format {
CliOutputFormat::Text => println!("{message}"),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "system-prompt",
"message": message,
"sections": sections,
}))?
),
}
Ok(())
}
fn print_version() {
println!("{}", render_version_report());
fn print_version(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
let report = render_version_report();
match output_format {
CliOutputFormat::Text => println!("{report}"),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "version",
"message": report,
"version": VERSION,
"git_sha": GIT_SHA,
"target": BUILD_TARGET,
}))?
),
}
Ok(())
}
fn resume_session(session_path: &Path, commands: &[String]) {
fn resume_session(session_path: &Path, commands: &[String], output_format: CliOutputFormat) {
let resolved_path = if session_path.exists() {
session_path.to_path_buf()
} else {
@@ -1425,7 +1566,35 @@ fn resume_session(session_path: &Path, commands: &[String]) {
}) => {
session = next_session;
if let Some(message) = message {
println!("{message}");
if output_format == CliOutputFormat::Json
&& matches!(command, SlashCommand::Status)
{
let tracker = UsageTracker::from_session(&session);
let usage = tracker.cumulative_usage();
let context = status_context(Some(&resolved_path)).expect("status context");
let value = json!({
"kind": "status",
"messages": session.messages.len(),
"turns": tracker.turns(),
"latest_total": tracker.current_turn_usage().total_tokens(),
"cumulative_input": usage.input_tokens,
"cumulative_output": usage.output_tokens,
"cumulative_total": usage.total_tokens(),
"workspace": {
"cwd": context.cwd,
"project_root": context.project_root,
"git_branch": context.git_branch,
"git_state": context.git_summary.headline(),
"session": context.session_path.as_ref().map_or_else(|| "live-repl".to_string(), |path| path.display().to_string()),
}
});
println!(
"{}",
serde_json::to_string_pretty(&value).expect("status json")
);
} else {
println!("{message}");
}
}
}
Err(error) => {
@@ -2357,13 +2526,7 @@ impl RuntimeMcpState {
fn build_runtime_mcp_state(
runtime_config: &runtime::RuntimeConfig,
) -> Result<
(
Option<Arc<Mutex<RuntimeMcpState>>>,
Vec<RuntimeToolDefinition>,
),
Box<dyn std::error::Error>,
> {
) -> Result<RuntimePluginStateBuildOutput, Box<dyn std::error::Error>> {
let Some((mcp_state, discovery)) = RuntimeMcpState::new(runtime_config)? else {
return Ok((None, Vec::new()));
};
@@ -2809,7 +2972,7 @@ impl LiveCli {
false
}
SlashCommand::Init => {
run_init()?;
run_init(CliOutputFormat::Text)?;
false
}
SlashCommand::Diff => {
@@ -2817,7 +2980,7 @@ impl LiveCli {
false
}
SlashCommand::Version => {
Self::print_version();
Self::print_version(CliOutputFormat::Text);
false
}
SlashCommand::Export { path } => {
@@ -2831,7 +2994,7 @@ impl LiveCli {
self.handle_plugins_command(action.as_deref(), target.as_deref())?
}
SlashCommand::Agents { args } => {
Self::print_agents(args.as_deref())?;
Self::print_agents(args.as_deref(), CliOutputFormat::Text)?;
false
}
SlashCommand::Skills { args } => {
@@ -3113,9 +3276,23 @@ impl LiveCli {
Ok(())
}
fn print_agents(args: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
fn print_agents(
args: Option<&str>,
output_format: CliOutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let cwd = env::current_dir()?;
println!("{}", handle_agents_slash_command(args, &cwd)?);
let message = handle_agents_slash_command(args, &cwd)?;
match output_format {
CliOutputFormat::Text => println!("{message}"),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "agents",
"message": message,
"args": args,
}))?
),
}
Ok(())
}
@@ -3154,8 +3331,8 @@ impl LiveCli {
Ok(())
}
fn print_version() {
println!("{}", render_version_report());
fn print_version(output_format: CliOutputFormat) {
let _ = crate::print_version(output_format);
}
fn export_session(
@@ -4060,8 +4237,18 @@ fn init_claude_md() -> Result<String, Box<dyn std::error::Error>> {
Ok(initialize_repo(&cwd)?.render())
}
fn run_init() -> Result<(), Box<dyn std::error::Error>> {
println!("{}", init_claude_md()?);
fn run_init(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
let message = init_claude_md()?;
match output_format {
CliOutputFormat::Text => println!("{message}"),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "init",
"message": message,
}))?
),
}
Ok(())
}
@@ -5916,16 +6103,13 @@ impl CliToolExecutor {
fn execute_search_tool(&self, value: serde_json::Value) -> Result<String, ToolError> {
let input: ToolSearchRequest = serde_json::from_value(value)
.map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
let (pending_mcp_servers, mcp_degraded) = self
.mcp_state
.as_ref()
.map(|state| {
let (pending_mcp_servers, mcp_degraded) =
self.mcp_state.as_ref().map_or((None, None), |state| {
let state = state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
(state.pending_servers(), state.degraded_report())
})
.unwrap_or((None, None));
});
serde_json::to_string_pretty(&self.tool_registry.search(
&input.query,
input.max_results.unwrap_or(5),
@@ -6203,8 +6387,21 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
Ok(())
}
fn print_help() {
let _ = print_help_to(&mut io::stdout());
fn print_help(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
let mut buffer = Vec::new();
print_help_to(&mut buffer)?;
let message = String::from_utf8(buffer)?;
match output_format {
CliOutputFormat::Text => print!("{message}"),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "help",
"message": message,
}))?
),
}
Ok(())
}
#[cfg(test)]
@@ -6513,11 +6710,15 @@ mod tests {
fn parses_version_flags_without_initializing_prompt_mode() {
assert_eq!(
parse_args(&["--version".to_string()]).expect("args should parse"),
CliAction::Version
CliAction::Version {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["-V".to_string()]).expect("args should parse"),
CliAction::Version
CliAction::Version {
output_format: CliOutputFormat::Text,
}
);
}
@@ -6579,6 +6780,7 @@ mod tests {
CliAction::PrintSystemPrompt {
cwd: PathBuf::from("/tmp/project"),
date: "2026-04-01".to_string(),
output_format: CliOutputFormat::Text,
}
);
}
@@ -6587,23 +6789,34 @@ mod tests {
fn parses_login_and_logout_subcommands() {
assert_eq!(
parse_args(&["login".to_string()]).expect("login should parse"),
CliAction::Login
CliAction::Login {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["logout".to_string()]).expect("logout should parse"),
CliAction::Logout
CliAction::Logout {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["doctor".to_string()]).expect("doctor should parse"),
CliAction::Doctor
CliAction::Doctor {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["init".to_string()]).expect("init should parse"),
CliAction::Init
CliAction::Init {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["agents".to_string()]).expect("agents should parse"),
CliAction::Agents { args: None }
CliAction::Agents {
args: None,
output_format: CliOutputFormat::Text
}
);
assert_eq!(
parse_args(&["mcp".to_string()]).expect("mcp should parse"),
@@ -6623,7 +6836,8 @@ mod tests {
parse_args(&["agents".to_string(), "--help".to_string()])
.expect("agents help should parse"),
CliAction::Agents {
args: Some("--help".to_string())
args: Some("--help".to_string()),
output_format: CliOutputFormat::Text,
}
);
}
@@ -6653,11 +6867,15 @@ mod tests {
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
assert_eq!(
parse_args(&["help".to_string()]).expect("help should parse"),
CliAction::Help
CliAction::Help {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["version".to_string()]).expect("version should parse"),
CliAction::Version
CliAction::Version {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["status".to_string()]).expect("status should parse"),
@@ -6727,7 +6945,10 @@ mod tests {
fn parses_direct_agents_mcp_and_skills_slash_commands() {
assert_eq!(
parse_args(&["/agents".to_string()]).expect("/agents should parse"),
CliAction::Agents { args: None }
CliAction::Agents {
args: None,
output_format: CliOutputFormat::Text
}
);
assert_eq!(
parse_args(&["/mcp".to_string(), "show".to_string(), "demo".to_string()])
@@ -6807,6 +7028,7 @@ mod tests {
CliAction::ResumeSession {
session_path: PathBuf::from("session.jsonl"),
commands: vec!["/compact".to_string()],
output_format: CliOutputFormat::Text,
}
);
}
@@ -6818,6 +7040,7 @@ mod tests {
CliAction::ResumeSession {
session_path: PathBuf::from("latest"),
commands: vec![],
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
@@ -6826,6 +7049,7 @@ mod tests {
CliAction::ResumeSession {
session_path: PathBuf::from("latest"),
commands: vec!["/status".to_string()],
output_format: CliOutputFormat::Text,
}
);
}
@@ -6848,6 +7072,7 @@ mod tests {
"/compact".to_string(),
"/cost".to_string(),
],
output_format: CliOutputFormat::Text,
}
);
}
@@ -6878,6 +7103,7 @@ mod tests {
"/export notes.txt".to_string(),
"/clear --confirm".to_string(),
],
output_format: CliOutputFormat::Text,
}
);
}
@@ -6896,6 +7122,7 @@ mod tests {
CliAction::ResumeSession {
session_path: PathBuf::from("session.jsonl"),
commands: vec!["/export /tmp/notes.txt".to_string(), "/status".to_string()],
output_format: CliOutputFormat::Text,
}
);
}
@@ -8005,6 +8232,7 @@ UU conflicted.rs",
}
#[test]
#[allow(clippy::too_many_lines)]
fn build_runtime_plugin_state_discovers_mcp_tools_and_surfaces_pending_servers() {
let config_home = temp_dir();
let workspace = temp_dir();

View File

@@ -729,8 +729,7 @@ fn assert_token_cost_reporting(_: &HarnessWorkspace, run: &ScenarioRun) {
assert!(
run.response["estimated_cost"]
.as_str()
.map(|cost| cost.starts_with('$'))
.unwrap_or(false),
.is_some_and(|cost| cost.starts_with('$')),
"estimated_cost should be a dollar-prefixed string"
);
}

View File

@@ -0,0 +1,193 @@
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use serde_json::Value;
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
#[test]
fn help_emits_json_when_requested() {
let root = unique_temp_dir("help-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let parsed = assert_json_command(&root, &["--output-format", "json", "help"]);
assert_eq!(parsed["kind"], "help");
assert!(parsed["message"]
.as_str()
.expect("help text")
.contains("Usage:"));
}
#[test]
fn version_emits_json_when_requested() {
let root = unique_temp_dir("version-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let parsed = assert_json_command(&root, &["--output-format", "json", "version"]);
assert_eq!(parsed["kind"], "version");
assert_eq!(parsed["version"], env!("CARGO_PKG_VERSION"));
}
#[test]
fn status_and_sandbox_emit_json_when_requested() {
let root = unique_temp_dir("status-sandbox-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let status = assert_json_command(&root, &["--output-format", "json", "status"]);
assert_eq!(status["kind"], "status");
assert!(status["workspace"]["cwd"].as_str().is_some());
let sandbox = assert_json_command(&root, &["--output-format", "json", "sandbox"]);
assert_eq!(sandbox["kind"], "sandbox");
assert!(sandbox["filesystem_mode"].as_str().is_some());
}
#[test]
fn inventory_commands_emit_structured_json_when_requested() {
let root = unique_temp_dir("inventory-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let agents = assert_json_command(&root, &["--output-format", "json", "agents"]);
assert_eq!(agents["kind"], "agents");
let mcp = assert_json_command(&root, &["--output-format", "json", "mcp"]);
assert_eq!(mcp["kind"], "mcp");
assert_eq!(mcp["action"], "list");
let skills = assert_json_command(&root, &["--output-format", "json", "skills"]);
assert_eq!(skills["kind"], "skills");
assert_eq!(skills["action"], "list");
}
#[test]
fn bootstrap_and_system_prompt_emit_json_when_requested() {
let root = unique_temp_dir("bootstrap-system-prompt-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let plan = assert_json_command(&root, &["--output-format", "json", "bootstrap-plan"]);
assert_eq!(plan["kind"], "bootstrap-plan");
assert!(plan["phases"].as_array().expect("phases").len() > 1);
let prompt = assert_json_command(&root, &["--output-format", "json", "system-prompt"]);
assert_eq!(prompt["kind"], "system-prompt");
assert!(prompt["message"]
.as_str()
.expect("prompt text")
.contains("interactive agent"));
}
#[test]
fn dump_manifests_and_init_emit_json_when_requested() {
let root = unique_temp_dir("manifest-init-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let upstream = write_upstream_fixture(&root);
let manifests = assert_json_command_with_env(
&root,
&["--output-format", "json", "dump-manifests"],
&[(
"CLAUDE_CODE_UPSTREAM",
upstream.to_str().expect("utf8 upstream"),
)],
);
assert_eq!(manifests["kind"], "dump-manifests");
assert_eq!(manifests["commands"], 1);
assert_eq!(manifests["tools"], 1);
let workspace = root.join("workspace");
fs::create_dir_all(&workspace).expect("workspace should exist");
let init = assert_json_command(&workspace, &["--output-format", "json", "init"]);
assert_eq!(init["kind"], "init");
assert!(workspace.join("CLAUDE.md").exists());
}
#[test]
fn doctor_and_resume_status_emit_json_when_requested() {
let root = unique_temp_dir("doctor-resume-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let doctor = assert_json_command(&root, &["--output-format", "json", "doctor"]);
assert_eq!(doctor["kind"], "doctor");
assert!(doctor["message"].is_string());
let session_path = root.join("session.jsonl");
fs::write(
&session_path,
"{\"type\":\"session_meta\",\"version\":3,\"session_id\":\"resume-json\",\"created_at_ms\":0,\"updated_at_ms\":0}\n{\"type\":\"message\",\"message\":{\"role\":\"user\",\"blocks\":[{\"type\":\"text\",\"text\":\"hello\"}]}}\n",
)
.expect("session should write");
let resumed = assert_json_command(
&root,
&[
"--output-format",
"json",
"--resume",
session_path.to_str().expect("utf8 session path"),
"/status",
],
);
assert_eq!(resumed["kind"], "status");
assert_eq!(resumed["messages"], 1);
}
fn assert_json_command(current_dir: &Path, args: &[&str]) -> Value {
assert_json_command_with_env(current_dir, args, &[])
}
fn assert_json_command_with_env(current_dir: &Path, args: &[&str], envs: &[(&str, &str)]) -> Value {
let output = run_claw(current_dir, args, envs);
assert!(
output.status.success(),
"stdout:\n{}\n\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
serde_json::from_slice(&output.stdout).expect("stdout should be valid json")
}
fn run_claw(current_dir: &Path, args: &[&str], envs: &[(&str, &str)]) -> Output {
let mut command = Command::new(env!("CARGO_BIN_EXE_claw"));
command.current_dir(current_dir).args(args);
for (key, value) in envs {
command.env(key, value);
}
command.output().expect("claw should launch")
}
fn write_upstream_fixture(root: &Path) -> PathBuf {
let upstream = root.join("claw-code");
let src = upstream.join("src");
let entrypoints = src.join("entrypoints");
fs::create_dir_all(&entrypoints).expect("upstream entrypoints dir should exist");
fs::write(
src.join("commands.ts"),
"import FooCommand from './commands/foo'\n",
)
.expect("commands fixture should write");
fs::write(
src.join("tools.ts"),
"import ReadTool from './tools/read'\n",
)
.expect("tools fixture should write");
fs::write(
entrypoints.join("cli.tsx"),
"if (args[0] === '--version') {}\nstartupProfiler()\n",
)
.expect("cli fixture should write");
upstream
}
fn unique_temp_dir(label: &str) -> PathBuf {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should be after epoch")
.as_millis();
let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!(
"claw-output-format-{label}-{}-{millis}-{counter}",
std::process::id()
))
}

View File

@@ -11,7 +11,8 @@ use api::{
use plugins::PluginTool;
use reqwest::blocking::Client;
use runtime::{
check_freshness, edit_file, execute_bash, glob_search, grep_search, load_system_prompt,
check_freshness, dedupe_superseded_commit_events, edit_file, execute_bash, glob_search,
grep_search, load_system_prompt,
lsp_client::LspRegistry,
mcp_tool_bridge::McpToolRegistry,
permission_enforcer::{EnforcementResult, PermissionEnforcer},
@@ -1704,7 +1705,7 @@ fn run_remote_trigger(input: RemoteTriggerInput) -> Result<String, String> {
"method": method,
"status_code": status,
"body": truncated_body,
"success": status >= 200 && status < 300
"success": (200..300).contains(&status)
}))
}
Err(e) => to_pretty_json(json!({
@@ -3276,9 +3277,11 @@ fn agent_permission_policy() -> PermissionPolicy {
}
fn write_agent_manifest(manifest: &AgentOutput) -> Result<(), String> {
let mut normalized = manifest.clone();
normalized.lane_events = dedupe_superseded_commit_events(&normalized.lane_events);
std::fs::write(
&manifest.manifest_file,
serde_json::to_string_pretty(manifest).map_err(|error| error.to_string())?,
&normalized.manifest_file,
serde_json::to_string_pretty(&normalized).map_err(|error| error.to_string())?,
)
.map_err(|error| error.to_string())
}
@@ -3297,7 +3300,7 @@ fn persist_agent_terminal_state(
let mut next_manifest = manifest.clone();
next_manifest.status = status.to_string();
next_manifest.completed_at = Some(iso8601_now());
next_manifest.current_blocker = blocker.clone();
next_manifest.current_blocker.clone_from(&blocker);
next_manifest.error = error;
if let Some(blocker) = blocker {
next_manifest
@@ -5823,6 +5826,7 @@ mod tests {
}
#[test]
#[allow(clippy::too_many_lines)]
fn agent_fake_runner_can_persist_completion_and_failure() {
let _guard = env_lock()
.lock()