mirror of
https://github.com/instructkr/claude-code.git
synced 2026-06-05 03:56:45 +00:00
fix: keep failed resume side-effect free
Generated with https://github.com/Yeachan-Heo/gajae-code Co-authored-by: Gajae Code <dev@gajae-code.com>
This commit is contained in:
@@ -6377,10 +6377,10 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
|
|||||||
433. **DONE — `--output-format` selection is typed, case-insensitive, env-configurable, and auditable** — fixed 2026-06-04 in `fix: type output format selection`. `CliOutputFormat::parse` now accepts `text`/`json` in any casing, `CLAW_OUTPUT_FORMAT` seeds the default output format when no CLI output-format flag is present, explicit flags override the env default, and repeated flags emit `warning: --output-format specified multiple times; using last value '...'`. `status --output-format json` exposes `format_source`, `format_raw`, and `format_overridden`; invalid values return typed `invalid_output_format` JSON with `value`, `expected:["text","json"]`, and a recovery hint instead of `kind:"unknown"`. Top-level help documents `CLAW_OUTPUT_FORMAT`, `CLAW_LOG`, and `RUST_LOG`, and doctor system JSON surfaces those env values. Regression coverage: `output_format_flags_and_env_have_typed_contract_433` and `classify_error_kind_returns_correct_discriminants`. Verification: `cargo fmt --manifest-path rust/Cargo.toml --all -- --check`, focused output-format and classifier tests, `scripts/roadmap-check-ids.sh`, `git diff --check`, `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --no-run`, and `cargo build --manifest-path rust/Cargo.toml --workspace --locked`.
|
433. **DONE — `--output-format` selection is typed, case-insensitive, env-configurable, and auditable** — fixed 2026-06-04 in `fix: type output format selection`. `CliOutputFormat::parse` now accepts `text`/`json` in any casing, `CLAW_OUTPUT_FORMAT` seeds the default output format when no CLI output-format flag is present, explicit flags override the env default, and repeated flags emit `warning: --output-format specified multiple times; using last value '...'`. `status --output-format json` exposes `format_source`, `format_raw`, and `format_overridden`; invalid values return typed `invalid_output_format` JSON with `value`, `expected:["text","json"]`, and a recovery hint instead of `kind:"unknown"`. Top-level help documents `CLAW_OUTPUT_FORMAT`, `CLAW_LOG`, and `RUST_LOG`, and doctor system JSON surfaces those env values. Regression coverage: `output_format_flags_and_env_have_typed_contract_433` and `classify_error_kind_returns_correct_discriminants`. Verification: `cargo fmt --manifest-path rust/Cargo.toml --all -- --check`, focused output-format and classifier tests, `scripts/roadmap-check-ids.sh`, `git diff --check`, `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --no-run`, and `cargo build --manifest-path rust/Cargo.toml --workspace --locked`.
|
||||||
|
|
||||||
|
|
||||||
434. **DONE — POSIX `--` and dash-prefixed shorthand prompts stay on the prompt path** — fixed 2026-06-04 in CI recovery after `41678eb` turned Rust CI red. Global argument parsing now treats `--` as an end-of-flags separator, stops JSON-output pre-scans before the separator, and forwards every following token as positional prompt text. Shorthand prompt mode accepts dash-prefixed text that is not a registered or near-miss CLI flag (`-not-a-flag`, `--bogus-flag-like literal`) while still rejecting real typo-like options such as `--resum` with the `--resume` suggestion. Direct `/status` invocation again remains REPL/session-only so the existing parser contract is restored. Help, USAGE, and rust README document the `claw -- "-prompt-with-dash"` form. Regression coverage: `parses_dash_prefixed_prompt_text_434`, plus rerun CI-red parser tests `parses_bare_prompt_and_json_output_flag` and `parses_direct_agents_mcp_and_skills_slash_commands`. Verification: `cargo fmt --manifest-path rust/Cargo.toml --all -- --check`, focused parser tests, `scripts/roadmap-check-ids.sh`, `git diff --check -- USAGE.md rust/README.md rust/crates/rusty-claude-cli/src/main.rs ROADMAP.md`, docs source-of-truth/release-readiness/unit helper checks, `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --no-run`, `cargo build --manifest-path rust/Cargo.toml --workspace --locked`, and `cargo clippy --manifest-path rust/Cargo.toml --workspace`. Local `cargo test --manifest-path rust/Cargo.toml --workspace` still hits the pre-existing Darwin-only `runtime::worker_boot::startup_preflight_warns_when_git_metadata_is_not_writable` permission assertion after all CLI parser tests pass; the red GitHub jobs were parser failures on head `41678eb`.
|
434. **DONE — POSIX `--` and dash-prefixed shorthand prompts stay on the prompt path** — fixed 2026-06-04 in CI recovery after `41678eb` turned Rust CI red. Global argument parsing now treats `--` as an end-of-flags separator, stops JSON-output pre-scans before the separator, and forwards every following token as positional prompt text. Shorthand prompt mode accepts dash-prefixed text that is not a registered or near-miss CLI flag (`-not-a-flag`, `--bogus-flag-like literal`) while still rejecting real typo-like options such as `--resum` with the `--resume` suggestion. Direct `/status` invocation routes to the local status action per the resume-safe slash command contract, matching the CI-red parser regression tests restored after `7cfd83f`. Help, USAGE, and rust README document the `claw -- "-prompt-with-dash"` form. Regression coverage: `parses_dash_prefixed_prompt_text_434`, plus rerun CI-red parser tests `parses_bare_prompt_and_json_output_flag` and `parses_direct_agents_mcp_and_skills_slash_commands`. Verification: `cargo fmt --manifest-path rust/Cargo.toml --all -- --check`, focused parser tests, `scripts/roadmap-check-ids.sh`, `git diff --check -- USAGE.md rust/README.md rust/crates/rusty-claude-cli/src/main.rs ROADMAP.md`, docs source-of-truth/release-readiness/unit helper checks, `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --no-run`, `cargo build --manifest-path rust/Cargo.toml --workspace --locked`, and `cargo clippy --manifest-path rust/Cargo.toml --workspace`. Local `cargo test --manifest-path rust/Cargo.toml --workspace` still hits the pre-existing Darwin-only `runtime::worker_boot::startup_preflight_warns_when_git_metadata_is_not_writable` permission assertion after all CLI parser tests pass; the red GitHub jobs were parser failures on head `41678eb`.
|
||||||
|
|
||||||
|
|
||||||
435. **`claw --resume latest` on a fresh workspace exit code is 0 in text mode but 1 in JSON mode (text mode lies about success); sibling: failed `--resume` creates the `.claw/sessions/<fingerprint>/` directory tree as a filesystem side effect of the failure** — dogfooded 2026-05-11 by Jobdori on `e29010ed` in response to Clawhip pinpoint nudge at `1503305692566655096`. Reproduction (fresh empty dir, no `.claw/`, no sessions): `claw --resume latest` (text mode) prints `failed to restore session: no managed sessions found in .claw/sessions/0ead448127a2de44/` and exits **0**. Same invocation with `--output-format json` correctly exits **1** with `kind:"session_load_failed"`. Exit-code parity broken on the same input depending on format flag. **Sibling filesystem-side-effect bug:** after the failed `--resume latest` on a fresh empty workspace, the directory `.claw/sessions/0ead448127a2de44/` (the workspace-fingerprint partition) is created on disk despite the operation failing. The user did not opt into creating workspace metadata — they asked to resume an existing session, the resume failed, and now there's a partition directory hanging around. The fingerprint directory ought to be created lazily on first successful session save, not as a side effect of every resume attempt. **Three sibling findings in the same probe:** (a) **`claw --compact` alone (no other args) drops into the interactive REPL with the ANSI welcome banner** — `--compact` is documented as a modifier that strips tool call details in text mode for piping (`--compact ... useful for piping`), not as a verb that activates the REPL. Running `claw --compact` with no positional should be a no-op or an error explaining the flag needs a subcommand or prompt; entering the REPL is the wrong default. (b) **`claw --compact "hello"` (shorthand prompt) returns `{"error":"unknown subcommand: hello.","hint":"Did you mean help","kind":"unknown"}` — `--compact` disables shorthand prompt mode entirely**, treating the positional as a subcommand instead of as prompt text. Users must use the explicit `prompt` verb (`claw --compact prompt "hello"`) which contradicts the `claw [flags] TEXT` usage line in `--help`. (c) `kind:"unknown"` again for the unknown-subcommand error in --compact path — same catch-all bucket bug appearing for the 11th time across pinpoints. **Required fix shape:** (a) exit code 1 for all `failed_to_restore` / `session_load_failed` text-mode failures; text mode should print to stderr and exit non-zero, not print to stdout and exit 0; (b) defer `.claw/sessions/<fingerprint>/` creation to first successful save; failed `--resume` must not leave filesystem droppings; (c) `claw --compact` alone (no positional, no subcommand, stdin is TTY) should emit `kind:"missing_argument"` with `argument:"prompt or subcommand"` rather than activating the REPL; (d) `--compact` must be transparent to shorthand prompt mode parsing — `claw --compact "hello"` is equivalent to `claw --compact prompt "hello"`, both should reach the prompt path; (e) emit typed `kind:"unknown_subcommand"` not `kind:"unknown"` for fallthrough cases. **Why this matters:** scripts that gate on `$?` after `claw --resume latest` see success on text mode and failure on JSON mode — the same operation, two outcomes. The filesystem side effect pollutes a user's worktree with workspace partitions they didn't ask for, and CI pipelines that snapshot `.claw/` size silently grow on every failed `--resume`. Cross-references #422 (exit-code parity across error envelopes), #423 (`kind:"unknown"` for `missing_argument`), #434 (shorthand prompt limitations). Source: Jobdori live dogfood, `e29010ed`, 2026-05-11.
|
435. **DONE — failed resume is non-zero and side-effect free; `--compact` stays a prompt modifier** — fixed 2026-06-04 in `fix: keep failed resume side-effect free`. Fresh-workspace `claw --resume latest` exits 1 in text and JSON modes; text writes the restore failure to stderr, JSON writes a typed `no_managed_sessions` restore envelope to stdout, and failed lookup no longer creates `.claw/sessions/<fingerprint>/`. `SessionStore::from_cwd`/`from_data_dir` now only derive the fingerprinted path; session save remains responsible for creating it. Global `--compact` no longer starts the REPL when it has no prompt or stdin: it returns typed `missing_argument` with `argument:"prompt or subcommand"`. `claw --compact "hello"` remains shorthand prompt mode and reaches provider/auth validation rather than command-not-found. Regression coverage: `session_store_from_cwd_is_side_effect_free_until_save`, `resume_latest_missing_session_fails_without_creating_session_dirs_435`, `compact_flag_missing_argument_and_shorthand_prompt_contract_435`, and `parses_compact_flag_for_prompt_mode`; broader checks reran `runtime session_control`, `resume_slash_commands`, `output_format_contract`, `claw` bin tests, `cargo fmt --all -- --check`, `scripts/roadmap-check-ids.sh`, `git diff --check`, and `cargo build --workspace --locked`.
|
||||||
|
|
||||||
|
|
||||||
436. **`claw init` shipped `.claw.json` template explicitly sets `permissions.defaultMode:"dontAsk"` — every user who runs `claw init` gets a config file that disables permission prompts by default; sibling: `init` creates an empty `.claw/` directory with no settings.json template inside, and when `.claw/` already exists it skips the whole artifact (no settings template materialized)** — dogfooded 2026-05-11 by Jobdori on `b8f989b6` in response to Clawhip pinpoint nudge at `1503313241751949335`. Reproduction: `mkdir /tmp/probe && cd /tmp/probe && claw init --output-format json` returns `artifacts:[{name:".claw/",status:"created"},{name:".claw.json",status:"created"},...]`. Inspecting the created `.claw.json`: `{"permissions":{"defaultMode":"dontAsk"}}`. This is the polar opposite of safe-by-default: every user who follows the documented onboarding flow (`claw init` after `curl install.sh`) ships their workspace with permission prompts disabled. Compounds with **#428** (default runtime permission_mode is `danger-full-access`) — between the runtime default and the init template, a fresh claw setup has zero user-facing safety friction. **Sibling: `.claw/` artifact is an empty directory.** After `claw init`, `find .claw -type f` returns nothing. No `settings.json`, no template, no scaffolding — just `mkdir .claw`. The `--help` description implies init produces a usable workspace, but `.claw/settings.json` (the project-scope counterpart of `~/.claw/settings.json`) is never templated. **Sibling: `.claw/` skip-on-exists drops the entire artifact.** If `.claw/` already exists (e.g., from a partial setup, a `--resume` failure side effect per #435, or manual creation), `claw init` returns `.claw/: skipped` and does not materialize any expected sub-content. The other artifacts (`.claw.json`, `.gitignore`, `CLAUDE.md`) are still created, but a future `claw skills install` or `claw plugins enable` may expect `.claw/` to contain template files that are now missing. **Required fix shape:** (a) the shipped `.claw.json` template must default to `permissions.defaultMode:"acceptEdits"` or `"plan"` (safe-by-default modes per #428 spec) — `"dontAsk"` requires explicit opt-in; (b) `claw init` must materialize `.claw/settings.json` with documented schema defaults inside `.claw/` so the directory is useful on its own; (c) when `.claw/` already exists, `init` must report `partial` status (not `skipped`) and still try to create missing sub-files like `.claw/settings.json` without overwriting existing files; (d) emit per-sub-file artifact entries for `.claw/settings.json` and `.claw/sessions/` (skipped status if absent, deferred-to-first-save acceptable) so automation knows what's present; (e) regression test: `claw init` produces a `.claw.json` whose `permissions.defaultMode` is NOT `dontAsk`; `.claw/` contains at least one templated file. **Why this matters:** init is the primary onboarding surface. Every first-time user piping `curl install.sh | sh && claw init` gets a workspace pre-configured to skip permission prompts — and that workspace gets committed to the user's repo via the `init`-added entry. The `.claw/` empty-directory bug means feature discovery (skills, plugins) lacks the scaffolding it implies. Cross-references #428 (runtime default permission_mode), #50/#87/#91/#94/#97/#101/#106/#115/#123 (permission-rule audit), #435 (filesystem side effects on failed resume). Source: Jobdori live dogfood, `b8f989b6`, 2026-05-11.
|
436. **`claw init` shipped `.claw.json` template explicitly sets `permissions.defaultMode:"dontAsk"` — every user who runs `claw init` gets a config file that disables permission prompts by default; sibling: `init` creates an empty `.claw/` directory with no settings.json template inside, and when `.claw/` already exists it skips the whole artifact (no settings template materialized)** — dogfooded 2026-05-11 by Jobdori on `b8f989b6` in response to Clawhip pinpoint nudge at `1503313241751949335`. Reproduction: `mkdir /tmp/probe && cd /tmp/probe && claw init --output-format json` returns `artifacts:[{name:".claw/",status:"created"},{name:".claw.json",status:"created"},...]`. Inspecting the created `.claw.json`: `{"permissions":{"defaultMode":"dontAsk"}}`. This is the polar opposite of safe-by-default: every user who follows the documented onboarding flow (`claw init` after `curl install.sh`) ships their workspace with permission prompts disabled. Compounds with **#428** (default runtime permission_mode is `danger-full-access`) — between the runtime default and the init template, a fresh claw setup has zero user-facing safety friction. **Sibling: `.claw/` artifact is an empty directory.** After `claw init`, `find .claw -type f` returns nothing. No `settings.json`, no template, no scaffolding — just `mkdir .claw`. The `--help` description implies init produces a usable workspace, but `.claw/settings.json` (the project-scope counterpart of `~/.claw/settings.json`) is never templated. **Sibling: `.claw/` skip-on-exists drops the entire artifact.** If `.claw/` already exists (e.g., from a partial setup, a `--resume` failure side effect per #435, or manual creation), `claw init` returns `.claw/: skipped` and does not materialize any expected sub-content. The other artifacts (`.claw.json`, `.gitignore`, `CLAUDE.md`) are still created, but a future `claw skills install` or `claw plugins enable` may expect `.claw/` to contain template files that are now missing. **Required fix shape:** (a) the shipped `.claw.json` template must default to `permissions.defaultMode:"acceptEdits"` or `"plan"` (safe-by-default modes per #428 spec) — `"dontAsk"` requires explicit opt-in; (b) `claw init` must materialize `.claw/settings.json` with documented schema defaults inside `.claw/` so the directory is useful on its own; (c) when `.claw/` already exists, `init` must report `partial` status (not `skipped`) and still try to create missing sub-files like `.claw/settings.json` without overwriting existing files; (d) emit per-sub-file artifact entries for `.claw/settings.json` and `.claw/sessions/` (skipped status if absent, deferred-to-first-save acceptable) so automation knows what's present; (e) regression test: `claw init` produces a `.claw.json` whose `permissions.defaultMode` is NOT `dontAsk`; `.claw/` contains at least one templated file. **Why this matters:** init is the primary onboarding surface. Every first-time user piping `curl install.sh | sh && claw init` gets a workspace pre-configured to skip permission prompts — and that workspace gets committed to the user's repo via the `init`-added entry. The `.claw/` empty-directory bug means feature discovery (skills, plugins) lacks the scaffolding it implies. Cross-references #428 (runtime default permission_mode), #50/#87/#91/#94/#97/#101/#106/#115/#123 (permission-rule audit), #435 (filesystem side effects on failed resume). Source: Jobdori live dogfood, `b8f989b6`, 2026-05-11.
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ pub struct SessionStore {
|
|||||||
impl SessionStore {
|
impl SessionStore {
|
||||||
/// Build a store from the server's current working directory.
|
/// Build a store from the server's current working directory.
|
||||||
///
|
///
|
||||||
/// The on-disk layout becomes `<cwd>/.claw/sessions/<workspace_hash>/`.
|
/// The on-disk layout is `<cwd>/.claw/sessions/<workspace_hash>/`,
|
||||||
|
/// created lazily on first successful session save.
|
||||||
pub fn from_cwd(cwd: impl AsRef<Path>) -> Result<Self, SessionControlError> {
|
pub fn from_cwd(cwd: impl AsRef<Path>) -> Result<Self, SessionControlError> {
|
||||||
let cwd = cwd.as_ref();
|
let cwd = cwd.as_ref();
|
||||||
// #151: canonicalize so equivalent paths (symlinks, relative vs
|
// #151: canonicalize so equivalent paths (symlinks, relative vs
|
||||||
@@ -40,7 +41,6 @@ impl SessionStore {
|
|||||||
.join(".claw")
|
.join(".claw")
|
||||||
.join("sessions")
|
.join("sessions")
|
||||||
.join(workspace_fingerprint(&canonical_cwd));
|
.join(workspace_fingerprint(&canonical_cwd));
|
||||||
fs::create_dir_all(&sessions_root)?;
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
sessions_root,
|
sessions_root,
|
||||||
workspace_root: canonical_cwd,
|
workspace_root: canonical_cwd,
|
||||||
@@ -49,7 +49,8 @@ impl SessionStore {
|
|||||||
|
|
||||||
/// Build a store from an explicit `--data-dir` flag.
|
/// Build a store from an explicit `--data-dir` flag.
|
||||||
///
|
///
|
||||||
/// The on-disk layout becomes `<data_dir>/sessions/<workspace_hash>/`
|
/// The on-disk layout is `<data_dir>/sessions/<workspace_hash>/`,
|
||||||
|
/// created lazily on first successful session save.
|
||||||
/// where `<workspace_hash>` is derived from `workspace_root`.
|
/// where `<workspace_hash>` is derived from `workspace_root`.
|
||||||
pub fn from_data_dir(
|
pub fn from_data_dir(
|
||||||
data_dir: impl AsRef<Path>,
|
data_dir: impl AsRef<Path>,
|
||||||
@@ -64,7 +65,6 @@ impl SessionStore {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.join("sessions")
|
.join("sessions")
|
||||||
.join(workspace_fingerprint(&canonical_workspace));
|
.join(workspace_fingerprint(&canonical_workspace));
|
||||||
fs::create_dir_all(&sessions_root)?;
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
sessions_root,
|
sessions_root,
|
||||||
workspace_root: canonical_workspace,
|
workspace_root: canonical_workspace,
|
||||||
@@ -760,14 +760,21 @@ mod tests {
|
|||||||
use crate::session::Session;
|
use crate::session::Session;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
fn temp_dir() -> PathBuf {
|
fn temp_dir() -> PathBuf {
|
||||||
let nanos = SystemTime::now()
|
let nanos = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.expect("time should be after epoch")
|
.expect("time should be after epoch")
|
||||||
.as_nanos();
|
.as_nanos();
|
||||||
std::env::temp_dir().join(format!("runtime-session-control-{nanos}"))
|
let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||||
|
std::env::temp_dir().join(format!(
|
||||||
|
"runtime-session-control-{}-{nanos}-{counter}",
|
||||||
|
std::process::id()
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn persist_session(root: &Path, text: &str) -> Session {
|
fn persist_session(root: &Path, text: &str) -> Session {
|
||||||
@@ -981,6 +988,38 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_store_from_cwd_is_side_effect_free_until_save() {
|
||||||
|
// given
|
||||||
|
let base = temp_dir();
|
||||||
|
let workspace = base.join("fresh-workspace");
|
||||||
|
fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||||
|
|
||||||
|
// when
|
||||||
|
let store = SessionStore::from_cwd(&workspace).expect("store should build");
|
||||||
|
|
||||||
|
// then — resolving the store must not create .claw/session partitions.
|
||||||
|
assert!(
|
||||||
|
!workspace.join(".claw").exists(),
|
||||||
|
"session store construction must not create .claw side effects"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!store.sessions_dir().exists(),
|
||||||
|
"session partition should be created lazily on save"
|
||||||
|
);
|
||||||
|
|
||||||
|
let session = persist_session_via_store(&store, "first saved turn");
|
||||||
|
assert!(
|
||||||
|
store
|
||||||
|
.sessions_dir()
|
||||||
|
.join(format!("{}.jsonl", session.session_id))
|
||||||
|
.exists(),
|
||||||
|
"saving a managed session should create the lazy session partition"
|
||||||
|
);
|
||||||
|
|
||||||
|
fs::remove_dir_all(base).expect("temp dir should clean up");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn session_store_from_cwd_isolates_sessions_by_workspace() {
|
fn session_store_from_cwd_isolates_sessions_by_workspace() {
|
||||||
// given
|
// given
|
||||||
|
|||||||
@@ -382,9 +382,16 @@ fn main() {
|
|||||||
object.insert("available".to_string(), serde_json::json!(available));
|
object.insert("available".to_string(), serde_json::json!(available));
|
||||||
object.insert("tool_aliases".to_string(), aliases);
|
object.insert("tool_aliases".to_string(), aliases);
|
||||||
}
|
}
|
||||||
} else if kind == "missing_argument" && message.contains("--allowedTools") {
|
} else if kind == "missing_argument" {
|
||||||
if let Some(object) = error_json.as_object_mut() {
|
if let Some(object) = error_json.as_object_mut() {
|
||||||
object.insert("argument".to_string(), serde_json::json!("--allowedTools"));
|
if message.contains("--allowedTools") {
|
||||||
|
object.insert("argument".to_string(), serde_json::json!("--allowedTools"));
|
||||||
|
} else if message.contains("prompt or subcommand") {
|
||||||
|
object.insert(
|
||||||
|
"argument".to_string(),
|
||||||
|
serde_json::json!("prompt or subcommand"),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// #819/#820/#823: JSON mode error envelopes must go to stdout so machine
|
// #819/#820/#823: JSON mode error envelopes must go to stdout so machine
|
||||||
@@ -1751,11 +1758,15 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
|
|
||||||
if rest.is_empty() {
|
if rest.is_empty() {
|
||||||
let permission_mode = permission_mode_override.unwrap_or_else(default_permission_mode);
|
let permission_mode = permission_mode_override.unwrap_or_else(default_permission_mode);
|
||||||
|
let stdin_is_terminal = std::io::stdin().is_terminal();
|
||||||
|
if compact && stdin_is_terminal {
|
||||||
|
return Err(compact_missing_argument_error());
|
||||||
|
}
|
||||||
// When stdin is not a terminal (pipe/redirect) and no prompt is given on the
|
// When stdin is not a terminal (pipe/redirect) and no prompt is given on the
|
||||||
// command line, read stdin as the prompt and dispatch as a one-shot Prompt
|
// command line, read stdin as the prompt and dispatch as a one-shot Prompt
|
||||||
// rather than starting the interactive REPL (which would consume the pipe and
|
// rather than starting the interactive REPL (which would consume the pipe and
|
||||||
// print the startup banner, then exit without sending anything to the API).
|
// print the startup banner, then exit without sending anything to the API).
|
||||||
if !std::io::stdin().is_terminal() {
|
if !stdin_is_terminal {
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
let _ = std::io::Read::read_to_string(&mut std::io::stdin(), &mut buf);
|
let _ = std::io::Read::read_to_string(&mut std::io::stdin(), &mut buf);
|
||||||
let piped = buf.trim().to_string();
|
let piped = buf.trim().to_string();
|
||||||
@@ -1766,12 +1777,15 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
allowed_tools,
|
allowed_tools,
|
||||||
permission_mode,
|
permission_mode,
|
||||||
output_format,
|
output_format,
|
||||||
compact: false,
|
compact,
|
||||||
base_commit,
|
base_commit,
|
||||||
reasoning_effort,
|
reasoning_effort,
|
||||||
allow_broad_cwd,
|
allow_broad_cwd,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if compact {
|
||||||
|
return Err(compact_missing_argument_error());
|
||||||
|
}
|
||||||
// Non-TTY stdin with no piped content: refuse to start the interactive
|
// Non-TTY stdin with no piped content: refuse to start the interactive
|
||||||
// REPL (it would block forever waiting for input that will never arrive).
|
// REPL (it would block forever waiting for input that will never arrive).
|
||||||
// (#696: emit a typed error instead of hanging indefinitely)
|
// (#696: emit a typed error instead of hanging indefinitely)
|
||||||
@@ -2087,7 +2101,8 @@ Usage: claw prompt <text> or echo '<text>' | claw prompt".to_string());
|
|||||||
allow_broad_cwd,
|
allow_broad_cwd,
|
||||||
),
|
),
|
||||||
other => {
|
other => {
|
||||||
if !other.starts_with('-')
|
if !compact
|
||||||
|
&& !other.starts_with('-')
|
||||||
&& looks_like_subcommand_typo(other)
|
&& looks_like_subcommand_typo(other)
|
||||||
&& (rest.len() == 1
|
&& (rest.len() == 1
|
||||||
|| (output_format == CliOutputFormat::Json && model_flag_raw.is_none()))
|
|| (output_format == CliOutputFormat::Json && model_flag_raw.is_none()))
|
||||||
@@ -2850,6 +2865,11 @@ fn allowed_tools_missing_error() -> String {
|
|||||||
"missing_argument: --allowedTools requires a tool list before subcommands or flags.\nUsage: --allowedTools <tool-name>[,<tool-name>...] e.g. --allowedTools read,glob".to_string()
|
"missing_argument: --allowedTools requires a tool list before subcommands or flags.\nUsage: --allowedTools <tool-name>[,<tool-name>...] e.g. --allowedTools read,glob".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn compact_missing_argument_error() -> String {
|
||||||
|
"missing_argument: --compact requires prompt text, piped stdin, or a subcommand. argument: prompt or subcommand\nUsage: claw --compact <prompt> or echo '<prompt>' | claw --compact"
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
fn allowed_tool_aliases_json(registry: &GlobalToolRegistry) -> Value {
|
fn allowed_tool_aliases_json(registry: &GlobalToolRegistry) -> Value {
|
||||||
Value::Object(
|
Value::Object(
|
||||||
registry
|
registry
|
||||||
@@ -13558,6 +13578,21 @@ mod tests {
|
|||||||
allow_broad_cwd: false,
|
allow_broad_cwd: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_args(&["--compact".to_string(), "hello".to_string()])
|
||||||
|
.expect("compact single-word prompt should parse"),
|
||||||
|
CliAction::Prompt {
|
||||||
|
prompt: "hello".to_string(),
|
||||||
|
model: DEFAULT_MODEL.to_string(),
|
||||||
|
output_format: CliOutputFormat::Text,
|
||||||
|
allowed_tools: None,
|
||||||
|
permission_mode: PermissionMode::WorkspaceWrite,
|
||||||
|
compact: true,
|
||||||
|
base_commit: None,
|
||||||
|
reasoning_effort: None,
|
||||||
|
allow_broad_cwd: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -5424,6 +5424,54 @@ fn multi_word_unknown_subcommand_json_emits_command_not_found_826() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compact_flag_missing_argument_and_shorthand_prompt_contract_435() {
|
||||||
|
let root = unique_temp_dir("compact-flag-435");
|
||||||
|
let config_home = root.join("config-home");
|
||||||
|
let home = root.join("home");
|
||||||
|
std::fs::create_dir_all(&root).expect("create temp dir");
|
||||||
|
std::fs::create_dir_all(&config_home).expect("create config home");
|
||||||
|
std::fs::create_dir_all(&home).expect("create home");
|
||||||
|
let envs = [
|
||||||
|
(
|
||||||
|
"CLAW_CONFIG_HOME",
|
||||||
|
config_home.to_str().expect("config home utf8"),
|
||||||
|
),
|
||||||
|
("HOME", home.to_str().expect("home utf8")),
|
||||||
|
("ANTHROPIC_API_KEY", ""),
|
||||||
|
("ANTHROPIC_AUTH_TOKEN", ""),
|
||||||
|
("OPENAI_API_KEY", ""),
|
||||||
|
];
|
||||||
|
|
||||||
|
let missing = run_claw(&root, &["--output-format", "json", "--compact"], &envs);
|
||||||
|
assert_eq!(missing.status.code(), Some(1));
|
||||||
|
assert!(
|
||||||
|
missing.stderr.is_empty(),
|
||||||
|
"compact missing-argument JSON should keep stderr empty: {}",
|
||||||
|
String::from_utf8_lossy(&missing.stderr)
|
||||||
|
);
|
||||||
|
let missing_json = parse_json_stdout(&missing, "compact missing argument");
|
||||||
|
assert_eq!(missing_json["error_kind"], "missing_argument");
|
||||||
|
assert_eq!(missing_json["argument"], "prompt or subcommand");
|
||||||
|
|
||||||
|
let prompt = run_claw(
|
||||||
|
&root,
|
||||||
|
&["--output-format", "json", "--compact", "hello"],
|
||||||
|
&envs,
|
||||||
|
);
|
||||||
|
assert_eq!(prompt.status.code(), Some(1));
|
||||||
|
assert!(
|
||||||
|
prompt.stderr.is_empty(),
|
||||||
|
"compact prompt JSON should keep stderr empty: {}",
|
||||||
|
String::from_utf8_lossy(&prompt.stderr)
|
||||||
|
);
|
||||||
|
let prompt_json = parse_json_stdout(&prompt, "compact shorthand prompt");
|
||||||
|
assert_eq!(
|
||||||
|
prompt_json["error_kind"], "missing_credentials",
|
||||||
|
"--compact hello should stay on the prompt/provider path, not command_not_found: {prompt_json}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// #827: direct /unknown-slash-command must emit typed error_kind, not "unknown"
|
// #827: direct /unknown-slash-command must emit typed error_kind, not "unknown"
|
||||||
// Uses the direct-slash CLI path (no session load needed; reproducible on CI).
|
// Uses the direct-slash CLI path (no session load needed; reproducible on CI).
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -222,6 +222,73 @@ fn resume_latest_restores_the_most_recent_managed_session() {
|
|||||||
assert!(stdout.contains(newer_path.to_str().expect("utf8 path")));
|
assert!(stdout.contains(newer_path.to_str().expect("utf8 path")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resume_latest_missing_session_fails_without_creating_session_dirs_435() {
|
||||||
|
// given
|
||||||
|
let temp_dir = unique_temp_dir("resume-latest-missing-435");
|
||||||
|
let project_dir = temp_dir.join("project");
|
||||||
|
let config_home = temp_dir.join("config-home");
|
||||||
|
let home = temp_dir.join("home");
|
||||||
|
fs::create_dir_all(&project_dir).expect("project dir should exist");
|
||||||
|
fs::create_dir_all(&config_home).expect("config home should exist");
|
||||||
|
fs::create_dir_all(&home).expect("home should exist");
|
||||||
|
let envs = [
|
||||||
|
(
|
||||||
|
"CLAW_CONFIG_HOME",
|
||||||
|
config_home.to_str().expect("utf8 config home"),
|
||||||
|
),
|
||||||
|
("HOME", home.to_str().expect("utf8 home")),
|
||||||
|
("ANTHROPIC_API_KEY", ""),
|
||||||
|
("ANTHROPIC_AUTH_TOKEN", ""),
|
||||||
|
("OPENAI_API_KEY", ""),
|
||||||
|
];
|
||||||
|
|
||||||
|
// when — both text and JSON resume failures should be non-zero and read-only.
|
||||||
|
let text = run_claw_with_env(&project_dir, &["--resume", "latest"], &envs);
|
||||||
|
let json = run_claw_with_env(
|
||||||
|
&project_dir,
|
||||||
|
&["--output-format", "json", "--resume", "latest"],
|
||||||
|
&envs,
|
||||||
|
);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert_eq!(
|
||||||
|
text.status.code(),
|
||||||
|
Some(1),
|
||||||
|
"text resume failure must be non-zero"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
text.stdout.is_empty(),
|
||||||
|
"text resume failure should not claim success on stdout: {}",
|
||||||
|
String::from_utf8_lossy(&text.stdout)
|
||||||
|
);
|
||||||
|
let text_stderr = String::from_utf8_lossy(&text.stderr);
|
||||||
|
assert!(
|
||||||
|
text_stderr.contains("no managed sessions found"),
|
||||||
|
"text failure should explain missing sessions: {text_stderr}"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
json.status.code(),
|
||||||
|
Some(1),
|
||||||
|
"JSON resume failure must be non-zero"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
json.stderr.is_empty(),
|
||||||
|
"JSON resume failure should keep stderr empty: {}",
|
||||||
|
String::from_utf8_lossy(&json.stderr)
|
||||||
|
);
|
||||||
|
let parsed: Value = serde_json::from_slice(&json.stdout)
|
||||||
|
.expect("JSON resume failure should emit JSON to stdout");
|
||||||
|
assert_eq!(parsed["status"], "error");
|
||||||
|
assert_eq!(parsed["action"], "restore");
|
||||||
|
assert_eq!(parsed["error_kind"], "no_managed_sessions");
|
||||||
|
assert!(
|
||||||
|
!project_dir.join(".claw").exists(),
|
||||||
|
"failed resume must not create .claw/session directories"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resumed_status_command_emits_structured_json_when_requested() {
|
fn resumed_status_command_emits_structured_json_when_requested() {
|
||||||
// given
|
// given
|
||||||
|
|||||||
Reference in New Issue
Block a user