Skip to content

Commit ae1b108

Browse files
Merge remote-tracking branch 'upstream/main' into dev
2 parents 71a6770 + 7bc66e8 commit ae1b108

8 files changed

Lines changed: 1981 additions & 75 deletions

File tree

ROADMAP.md

Lines changed: 715 additions & 1 deletion
Large diffs are not rendered by default.

USAGE.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,26 @@ cd rust
5252

5353
**Note:** Diagnostic verbs (`doctor`, `status`, `sandbox`, `version`) support `--output-format json` for machine-readable output. Invalid suffix arguments (e.g., `--json`) are now rejected at parse time rather than falling through to prompt dispatch.
5454

55+
### Initialize a repository
56+
57+
Set up a new repository with `.claw` config, `.claw.json`, `.gitignore` entries, and a `CLAUDE.md` guidance file:
58+
59+
```bash
60+
cd /path/to/your/repo
61+
./target/debug/claw init
62+
```
63+
64+
Text mode (human-readable) shows artifact creation summary with project path and next steps. Idempotent — running multiple times in the same repo marks already-created files as "skipped".
65+
66+
JSON mode for scripting:
67+
```bash
68+
./target/debug/claw init --output-format json
69+
```
70+
71+
Returns structured output with `project_path`, `created[]`, `updated[]`, `skipped[]` arrays (one per artifact), and `artifacts[]` carrying each file's `name` and machine-stable `status` tag. The legacy `message` field preserves backward compatibility.
72+
73+
**Why structured fields matter:** Claws can detect per-artifact state (`created` vs `updated` vs `skipped`) without substring-matching human prose. Use the `created[]`, `updated[]`, and `skipped[]` arrays for conditional follow-up logic (e.g., only commit if files were actually created, not just updated).
74+
5575
### Interactive REPL
5676

5777
```bash
@@ -80,6 +100,31 @@ cd rust
80100
./target/debug/claw --output-format json prompt "status"
81101
```
82102

103+
### Inspect worker state
104+
105+
The `claw state` command reads `.claw/worker-state.json`, which is written by the interactive REPL or a one-shot prompt when a worker executes a task. This file contains the worker ID, session reference, model, and permission mode.
106+
107+
Prerequisite: You must run `claw` (interactive REPL) or `claw prompt <text>` at least once in the repository to produce the worker state file.
108+
109+
```bash
110+
cd rust
111+
./target/debug/claw state
112+
```
113+
114+
JSON mode:
115+
```bash
116+
./target/debug/claw state --output-format json
117+
```
118+
119+
If you run `claw state` before any worker has executed, you will see a helpful error:
120+
```
121+
error: no worker state file found at .claw/worker-state.json
122+
Hint: worker state is written by the interactive REPL or a non-interactive prompt.
123+
Run: claw # start the REPL (writes state on first turn)
124+
Or: claw prompt <text> # run one non-interactive turn
125+
Then rerun: claw state [--output-format json]
126+
```
127+
83128
## Model and permission controls
84129

