diff --git a/ROADMAP.md b/ROADMAP.md index ec941bfa..c49f06d6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6383,7 +6383,7 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed) 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//`. `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. **DONE — `claw init` scaffolds safe project settings and reports partial/deferred artifacts** — fixed 2026-06-04 in `fix: scaffold safe init settings`. The starter `.claw.json` and new `.claw/settings.json` template now both use `permissions.defaultMode:"acceptEdits"` instead of unsafe `dontAsk`. Fresh init materializes `.claw/settings.json`, keeps `.claw/sessions/` deferred until the first successful session save, and emits per-artifact entries for `.claw/`, `.claw/settings.json`, `.claw/sessions/`, `.claw.json`, `.gitignore`, and `CLAUDE.md`. When `.claw/` already exists but its settings template is missing, init creates `.claw/settings.json` without overwriting existing files and reports `.claw/` as `partial` rather than `skipped`; idempotent reruns keep existing artifacts skipped and session storage deferred. JSON init output now includes `partial[]` and `deferred[]` alongside `created[]`, `updated[]`, and `skipped[]`, and init help/USAGE document the artifact statuses. Regression coverage: `initialize_repo_creates_expected_files_and_gitignore_entries`, `initialize_repo_is_idempotent_and_preserves_existing_files`, `artifacts_with_status_partitions_fresh_and_idempotent_runs`, and `init_json_envelope_has_hint_and_already_initialized_783`. 437. **`version --output-format json` omits build provenance fields — no `is_dirty`, `branch`, `commit_date`, `commit_timestamp`, `rustc_version`; `git_sha` is truncated to 7 chars instead of full 40-char hash; sibling: `executable_path` leaks the build host's path (`/tmp/claw-dog-0530/...`) into runtime output** — dogfooded 2026-05-11 by Jobdori on `8cf628a5` in response to Clawhip pinpoint nudge at `1503320791582900344`. Reproduction: `claw version --output-format json` returns `{"build_date":"2026-05-11","executable_path":"/tmp/claw-dog-0530/rust/target/release/claw","git_sha":"b98b9a7","kind":"version","message":"Claw Code\n Version 0.1.0\n Git SHA b98b9a7\n Target aarch64-apple-darwin\n Build date 2026-05-11","target":"aarch64-apple-darwin","version":"0.1.0"}`. Critical provenance fields missing: (a) **`is_dirty`** — was the working tree clean at build time? Automation that pins on build provenance cannot tell if the binary was built from a clean commit or includes uncommitted changes; (b) **`branch`** — was this built from `main`, `dev/rust`, a release tag, or a feature branch? The `git_sha` alone doesn't reveal the integration point; (c) **`commit_date` / `commit_timestamp`** — only `build_date` (when the binary was compiled) is exposed; the commit itself might be days/weeks older if the build happened later. Reproducibility audits need both; (d) **`rustc_version`** — what Rust compiler version produced this binary? Critical for security advisories (e.g., known regressions in specific rustc versions); (e) **`git_sha` truncated to 7 chars** ("b98b9a7" instead of full "b98b9a71..."): 7-char shas have known collision rates in large repos and prevent unambiguous git rev-parse round-trip. **Sibling: `executable_path` leaks build-host path.** The `executable_path` field returns `/tmp/claw-dog-0530/rust/target/release/claw` — the directory where the binary was compiled, embedded into the binary metadata. For a binary copied/installed/symlinked to a different location, this field still reports the build path, not the actual invocation path. Either the field should reflect the runtime path via `std::env::current_exe()` at runtime (not compile-time), or it should be dropped to avoid leaking compile-host filesystem layout. **Sibling: prose `message` field duplicates structured data.** The `message` field still contains the entire text-mode prose version block (`"Claw Code\n Version 0.1.0\n Git SHA b98b9a7\n..."`) — every field present as structured JSON (`version`, `git_sha`, `target`, `build_date`) is also embedded in the prose. Same issue as #391 (`version json includes prose message field`) which was closed as "fixed" — the prose remains. **Required fix shape:** (a) add `is_dirty:bool`, `branch:string|null`, `commit_date:string` (ISO-8601), `commit_timestamp:int` (Unix epoch), `rustc_version:string` to the JSON envelope; (b) preserve full 40-char `git_sha` and add `git_sha_short:string` as a derived field if 7-char form is needed for UX; (c) `executable_path` should be `std::env::current_exe()` at runtime, not the compile-time path; (d) drop the prose `message` field from JSON or rename it `human_readable:string` and make it explicitly secondary to the structured fields; (e) re-verify #391 closure — the prose `message` is still present, the fix didn't fully land. **Why this matters:** version surface is the canonical provenance probe for security audits, build reproducibility, and bug-report metadata. Missing `is_dirty` means automated triage cannot distinguish "issue against a clean main commit" from "issue against a developer's uncommitted hack". Truncated `git_sha` blocks unambiguous git lookup. Leaked `executable_path` exposes build-host layout. Cross-references #391 (version prose duplication — apparently not fully fixed), #334 (version json omits build_date — fixed, but partial scope), #100 (commit identity audit). Source: Jobdori live dogfood, `8cf628a5`, 2026-05-11. diff --git a/USAGE.md b/USAGE.md index a760cef9..859c1de7 100644 --- a/USAGE.md +++ b/USAGE.md @@ -54,23 +54,23 @@ cd rust ### Initialize a repository -Set up a new repository with `.claw` config, `.claw.json`, `.gitignore` entries, and a `CLAUDE.md` guidance file: +Set up a new repository with `.claw/settings.json`, `.claw.json`, `.gitignore` entries, and a `CLAUDE.md` guidance file: ```bash cd /path/to/your/repo ./target/debug/claw init ``` -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". +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", reports `.claw/` as "partial" when missing sub-files are materialized, and keeps `.claw/sessions/` deferred until the first successful session save. JSON mode for scripting: ```bash ./target/debug/claw init --output-format json ``` -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. +Returns structured output with `project_path`, `created[]`, `updated[]`, `partial[]`, `deferred[]`, and `skipped[]` arrays (one per artifact status), and `artifacts[]` carrying each file's `name` and machine-stable `status` tag. The legacy `message` field preserves backward compatibility. -**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). +**Why structured fields matter:** Claws can detect per-artifact state (`created`, `updated`, `partial`, `deferred`, or `skipped`) without substring-matching human prose. Use the status arrays for conditional follow-up logic (e.g., only commit if files were actually created, not just updated). ### Interactive REPL diff --git a/rust/crates/rusty-claude-cli/src/init.rs b/rust/crates/rusty-claude-cli/src/init.rs index eb012dbd..ac192339 100644 --- a/rust/crates/rusty-claude-cli/src/init.rs +++ b/rust/crates/rusty-claude-cli/src/init.rs @@ -4,7 +4,14 @@ use std::path::{Path, PathBuf}; const STARTER_CLAW_JSON: &str = concat!( "{\n", " \"permissions\": {\n", - " \"defaultMode\": \"dontAsk\"\n", + " \"defaultMode\": \"acceptEdits\"\n", + " }\n", + "}\n", +); +const STARTER_SETTINGS_JSON: &str = concat!( + "{\n", + " \"permissions\": {\n", + " \"defaultMode\": \"acceptEdits\"\n", " }\n", "}\n", ); @@ -15,6 +22,8 @@ const GITIGNORE_ENTRIES: [&str; 3] = [".claw/settings.local.json", ".claw/sessio pub(crate) enum InitStatus { Created, Updated, + Partial, + Deferred, Skipped, } @@ -24,6 +33,8 @@ impl InitStatus { match self { Self::Created => "created", Self::Updated => "updated", + Self::Partial => "partial (created missing sub-files)", + Self::Deferred => "deferred (created on first session save)", Self::Skipped => "skipped (already exists)", } } @@ -36,6 +47,8 @@ impl InitStatus { match self { Self::Created => "created", Self::Updated => "updated", + Self::Partial => "partial", + Self::Deferred => "deferred", Self::Skipped => "skipped", } } @@ -123,9 +136,30 @@ pub(crate) fn initialize_repo(cwd: &Path) -> Result String { .to_string(), LocalHelpTopic::Init => "Init Usage claw init [--output-format ] - Purpose create .claw/, .claw.json, .gitignore, and CLAUDE.md in the current project - Output list of created vs. skipped files (idempotent: safe to re-run) + Purpose create .claw/settings.json, .claw.json, .gitignore, and CLAUDE.md in the current project + Output per-artifact created/updated/partial/deferred/skipped status (idempotent: safe to re-run) Formats text (default), json Related claw status · claw doctor" .to_string(), @@ -9774,10 +9774,12 @@ fn init_json_value(report: &crate::init::InitReport, message: &str) -> serde_jso // Derive top-level status: "ok" when all artifacts succeeded (created or // skipped = idempotent); no failure path exists today so always "ok". let status = "ok"; - // #783: already_initialized lets orchestrators detect the idempotent case - // without checking created.len() == 0; hint gives a stable next-action pointer. + // #783/#436: already_initialized lets orchestrators detect the idempotent + // case without checking every status bucket; deferred session storage does + // not make the workspace uninitialized because it is created on first save. let already_initialized = report.artifacts_with_status(InitStatus::Created).is_empty() - && report.artifacts_with_status(InitStatus::Updated).is_empty(); + && report.artifacts_with_status(InitStatus::Updated).is_empty() + && report.artifacts_with_status(InitStatus::Partial).is_empty(); let hint = if already_initialized { "Workspace already initialised. Run `claw doctor` to verify health, or edit CLAUDE.md to customise guidance." } else { @@ -9792,6 +9794,8 @@ fn init_json_value(report: &crate::init::InitReport, message: &str) -> serde_jso "created": report.artifacts_with_status(InitStatus::Created), "updated": report.artifacts_with_status(InitStatus::Updated), "skipped": report.artifacts_with_status(InitStatus::Skipped), + "partial": report.artifacts_with_status(InitStatus::Partial), + "deferred": report.artifacts_with_status(InitStatus::Deferred), "artifacts": report.artifact_json_entries(), "hint": hint, "next_step": crate::init::InitReport::NEXT_STEP, diff --git a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs index c891c52f..1ac7c7ef 100644 --- a/rust/crates/rusty-claude-cli/tests/output_format_contract.rs +++ b/rust/crates/rusty-claude-cli/tests/output_format_contract.rs @@ -5,7 +5,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; use runtime::Session; -use serde_json::Value; +use serde_json::{json, Value}; static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0); @@ -3928,6 +3928,41 @@ fn init_json_envelope_has_hint_and_already_initialized_783() { hint.contains("CLAUDE.md") || hint.contains("doctor"), "fresh-init hint should mention CLAUDE.md or doctor, got: {hint:?}" ); + assert_eq!( + parsed["created"], + json!([ + ".claw/", + ".claw/settings.json", + ".claw.json", + ".gitignore", + "CLAUDE.md" + ]), + "fresh init should materialize .claw/settings.json and safe .claw.json" + ); + assert_eq!( + parsed["deferred"], + json!([".claw/sessions/"]), + "session storage should be reported as deferred until first save" + ); + assert_eq!(parsed["partial"], json!([])); + let claw_json = fs::read_to_string(root.join(".claw.json")).expect("read .claw.json"); + assert!( + claw_json.contains("\"defaultMode\": \"acceptEdits\""), + "init must not scaffold dontAsk in .claw.json: {claw_json}" + ); + assert!( + !claw_json.contains("dontAsk"), + "init must not scaffold unsafe dontAsk permission mode: {claw_json}" + ); + let settings_json = root.join(".claw").join("settings.json"); + assert!( + settings_json.is_file(), + "init should template .claw/settings.json" + ); + assert!( + !root.join(".claw").join("sessions").exists(), + "sessions directory should remain deferred until first save" + ); // Idempotent re-init — already_initialized should be true let output2 = run_claw(&root, &["--output-format", "json", "init"], &[]); @@ -3954,6 +3989,43 @@ fn init_json_envelope_has_hint_and_already_initialized_783() { hint2.contains("already") || hint2.contains("doctor"), "re-init hint should acknowledge workspace exists, got: {hint2:?}" ); + + let existing_claw_root = unique_temp_dir("init-existing-claw-436"); + fs::create_dir_all(existing_claw_root.join(".claw")).expect("existing .claw dir"); + let partial_output = run_claw( + &existing_claw_root, + &["--output-format", "json", "init"], + &[], + ); + assert!( + partial_output.status.success(), + "init with existing .claw should succeed" + ); + let partial_stdout = String::from_utf8_lossy(&partial_output.stdout); + let partial: serde_json::Value = + serde_json::from_str(partial_stdout.trim()).expect("partial init should emit valid JSON"); + assert_eq!( + partial["partial"], + json!([".claw/"]), + "existing .claw with newly-created settings should report partial .claw/" + ); + assert_eq!( + partial["created"], + json!([ + ".claw/settings.json", + ".claw.json", + ".gitignore", + "CLAUDE.md" + ]), + "init should still create missing sub-files when .claw already exists" + ); + assert!( + existing_claw_root + .join(".claw") + .join("settings.json") + .is_file(), + "existing .claw must receive missing settings template" + ); } #[test]