Skip to content

Commit 541c5bb

Browse files
committed
feat: ultraworkers#139 actionable worker-state guidance in claw state error + help
Previously `claw state` errored with "no worker state file found ... — run a worker first" but there is no `claw worker` subcommand, so claws had no discoverable path from the error to a fix. Changes: - Rewrite the missing-state error to name the two concrete commands that produce .claw/worker-state.json: * `claw` (interactive REPL, writes state on first turn) * `claw prompt <text>` (one non-interactive turn) Also tell the user what to rerun: `claw state [--output-format json]`. - Expand the State --help topic with "Produces state", "Observes state", and "Exit codes" lines so the worker-state contract is discoverable before the user hits the error. - Add regression test state_error_surfaces_actionable_worker_commands_139 asserting the error contains `claw prompt`, REPL mention, and the rerun path, plus that the help topic documents the producer contract. Verified live: $ claw state error: no worker state file found at .claw/worker-state.json Hint: worker state is written by the interactive REPL or a non-interactive prompt. Run: claw # start the REPL (writes state on first turn) Or: claw prompt <text> # run one non-interactive turn Then rerun: claw state [--output-format json] JSON mode preserves the full hint inside the error envelope so CI/claws can match on `claw prompt` without losing the canonical prefix. Full workspace test green except pre-existing resume_latest flake (unrelated). Closes ROADMAP ultraworkers#139.
1 parent 611eed1 commit 541c5bb

1 file changed

Lines changed: 65 additions & 10 deletions

File tree

  • rust/crates/rusty-claude-cli/src

rust/crates/rusty-claude-cli/src/main.rs

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1690,14 +1690,21 @@ fn run_worker_state(output_format: CliOutputFormat) -> Result<(), Box<dyn std::e
16901690
let cwd = env::current_dir()?;
16911691
let state_path = cwd.join(".claw").join("worker-state.json");
16921692
if !state_path.exists() {
1693-
// Emit a structured error, then return Err so the process exits 1.
1694-
// Callers (scripts, CI) need a non-zero exit to detect "no state" without
1695-
// parsing prose output.
1696-
// Let the error propagate to main() which will format it correctly
1697-
// (prose for text mode, JSON envelope for --output-format json).
1693+
// #139: this error used to say "run a worker first" without telling
1694+
// callers how to run one. "worker" is an internal concept (there is
1695+
// no `claw worker` subcommand), so claws/CI had no discoverable path
1696+
// from the error to a fix. Emit an actionable, structured error that
1697+
// names the two concrete commands that produce worker state.
1698+
//
1699+
// Format in both text and JSON modes is stable so scripts can match:
1700+
// error: no worker state file found at <path>
1701+
// Hint: worker state is written by the interactive REPL or a non-interactive prompt.
1702+
// Run: claw # start the REPL (writes state on first turn)
1703+
// Or: claw prompt <text> # run one non-interactive turn
1704+
// Then rerun: claw state [--output-format json]
16981705
return Err(format!(
1699-
"no worker state file found at {} — run a worker first",
1700-
state_path.display()
1706+
"no worker state file found at {path}\n Hint: worker state is written by the interactive REPL or a non-interactive prompt.\n Run: claw # start the REPL (writes state on first turn)\n Or: claw prompt <text> # run one non-interactive turn\n Then rerun: claw state [--output-format json]",
1707+
path = state_path.display()
17011708
)
17021709
.into());
17031710
}
@@ -5400,11 +5407,13 @@ fn render_help_topic(topic: LocalHelpTopic) -> String {
54005407
.to_string(),
54015408
LocalHelpTopic::State => "State
54025409
Usage claw state [--output-format <format>]
5403-
Purpose read the worker state file written by the interactive REPL
5410+
Purpose read .claw/worker-state.json written by the interactive REPL or a one-shot prompt
54045411
Output worker id, model, permissions, session reference (text or json)
54055412
Formats text (default), json
5406-
Prerequisite run `claw` interactively or `claw prompt <text>` to produce worker state first
5407-
Related ROADMAP #139 (worker-concept discoverability) · claw status"
5413+
Produces state `claw` (interactive REPL) or `claw prompt <text>` (one non-interactive turn)
5414+
Observes state `claw state` reads; clawhip/CI may poll this file without HTTP
5415+
Exit codes 0 if state file exists and parses; 1 with actionable hint otherwise
5416+
Related claw status · ROADMAP #139 (this worker-concept contract)"
54085417
.to_string(),
54095418
LocalHelpTopic::Export => "Export
54105419
Usage claw export [--session <id|latest>] [--output <path>] [--output-format <format>]
@@ -9614,6 +9623,52 @@ mod tests {
96149623
}
96159624
}
96169625

9626+
#[test]
9627+
fn state_error_surfaces_actionable_worker_commands_139() {
9628+
// #139: the error for missing `.claw/worker-state.json` must name
9629+
// the concrete commands that produce worker state, otherwise claws
9630+
// have no discoverable path from the error to a fix.
9631+
let _guard = env_lock();
9632+
let root = temp_dir();
9633+
let cwd = root.join("project-with-no-state");
9634+
std::fs::create_dir_all(&cwd).expect("project dir should exist");
9635+
9636+
let error = with_current_dir(&cwd, || {
9637+
super::run_worker_state(CliOutputFormat::Text).expect_err("missing state should error")
9638+
});
9639+
let message = error.to_string();
9640+
9641+
// Keep the original locator so scripts grepping for it still work.
9642+
assert!(
9643+
message.contains("no worker state file found at"),
9644+
"error should keep the canonical prefix: {message}"
9645+
);
9646+
// New actionable hints — this is what #139 is fixing.
9647+
assert!(
9648+
message.contains("claw prompt"),
9649+
"error should name `claw prompt <text>` as a producer: {message}"
9650+
);
9651+
assert!(
9652+
message.contains("REPL"),
9653+
"error should mention the interactive REPL as a producer: {message}"
9654+
);
9655+
assert!(
9656+
message.contains("claw state"),
9657+
"error should tell the user what to rerun once state exists: {message}"
9658+
);
9659+
// And the State --help topic must document the worker relationship
9660+
// so claws can discover the contract without hitting the error first.
9661+
let state_help = render_help_topic(LocalHelpTopic::State);
9662+
assert!(
9663+
state_help.contains("Produces state"),
9664+
"state help must document how state is produced: {state_help}"
9665+
);
9666+
assert!(
9667+
state_help.contains("claw prompt"),
9668+
"state help must name `claw prompt <text>` as a producer: {state_help}"
9669+
);
9670+
}
9671+
96179672
#[test]
96189673
fn parses_single_word_command_aliases_without_falling_back_to_prompt_mode() {
96199674
let _guard = env_lock();

0 commit comments

Comments
 (0)