85130
```bash

rust/crates/commands/src/lib.rs

Lines changed: 157 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2554,11 +2554,22 @@ fn render_mcp_report_for(
25542554

25552555
match normalize_optional_args(args) {
25562556
None | Some("list") => {
2557-
let runtime_config = loader.load()?;
2558-
Ok(render_mcp_summary_report(
2559-
cwd,
2560-
runtime_config.mcp().servers(),
2561-
))
2557+
// #144: degrade gracefully on config parse failure (same contract
2558+
// as #143 for `status`). Text mode prepends a "Config load error"
2559+
// block before the MCP list; the list falls back to empty.
2560+
match loader.load() {
2561+
Ok(runtime_config) => Ok(render_mcp_summary_report(
2562+
cwd,
2563+
runtime_config.mcp().servers(),
2564+
)),
2565+
Err(err) => {
2566+
let empty = std::collections::BTreeMap::new();
2567+
Ok(format!(
2568+
"Config load error\n Status fail\n Summary runtime config failed to load; reporting partial MCP view\n Details {err}\n Hint `claw doctor` classifies config parse errors; fix the listed field and rerun\n\n{}",
2569+
render_mcp_summary_report(cwd, &empty)
2570+
))
2571+
}
2572+
}
25622573
}
25632574
Some(args) if is_help_arg(args) => Ok(render_mcp_usage(None)),
25642575
Some("show") => Ok(render_mcp_usage(Some("show"))),
@@ -2571,12 +2582,19 @@ fn render_mcp_report_for(
25712582
if parts.next().is_some() {
25722583
return Ok(render_mcp_usage(Some(args)));
25732584
}
2574-
let runtime_config = loader.load()?;
2575-
Ok(render_mcp_server_report(
2576-
cwd,
2577-
server_name,
2578-
runtime_config.mcp().get(server_name),
2579-
))
2585+
// #144: same degradation for `mcp show`; if config won't parse,
2586+
// the specific server lookup can't succeed, so report the parse
2587+
// error with context.
2588+
match loader.load() {
2589+
Ok(runtime_config) => Ok(render_mcp_server_report(
2590+
cwd,
2591+
server_name,
2592+
runtime_config.mcp().get(server_name),
2593+
)),
2594+
Err(err) => Ok(format!(
2595+
"Config load error\n Status fail\n Summary runtime config failed to load; cannot resolve `{server_name}`\n Details {err}\n Hint `claw doctor` classifies config parse errors; fix the listed field and rerun"
2596+
)),
2597+
}
25802598
}
25812599
Some(args) => Ok(render_mcp_usage(Some(args))),
25822600
}
@@ -2599,11 +2617,35 @@ fn render_mcp_report_json_for(
25992617

26002618
match normalize_optional_args(args) {
26012619
None | Some("list") => {
2602-
let runtime_config = loader.load()?;
2603-
Ok(render_mcp_summary_report_json(
2604-
cwd,
2605-
runtime_config.mcp().servers(),
2606-
))
2620+
// #144: match #143's degraded envelope contract. On config parse
2621+
// failure, emit top-level `status: "degraded"` with
2622+
// `config_load_error`, empty servers[], and exit 0. On clean
2623+
// runs, the existing serializer adds `status: "ok"` below.
2624+
match loader.load() {
2625+
Ok(runtime_config) => {
2626+
let mut value = render_mcp_summary_report_json(
2627+
cwd,
2628+
runtime_config.mcp().servers(),
2629+
);
2630+
if let Some(map) = value.as_object_mut() {
2631+
map.insert("status".to_string(), Value::String("ok".to_string()));
2632+
map.insert("config_load_error".to_string(), Value::Null);
2633+
}
2634+
Ok(value)
2635+
}
2636+
Err(err) => {
2637+
let empty = std::collections::BTreeMap::new();
2638+
let mut value = render_mcp_summary_report_json(cwd, &empty);
2639+
if let Some(map) = value.as_object_mut() {
2640+
map.insert("status".to_string(), Value::String("degraded".to_string()));
2641+
map.insert(
2642+
"config_load_error".to_string(),
2643+
Value::String(err.to_string()),
2644+
);
2645+
}
2646+
Ok(value)
2647+
}
2648+
}
26072649
}
26082650
Some(args) if is_help_arg(args) => Ok(render_mcp_usage_json(None)),
26092651
Some("show") => Ok(render_mcp_usage_json(Some("show"))),
@@ -2616,12 +2658,29 @@ fn render_mcp_report_json_for(
26162658
if parts.next().is_some() {
26172659
return Ok(render_mcp_usage_json(Some(args)));
26182660
}
2619-
let runtime_config = loader.load()?;
2620-
Ok(render_mcp_server_report_json(
2621-
cwd,
2622-
server_name,
2623-
runtime_config.mcp().get(server_name),
2624-
))
2661+
// #144: same degradation pattern for show action.
2662+
match loader.load() {
2663+
Ok(runtime_config) => {
2664+
let mut value = render_mcp_server_report_json(
2665+
cwd,
2666+
server_name,
2667+
runtime_config.mcp().get(server_name),
2668+
);
2669+
if let Some(map) = value.as_object_mut() {
2670+
map.insert("status".to_string(), Value::String("ok".to_string()));
2671+
map.insert("config_load_error".to_string(), Value::Null);
2672+
}
2673+
Ok(value)
2674+
}
2675+
Err(err) => Ok(serde_json::json!({
2676+
"kind": "mcp",
2677+
"action": "show",
2678+
"server": server_name,
2679+
"status": "degraded",
2680+
"config_load_error": err.to_string(),
2681+
"working_directory": cwd.display().to_string(),
2682+
})),
2683+
}
26252684
}
26262685
Some(args) => Ok(render_mcp_usage_json(Some(args))),
26272686
}
@@ -5479,6 +5538,82 @@ mod tests {
54795538
let _ = fs::remove_dir_all(config_home);
54805539
}
54815540

5541+
#[test]
5542+
fn mcp_degrades_gracefully_on_malformed_mcp_config_144() {
5543+
// #144: mirror of #143's partial-success contract for `claw mcp`.
5544+
// Previously `mcp` hard-failed on any config parse error, hiding
5545+
// well-formed servers and forcing claws to fall back to `doctor`.
5546+
// Now `mcp` emits a degraded envelope instead: exit 0, status:
5547+
// "degraded", config_load_error populated, servers[] empty.
5548+
let _guard = env_guard();
5549+
let workspace = temp_dir("mcp-degrades-144");
5550+
let config_home = temp_dir("mcp-degrades-144-cfg");
5551+
fs::create_dir_all(workspace.join(".claw")).expect("create workspace .claw dir");
5552+
fs::create_dir_all(&config_home).expect("create config home");
5553+
// One valid server + one malformed entry missing `command`.
5554+
fs::write(
5555+
workspace.join(".claw.json"),
5556+
r#"{
5557+
"mcpServers": {
5558+
"everything": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-everything"]},
5559+
"missing-command": {"args": ["arg-only-no-command"]}
5560+
}
5561+
}
5562+
"#,
5563+
)
5564+
.expect("write malformed .claw.json");
5565+
5566+
let loader = ConfigLoader::new(&workspace, &config_home);
5567+
// list action: must return Ok (not Err) with degraded envelope.
5568+
let list = render_mcp_report_json_for(&loader, &workspace, None)
5569+
.expect("mcp list should not hard-fail on config parse errors (#144)");
5570+
assert_eq!(list["kind"], "mcp");
5571+
assert_eq!(list["action"], "list");
5572+
assert_eq!(
5573+
list["status"].as_str(),
5574+
Some("degraded"),
5575+
"top-level status should be 'degraded': {list}"
5576+
);
5577+
let err = list["config_load_error"]
5578+
.as_str()
5579+
.expect("config_load_error must be a string on degraded runs");
5580+
assert!(
5581+
err.contains("mcpServers.missing-command"),
5582+
"config_load_error should name the malformed field path: {err}"
5583+
);
5584+
assert_eq!(list["configured_servers"], 0);
5585+
assert!(list["servers"].as_array().unwrap().is_empty());
5586+
5587+
// show action: should also degrade (not hard-fail).
5588+
let show = render_mcp_report_json_for(&loader, &workspace, Some("show everything"))
5589+
.expect("mcp show should not hard-fail on config parse errors (#144)");
5590+
assert_eq!(show["kind"], "mcp");
5591+
assert_eq!(show["action"], "show");
5592+
assert_eq!(
5593+
show["status"].as_str(),
5594+
Some("degraded"),
5595+
"show action should also report status: 'degraded': {show}"
5596+
);
5597+
assert!(show["config_load_error"].is_string());
5598+
5599+
// Clean path: status: "ok", config_load_error: null.
5600+
let clean_ws = temp_dir("mcp-degrades-144-clean");
5601+
fs::create_dir_all(&clean_ws).expect("clean ws");
5602+
let clean_loader = ConfigLoader::new(&clean_ws, &config_home);
5603+
let clean_list = render_mcp_report_json_for(&clean_loader, &clean_ws, None)
5604+
.expect("clean mcp list should succeed");
5605+
assert_eq!(
5606+
clean_list["status"].as_str(),
5607+
Some("ok"),
5608+
"clean run should report status: 'ok'"
5609+
);
5610+
assert!(clean_list["config_load_error"].is_null());
5611+
5612+
let _ = fs::remove_dir_all(workspace);
5613+
let _ = fs::remove_dir_all(config_home);
5614+
let _ = fs::remove_dir_all(clean_ws);
5615+
}
5616+
54825617
#[test]
54835618
fn parses_quoted_skill_frontmatter_values() {
54845619
let contents = "---\nname: \"hud\"\ndescription: 'Quoted description'\n---\n";

rust/crates/runtime/src/config.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1254,11 +1254,21 @@ mod tests {
12541254
use std::time::{SystemTime, UNIX_EPOCH};
12551255

12561256
fn temp_dir() -> std::path::PathBuf {
1257+
// #149: previously used `runtime-config-{nanos}` which collided
1258+
// under parallel `cargo test --workspace` when multiple tests
1259+
// started within the same nanosecond bucket on fast machines.
1260+
// Add process id + a monotonically-incrementing atomic counter
1261+
// so every callsite gets a provably-unique directory regardless
1262+
// of clock resolution or scheduling.
1263+
use std::sync::atomic::{AtomicU64, Ordering};
1264+
static COUNTER: AtomicU64 = AtomicU64::new(0);
12571265
let nanos = SystemTime::now()
12581266
.duration_since(UNIX_EPOCH)
12591267
.expect("time should be after epoch")
12601268
.as_nanos();
1261-
std::env::temp_dir().join(format!("runtime-config-{nanos}"))
1269+
let pid = std::process::id();
1270+
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
1271+
std::env::temp_dir().join(format!("runtime-config-{pid}-{nanos}-{seq}"))
12621272
}
12631273

12641274
#[test]

0 commit comments

Comments
 (0)