mirror of
https://github.com/instructkr/claw-code.git
synced 2026-06-14 07:24:41 -04:00
Compare commits
14 Commits
a7b1fef176
...
4f83a81cf6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f83a81cf6 | ||
|
|
1d83e67802 | ||
|
|
763437a0b3 | ||
|
|
491386f0a5 | ||
|
|
5c85e5ad12 | ||
|
|
b825713db3 | ||
|
|
06d1b8ac87 | ||
|
|
4f84607ad6 | ||
|
|
8eb93e906c | ||
|
|
264fdc214e | ||
|
|
a4921cb262 | ||
|
|
d40929cada | ||
|
|
2d5f836988 | ||
|
|
4e199ec52a |
30
ROADMAP.md
30
ROADMAP.md
@@ -344,7 +344,7 @@ Priority order: P0 = blocks CI/green state, P1 = blocks integration wiring, P2 =
|
||||
- Forks inherit the parent's workspace root by default; an explicit re-bind is required to move a session to a new worktree, and that re-bind is itself recorded as a structured event so the orchestrator can audit cross-worktree handoffs.
|
||||
- Surface a `branch.workspace_mismatch` lane event so clawhip stops counting wrong-CWD writes as lane completions.
|
||||
|
||||
**Status.** Done. Managed-session creation/list/latest/load/fork now route through the per-worktree `SessionStore` namespace in runtime + CLI paths, session loads/resumes reject wrong-workspace access with typed `SessionControlError::WorkspaceMismatch` details, `branch.workspace_mismatch` / `workspace_mismatch` are available on the lane-event surface, and same-workspace legacy flat sessions remain readable while mismatched legacy access is blocked. Focused runtime/CLI/tools coverage for the isolation path is green, and `cargo test --workspace --exclude compat-harness` passes. `cargo clippy --workspace --all-targets -- -D warnings` still fails on pre-existing lints in unchanged `rust/crates/rusty-claude-cli/build.rs`, so that lint cleanup remains outside this roadmap item.
|
||||
**Status.** Done. Managed-session creation/list/latest/load/fork now route through the per-worktree `SessionStore` namespace in runtime + CLI paths, session loads/resumes reject wrong-workspace access with typed `SessionControlError::WorkspaceMismatch` details, `branch.workspace_mismatch` / `workspace_mismatch` are available on the lane-event surface, and same-workspace legacy flat sessions remain readable while mismatched legacy access is blocked. Focused runtime/CLI/tools coverage for the isolation path is green, and the current full workspace gates now pass: `cargo fmt --all --check`, `cargo clippy --workspace --all-targets -- -D warnings`, and `cargo test --workspace`.
|
||||
|
||||
## Deployment Architecture Gap (filed from dogfood 2026-04-08)
|
||||
|
||||
@@ -422,6 +422,8 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes
|
||||
|
||||
30. **DashScope model routing in ProviderClient dispatch uses wrong config** — **done at `adcea6b` on 2026-04-08**. `ProviderClient::from_model_with_anthropic_auth` dispatched all `ProviderKind::OpenAi` matches to `OpenAiCompatConfig::openai()` (reads `OPENAI_API_KEY`, points at `api.openai.com`). But DashScope models (`qwen-plus`, `qwen/qwen-max`) return `ProviderKind::OpenAi` because DashScope speaks the OpenAI wire format — they need `OpenAiCompatConfig::dashscope()` (reads `DASHSCOPE_API_KEY`, points at `dashscope.aliyuncs.com/compatible-mode/v1`). Fix: consult `metadata_for_model` in the `OpenAi` dispatch arm and pick `dashscope()` vs `openai()` based on `metadata.auth_env`. Adds regression test + `pub base_url()` accessor. 2 files, +94/−3. Authored by droid (Kimi K2.5 Turbo) via acpx, cleaned up by Jobdori.
|
||||
|
||||
31. **`code-on-disk → verified commit lands` depends on undocumented executor quirks** — **verified external/non-actionable on 2026-04-12:** current `main` has no repo-local implementation surface for `acpx`, `use-droid`, `run-acpx`, `commit-wrapper`, or the cited `spawn ENOENT` behavior outside `ROADMAP.md`; those failures live in the external droid/acpx executor-orchestrator path, not claw-code source in this repository. Treat this as an external tracking note instead of an in-repo Immediate Backlog item. **Original filing below.**
|
||||
|
||||
31. **`code-on-disk → verified commit lands` depends on undocumented executor quirks** — dogfooded 2026-04-08 during live fix session. Three hidden contracts tripped the "last mile" path when using droid via acpx in the claw-code workspace: **(a) hidden CWD contract** — droid's `terminal/create` rejects `cd /path && cargo build` compound commands with `spawn ENOENT`; callers must pass `--cwd` or split commands; **(b) hidden commit-message transport limit** — embedding a multi-line commit message in a single shell invocation hits `ENAMETOOLONG`; workaround is `git commit -F <file>` but the caller must know to write the file first; **(c) hidden workspace lint/edition contract** — `unsafe_code = "forbid"` workspace-wide with Rust 2021 edition makes `unsafe {}` wrappers incorrect for `set_var`/`remove_var`, but droid generates Rust 2024-style unsafe blocks without inspecting the workspace Cargo.toml or clippy config. Each of these required the orchestrator to learn the constraint by failing, then switching strategies. **Acceptance bar:** a fresh agent should be able to verify/commit/push a correct diff in this workspace without needing to know executor-specific shell trivia ahead of time. **Fix shape:** (1) `run-acpx.sh`-style wrapper that normalizes the commit idiom (always writes to temp file, sets `--cwd`, splits compound commands); (2) inject workspace constraints into the droid/acpx task preamble (edition, lint gates, known shell executor quirks) so the model doesn't have to discover them from failures; (3) or upstream a fix to the executor itself so `cd /path && cmd` chains work correctly.
|
||||
|
||||
32. **OpenAI-compatible provider/model-id passthrough is not fully literal** — **verified no-bug on 2026-04-09**: `resolve_model_alias()` only matches bare shorthand aliases (`opus`/`sonnet`/`haiku`) and passes everything else through unchanged, so `openai/gpt-4` reaches the dispatch layer unmodified. `strip_routing_prefix()` at `openai_compat.rs:732` then strips only recognised routing prefixes (`openai`, `xai`, `grok`, `qwen`) so the wire model is the bare backend id. No fix needed. **Original filing below.**
|
||||
@@ -431,33 +433,35 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes
|
||||
33. **OpenAI `/responses` endpoint rejects claw's tool schema: `object schema missing properties` / `invalid_function_parameters`** — **done at `e7e0fd2` on 2026-04-09**. Added `normalize_object_schema()` in `openai_compat.rs` which recursively walks JSON Schema trees and injects `"properties": {}` and `"additionalProperties": false` on every object-type node (without overwriting existing values). Called from `openai_tool_definition()` so both `/chat/completions` and `/responses` receive strict-validator-safe schemas. 3 unit tests added. All api tests pass. **Original filing below.**
|
||||
33. **OpenAI `/responses` endpoint rejects claw's tool schema: `object schema missing properties` / `invalid_function_parameters`** — dogfooded 2026-04-08 via live user in #claw-code. Repro: startup succeeds, provider routing succeeds (`Connected: gpt-5.4 via openai`), but request fails when claw sends tool/function schema to a `/responses`-compatible OpenAI backend. Backend rejects `StructuredOutput` with `object schema missing properties` and `invalid_function_parameters`. This is distinct from the `#32` model-id passthrough issue — routing and transport work correctly. The failure is at the schema validation layer: claw's tool schema is acceptable for `/chat/completions` but not strict enough for `/responses` endpoint validation. **Sharp next check:** emit what schema claw sends for `StructuredOutput` tool functions, compare against OpenAI `/responses` spec for strict JSON schema validation (required `properties` object, `additionalProperties: false`, etc). Likely fix: add missing `properties: {}` on object types, ensure `additionalProperties: false` is present on all object schemas in the function tool JSON. **Source:** live user in #claw-code 2026-04-08 with `gpt-5.4` on OpenAI-compat backend.
|
||||
|
||||
34. **`reasoning_effort` / `budget_tokens` not surfaced on OpenAI-compat path** — **done (verified 2026-04-11):** current `main` already carries the Rust-side OpenAI-compat parity fix. `MessageRequest` now includes `reasoning_effort: Option<String>` in `rust/crates/api/src/types.rs`, `build_chat_completion_request()` emits `"reasoning_effort"` in `rust/crates/api/src/providers/openai_compat.rs`, and the CLI threads `--reasoning-effort low|medium|high` through to the API client in `rust/crates/rusty-claude-cli/src/main.rs`. The OpenAI-side parity target here is `reasoning_effort`; Anthropic-only `budget_tokens` remains handled on the Anthropic path. Re-verified on current `origin/main` / HEAD `2d5f836`: `cargo test -p api reasoning_effort -- --nocapture` passes (2 passed), and `cargo test -p rusty-claude-cli reasoning_effort -- --nocapture` passes (2 passed). Historical proof: `e4c3871` added the request field + OpenAI-compatible payload serialization, `ca8950c2` wired the CLI end-to-end, and `f741a425` added CLI validation coverage. **Original filing below.**
|
||||
|
||||
34. **`reasoning_effort` / `budget_tokens` not surfaced on OpenAI-compat path** — dogfooded 2026-04-09. Users asking for "reasoning effort parity with opencode" are hitting a structural gap: `MessageRequest` in `rust/crates/api/src/types.rs` has no `reasoning_effort` or `budget_tokens` field, and `build_chat_completion_request` in `openai_compat.rs` does not inject either into the request body. This means passing `--thinking` or equivalent to an OpenAI-compat reasoning model (e.g. `o4-mini`, `deepseek-r1`, any model that accepts `reasoning_effort`) silently drops the field — the model runs without the requested effort level, and the user gets no warning. **Contrast with Anthropic path:** `anthropic.rs` already maps `thinking` config into `anthropic.thinking.budget_tokens` in the request body. **Fix shape:** (a) Add optional `reasoning_effort: Option<String>` field to `MessageRequest`; (b) In `build_chat_completion_request`, if `reasoning_effort` is `Some`, emit `"reasoning_effort": value` in the JSON body; (c) In the CLI, wire `--thinking low/medium/high` or equivalent to populate the field when the resolved provider is `ProviderKind::OpenAi`; (d) Add unit test asserting `reasoning_effort` appears in the request body when set. **Source:** live user questions in #claw-code 2026-04-08/09 (dan_theman369 asking for "same flow as opencode for reasoning effort"; gaebal-gajae confirmed gap at `1491453913100976339`). Companion gap to #33 on the OpenAI-compat path.
|
||||
|
||||
35. **OpenAI gpt-5.x requires max_completion_tokens not max_tokens** -- dogfooded 2026-04-09. rklehm repro: gpt-5.2 via OpenAI-compat, startup OK, routing OK, but requests fail because claw emits max_tokens where gpt-5* requires max_completion_tokens. Fix: emit max_completion_tokens on OpenAI-compat path (backward-compatible). Add unit test. Source: rklehm in #claw-code 2026-04-09.
|
||||
35. **OpenAI gpt-5.x requires max_completion_tokens not max_tokens** — **done (verified 2026-04-11):** current `main` already carries the Rust-side OpenAI-compat fix. `build_chat_completion_request()` in `rust/crates/api/src/providers/openai_compat.rs` switches the emitted key to `"max_completion_tokens"` whenever the wire model starts with `gpt-5`, while older models still use `"max_tokens"`. Regression test `gpt5_uses_max_completion_tokens_not_max_tokens()` proves `gpt-5.2` emits `max_completion_tokens` and omits `max_tokens`. Re-verified against current `origin/main` `d40929ca`: `cargo test -p api gpt5_uses_max_completion_tokens_not_max_tokens -- --nocapture` passes. Historical proof: `eb044f0a` landed the request-field switch plus regression test on 2026-04-09. Source: rklehm in #claw-code 2026-04-09.
|
||||
|
||||
36. **Custom/project skill invocation disconnected from skill discovery** -- dogfooded 2026-04-09. /skills lists custom skills (e.g. caveman) but bare skill-name invocation does not dispatch them; falls through to plain model prompt. Fix: audit classify_skills_slash_command, ensure any skill listed by /skills has a deterministic invocation path, or document the correct syntax. Source: gaebal-gajae dogfood 2026-04-09.
|
||||
36. **Custom/project skill invocation disconnected from skill discovery** — **done (verified 2026-04-11):** current `main` already routes bare-word skill input in the REPL through `resolve_skill_invocation()` instead of forwarding it to the model. `rust/crates/rusty-claude-cli/src/main.rs` now treats a leading bare token that matches a known skill name as `/skills <input>`, while `rust/crates/commands/src/lib.rs` validates the skill against discovered project/user skill roots and reports available-skill guidance on miss. Fresh regression coverage proves the known-skill dispatch path and the unknown/non-skill bypass. Historical proof: `8d0308ee` landed the REPL dispatch fix. Source: gaebal-gajae dogfood 2026-04-09.
|
||||
|
||||
37. **Claude subscription login path should be removed, not deprecated** -- dogfooded 2026-04-09. Official auth should be API key only (`ANTHROPIC_API_KEY`) or OAuth bearer token via `ANTHROPIC_AUTH_TOKEN`; the local `claw login` / `claw logout` subscription-style flow created legal/billing ambiguity and a misleading saved-OAuth fallback. **Done (verified 2026-04-11):** removed the direct `claw login` / `claw logout` CLI surface, removed `/login` and `/logout` from shared slash-command discovery, changed both CLI and provider startup auth resolution to ignore saved OAuth credentials, and updated auth diagnostics to point only at `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN`. Verification: targeted `commands`, `api`, and `rusty-claude-cli` tests for removed login/logout guidance and ignored saved OAuth all pass, and `cargo check -p api -p commands -p rusty-claude-cli` passes. Source: gaebal-gajae policy decision 2026-04-09.
|
||||
|
||||
38. **Dead-session opacity: bot cannot self-detect compaction vs broken tool surface** -- dogfooded 2026-04-09. Jobdori session spent ~15h declaring itself "dead" in-channel while tools were actually returning correct results within each turn. Root cause: context compaction causes tool outputs to be summarised away between turns, making the bot interpret absence-of-remembered-output as tool failure. This is a distinct failure mode from ROADMAP #31 (executor quirks): the session is alive and tools are functional, but the agent cannot tell the difference between "my last tool call produced no output" (compaction) and "the tool is broken". **Done (verified 2026-04-11):** `ConversationRuntime::run_turn()` now runs a post-compaction session-health probe through `glob_search`, fails fast with a targeted recovery error if the tool surface is broken, and skips the probe for a freshly compacted empty session. Fresh regression coverage proves both the failure gate and the empty-session bypass. Source: Jobdori self-dogfood 2026-04-09; observed in #clawcode-building-in-public across multiple Clawhip nudge cycles.
|
||||
|
||||
39. **Several slash commands are registered but not implemented: /branch, /rewind, /ide, /tag, /output-style, /add-dir** -- dogfooded 2026-04-09. These commands appear in the REPL completions surface but silently print 'Command registered but not yet implemented.' and return false. Users (mezz2301 in #claw-code) hit this as 'many features are not supported in this version now'. Fix shape: either (a) implement the missing commands, or (b) remove them from completions/help output until they are ready, so the discovery surface matches what actually works. Source: mezz2301 in #claw-code 2026-04-09; pinpointed in main.rs:3728.
|
||||
39. **Several slash commands were registered but not implemented: /branch, /rewind, /ide, /tag, /output-style, /add-dir** — **done (verified 2026-04-12):** current `main` already hides those stub commands from the user-facing discovery surfaces that mattered for the original report. Shared help rendering excludes them via `render_slash_command_help_filtered(...)`, and REPL completions exclude them via `STUB_COMMANDS`. Fresh proof: `cargo test -p commands renders_help_from_shared_specs -- --nocapture`, `cargo test -p rusty-claude-cli shared_help_uses_resume_annotation_copy -- --nocapture`, and `cargo test -p rusty-claude-cli stub_commands_absent_from_repl_completions -- --nocapture` all pass on current `origin/main`. Source: mezz2301 in #claw-code 2026-04-09; pinpointed in main.rs:3728.
|
||||
|
||||
40. **Surface broken installed plugins before they become support ghosts** — community-support lane. Clawhip commit `ff6d3b7` on worktree `claw-code-community-support-plugin-list-load-failures` / branch `community-support/plugin-list-load-failures`. When an installed plugin has a broken manifest (missing hook scripts, parse errors, bad json), the plugin silently fails to load and the user sees nothing — no warning, no list entry, no hint. Related to ROADMAP #27 (host plugin path leaking into tests) but at the user-facing surface: the test gap and the UX gap are siblings of the same root. Landing on `main` will close the silent-ghost class of support issues where users report "my plugin does nothing" with no error to share. Track until merged to `main`.
|
||||
40. **Surface broken installed plugins before they become support ghosts** — community-support lane. Clawhip commit `ff6d3b7` on worktree `claw-code-community-support-plugin-list-load-failures` / branch `community-support/plugin-list-load-failures`. When an installed plugin has a broken manifest (missing hook scripts, parse errors, bad json), the plugin silently fails to load and the user sees nothing — no warning, no list entry, no hint. Related to ROADMAP #27 (host plugin path leaking into tests) but at the user-facing surface: the test gap and the UX gap are siblings of the same root. **Done (verified 2026-04-11):** `PluginManager::plugin_registry_report()` and `installed_plugin_registry_report()` now preserve valid plugins while collecting `PluginLoadFailure`s, and the command-layer renderer emits a `Warnings:` block for broken plugins instead of silently hiding them. Fresh proof: `cargo test -p plugins plugin_registry_report_collects_load_failures_without_dropping_valid_plugins -- --nocapture`, `cargo test -p plugins installed_plugin_registry_report_collects_load_failures_from_install_root -- --nocapture`, and a new `commands` regression covering `render_plugins_report_with_failures()` all pass on current main.
|
||||
|
||||
40. **Surface broken installed plugins before they become support ghosts** — community-support lane. Clawhip commit `ff6d3b7` on worktree `claw-code-community-support-plugin-list-load-failures` / branch `community-support/plugin-list-load-failures`. When an installed plugin has a broken manifest (missing hook scripts, parse errors, bad json), the plugin silently fails to load and the user sees nothing — no warning, no list entry, no hint. Related to ROADMAP #27 (host plugin path leaking into tests) but at the user-facing surface: the test gap and the UX gap are siblings of the same same root. Landing on `main` will close the silent-ghost class of support issues where users report "my plugin does nothing" with no error to share. Track until merged to `main`.
|
||||
|
||||
41. **Stop ambient plugin state from skewing CLI regression checks** — community-support lane. Clawhip commit `7d493a7` on worktree `claw-code-community-support-plugin-test-sealing` / branch `community-support/plugin-test-sealing`. Companion to #40: the test sealing gap is the CI/developer side of the same root — host `~/.claude/plugins/installed/` bleeds into CLI test runs, making regression checks non-deterministic on any machine with a non-pristine plugin install. Closely related to ROADMAP #27 (dev/rust `cargo test` reads host plugin state). Track until merged to `main`.
|
||||
41. **Stop ambient plugin state from skewing CLI regression checks** — community-support lane. Clawhip commit `7d493a7` on worktree `claw-code-community-support-plugin-test-sealing` / branch `community-support/plugin-test-sealing`. Companion to #40: the test sealing gap is the CI/developer side of the same root — host `~/.claude/plugins/installed/` bleeds into CLI test runs, making regression checks non-deterministic on any machine with a non-pristine plugin install. Closely related to ROADMAP #27 (dev/rust `cargo test` reads host plugin state). **Done (verified 2026-04-11):** the plugins crate now carries dedicated test-isolation helpers in `rust/crates/plugins/src/test_isolation.rs`, and regression `claw_config_home_isolation_prevents_host_plugin_leakage()` proves `CLAW_CONFIG_HOME` isolation prevents host plugin state from leaking into installed-plugin discovery during tests.
|
||||
|
||||
42. **`--output-format json` errors emitted as prose, not JSON** — dogfooded 2026-04-09. When `claw --output-format json prompt` hits an API error, the error was printed as plain text (`error: api returned 401 ...`) to stderr instead of a JSON object. Any tool or CI step parsing claw's JSON output gets nothing parseable on failure — the error is invisible to the consumer. **Fix (`a...`):** detect `--output-format json` in `main()` at process exit and emit `{"type":"error","error":"<message>"}` to stderr instead of the prose format. Non-JSON path unchanged. **Done** in this nudge cycle.
|
||||
|
||||
43. **Hook ingress opacity: typed hook-health/delivery report missing** — **verified likely external tracking on 2026-04-12:** repo-local searches for `/hooks/health`, `/hooks/status`, and hook-ingress route code found no implementation surface outside `ROADMAP.md`, and the prior state-surface note below already records that the HTTP server is not owned by claw-code. Treat this as likely upstream/server-surface tracking rather than an immediate claw-code task. **Original filing below.**
|
||||
43. **Hook ingress opacity: typed hook-health/delivery report missing** — dogfooded 2026-04-09 while wiring the agentika timer→hook→session bridge. Debugging hook delivery required manual HTTP probing and inferring state from raw status codes (404 = no route, 405 = route exists, 400 = body missing required field). No typed endpoint exists to report: route present/absent, accepted methods, mapping matched/not matched, target session resolved/not resolved, last delivery failure class. Fix shape: add `GET /hooks/health` (or `/hooks/status`) returning a structured JSON diagnostic — no auth exposure, just routing/matching/session state. Source: gaebal-gajae dogfood 2026-04-09.
|
||||
|
||||
44. **Broad-CWD guardrail is warning-only; needs policy-level enforcement** — dogfooded 2026-04-09. `5f6f453` added a stderr warning when claw starts from `$HOME` or filesystem root (live user kapcomunica scanned their whole machine). Warning is a mitigation, not a guardrail: the agent still proceeds with unbounded scope. Follow-up fix shape: (a) add `--allow-broad-cwd` flag to suppress the warning explicitly (for legitimate home-dir use cases); (b) in default interactive mode, prompt "You are running from your home directory — continue? [y/N]" and exit unless confirmed; (c) in `--output-format json` or piped mode, treat broad-CWD as a hard error (exit 1) with `{"type":"error","error":"broad CWD: running from home directory requires --allow-broad-cwd"}`. Source: kapcomunica in #claw-code 2026-04-09; gaebal-gajae ROADMAP note same cycle.
|
||||
|
||||
45. **`claw dump-manifests` fails with opaque "No such file or directory"** — dogfooded 2026-04-09. `claw dump-manifests` emits `error: failed to extract manifests: No such file or directory (os error 2)` with no indication of which file or directory is missing. **Partial fix at `47aa1a5`+1**: error message now includes `looked in: <path>` so the build-tree path is visible, what manifests are, or how to fix it. Fix shape: (a) surface the missing path in the error message; (b) add a pre-check that explains what manifests are and where they should be (e.g. `.claw/manifests/` or the plugins directory); (c) if the command is only valid after `claw init` or after installing plugins, say so explicitly. Source: Jobdori dogfood 2026-04-09.
|
||||
|
||||
45. **`claw dump-manifests` fails with opaque `No such file or directory`** — **done (verified 2026-04-12):** current `main` now accepts `claw dump-manifests --manifests-dir PATH`, pre-checks for the required upstream manifest files (`src/commands.ts`, `src/tools.ts`, `src/entrypoints/cli.tsx`), and replaces the opaque os error with guidance that points users to `CLAUDE_CODE_UPSTREAM` or `--manifests-dir`. Fresh proof: parser coverage for both flag forms, unit coverage for missing-manifest and explicit-path flows, and `output_format_contract` JSON coverage via the new flag all pass. **Original filing below.**
|
||||
45. **`claw dump-manifests` fails with opaque `No such file or directory`** — **done (verified 2026-04-12):** current `main` now accepts `claw dump-manifests --manifests-dir PATH`, pre-checks for the required upstream manifest files (`src/commands.ts`, `src/tools.ts`, `src/entrypoints/cli.tsx`), and replaces the opaque os error with guidance that points users to `CLAUDE_CODE_UPSTREAM` or `--manifests-dir`. Fresh proof: parser coverage for both flag forms, unit coverage for missing-manifest and explicit-path flows, and `output_format_contract` JSON coverage via the new flag all pass. **Original filing below.**
|
||||
46. **`/tokens`, `/cache`, `/stats` were dead spec — parse arms missing** — dogfooded 2026-04-09. All three had spec entries with `resume_supported: true` but no parse arms, producing the circular error "Unknown slash command: /tokens — Did you mean /tokens". Also `SlashCommand::Stats` existed but was unimplemented in both REPL and resume dispatch. **Done at `60ec2ae` 2026-04-09**: `"tokens" | "cache"` now alias to `SlashCommand::Stats`; `Stats` is wired in both REPL and resume path with full JSON output. Source: Jobdori dogfood.
|
||||
|
||||
47. **`/diff` fails with cryptic "unknown option 'cached'" outside a git repo; resume /diff used wrong CWD** — dogfooded 2026-04-09. `claw --resume <session> /diff` in a non-git directory produced `git diff --cached failed: error: unknown option 'cached'` because git falls back to `--no-index` mode outside a git tree. Also resume `/diff` used `session_path.parent()` (the `.claw/sessions/<id>/` dir) as CWD for the diff — never a git repo. **Done at `aef85f8` 2026-04-09**: `render_diff_report_for()` now checks `git rev-parse --is-inside-work-tree` first and returns a clear "no git repository" message; resume `/diff` uses `std::env::current_dir()`. Source: Jobdori dogfood.
|
||||
@@ -490,7 +494,9 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes
|
||||
|
||||
61. **`OPENAI_BASE_URL` ignored when model name has no recognized prefix** — user report 2026-04-10 in #claw-code (MaxDerVerpeilte, Ollama). User set `OPENAI_BASE_URL=http://127.0.0.1:11434/v1` with model `qwen2.5-coder:7b` but claw asked for Anthropic credentials. `detect_provider_kind()` checks model prefix first, then falls through to env-var presence — but `OPENAI_BASE_URL` was not in the cascade, so unrecognized model names always hit the Anthropic default. **Done at `1ecdb10` 2026-04-10**: `OPENAI_BASE_URL` + `OPENAI_API_KEY` now beats Anthropic env-check. `OPENAI_BASE_URL` alone (no key, e.g. Ollama) is last-resort before Anthropic default. Source: MaxDerVerpeilte in #claw-code; traced by gaebal-gajae.
|
||||
|
||||
62. **Worker state file surface not implemented** — dogfooded 2026-04-10. ROADMAP "Deployment Architecture Gap" section documents `.claw/worker-state.json` as the "canonical observability contract" for clawhip, but `emit_state_file()` is not wired in `WorkerRegistry::transition()`. The file is never written. **Status:** documentation exists, code does not. Fix shape: wire `emit_state_file()` to write `.claw/worker-state.json` atomically on every `WorkerStatus` transition; add `claw state` CLI subcommand to read/display it. Blocker: none. Source: Jobdori dogfood.
|
||||
62. **Worker state file surface not implemented** — **done (verified 2026-04-12):** current `main` already wires `emit_state_file(worker)` into the worker transition path in `rust/crates/runtime/src/worker_boot.rs`, atomically writes `.claw/worker-state.json`, and exposes the documented reader surface through `claw state` / `claw state --output-format json` in `rust/crates/rusty-claude-cli/src/main.rs`. Fresh proof exists in `runtime` regression `emit_state_file_writes_worker_status_on_transition`, the end-to-end `tools` regression `recovery_loop_state_file_reflects_transitions`, and direct CLI parsing coverage for `state` / `state --output-format json`. Source: Jobdori dogfood.
|
||||
|
||||
**Scope note (verified 2026-04-12):** ROADMAP #31, #43, and #63-#68 currently appear to describe acpx/droid or upstream OMX/server orchestration behavior, not claw-code source already present in this repository. Repo-local searches for `acpx`, `use-droid`, `run-acpx`, `commit-wrapper`, `ultraclaw`, `roadmap-nudge-10min`, `OMX_TMUX_INJECT`, `/hooks/health`, and `/hooks/status` found no implementation hits outside `ROADMAP.md`, and the earlier state-surface note already records that the HTTP server is not owned by claw-code. With #45 now fixed, the remaining unresolved items in this section look like external tracking notes rather than confirmed repo-local backlog; re-check if new repo-local evidence appears.
|
||||
|
||||
63. **Droid session completion semantics broken: code arrives after "status: completed"** — dogfooded 2026-04-12. Ultraclaw droid sessions (use-droid via acpx) report `session.status: completed` before file writes are fully flushed/synced to the working tree. Discovered +410 lines of "late-arriving" droid output that appeared after I had already assessed 8 sessions as "no code produced." This creates false-negative assessments and duplicate work. **Fix shape:** (a) droid agent should only report completion after explicit file-write confirmation (fsync or existence check); (b) or, claw-code should expose a `pending_writes` status that indicates "agent responded, disk flush pending"; (c) lane orchestrators should poll for file changes for N seconds after completion before final assessment. **Blocker:** none. Source: Jobdori ultraclaw dogfood 2026-04-12.
|
||||
|
||||
@@ -499,3 +505,9 @@ Model name prefix now wins unconditionally over env-var presence. Regression tes
|
||||
65. **Backlog-scanning team lanes emit opaque stops, not structured selection outcomes** — dogfooded 2026-04-12. $ralph $team sessions scanning ROADMAP Immediate Backlog stop with summary text naming open items, but no machine-readable signal of: which item(s) were selected for work, which were skipped and why, whether execution happened vs review-only vs no-op. **Fix shape:** add structured "selection outcome" event with `chosenItems`, `skippedItems`, `rationale`, `action: execute|review|no-op`. Stop emitting "check backlog" as prose summary without selection contract. Blocker: none. Source: gaebal-gajae dogfood analysis 2026-04-12.
|
||||
|
||||
66. **Completion-aware reminder shutdown missing** — dogfooded 2026-04-12. Ultraclaw batch completed and was reported as done, but 10-minute cron reminder (`roadmap-nudge-10min`) kept firing into channel as if work still pending. Reminder/cron state not coupled to terminal task state. **Fix shape:** (a) cron jobs should check task completion state before firing; (b) or, provide explicit `cron.remove` on task completion; (c) or, reminders should include "work complete" detection and auto-expire. Blocker: none. Source: gaebal-gajae dogfood analysis 2026-04-12.
|
||||
|
||||
67. **Scoped review lanes do not emit structured verdicts** — dogfooded 2026-04-12. OMX review lanes now have improved scope (specific ROADMAP items, specific files, explicit APPROVE/REJECT contract), but the stop event only contains the review request — not the actual verdict. Operators must infer approval/rejection/blockage from later git commits or surrounding chatter. **Fix shape:** emit structured review result on stop with: `verdict: approve|reject|blocked`, `target: commit/diff reviewed`, `rationale: short summary`. Blocker: none. Source: gaebal-gajae dogfood analysis 2026-04-12.
|
||||
|
||||
68. **Internal reinjection/resume paths leak opaque control prose** — dogfooded 2026-04-12. OMX lanes stopping with `Continue from current mode state. [OMX_TMUX_INJECT]` expose internal implementation details instead of operator-meaningful state. The event tells us *that* tmux reinjection happened, but not *why* (retry after failure? resume after idle? manual recovery?), *what state was preserved*, or *what the lane was trying to do*. **Fix shape:** recovery/reinject events should emit structured cause like: `resume_after_stop`, `retry_after_tool_failure`, `tmux_reinject_after_idle`, `manual_recovery` plus preserved state / target lane info. Never leak bare internal markers like `[OMX_TMUX_INJECT]` as the primary summary. Blocker: none. Source: gaebal-gajae dogfood analysis 2026-04-12.
|
||||
|
||||
69. **Lane stop summaries have no minimum quality floor** — dogfooded 2026-04-12. `clawcode-human` session stopped with summary `commit push everyting, keep sweeping $ralph` — vague, typo-ridden, operationally useless. Unlike well-scoped review lanes, this summary regressed to mushy command prose with no outcome clarity. **Fix shape:** (a) enforce minimum stop/result summary standards: what was done (outcome), what was scoped (target), what's next (state); (b) typo/grammar validation; (c) reject summaries that are shorter than N words or contain only control verbs without context. Blocker: none. Source: gaebal-gajae dogfood analysis 2026-04-12.
|
||||
@@ -1,2 +1 @@
|
||||
{"created_at_ms":1775386832313,"session_id":"session-1775386832313-0","type":"session_meta","updated_at_ms":1775386832313,"version":1}
|
||||
{"message":{"blocks":[{"text":"status --help","type":"text"}],"role":"user"},"type":"message"}
|
||||
{"created_at_ms":1775777421902,"session_id":"session-1775777421902-1","type":"session_meta","updated_at_ms":1775777421902,"version":1}
|
||||
|
||||
@@ -4121,12 +4121,15 @@ mod tests {
|
||||
handle_plugins_slash_command, handle_skills_slash_command_json, handle_slash_command,
|
||||
load_agents_from_roots, load_skills_from_roots, render_agents_report,
|
||||
render_agents_report_json, render_mcp_report_json_for, render_plugins_report,
|
||||
render_skills_report, render_slash_command_help, render_slash_command_help_detail,
|
||||
resolve_skill_path, resume_supported_slash_commands, slash_command_specs,
|
||||
suggest_slash_commands, validate_slash_command_input, DefinitionSource, SkillOrigin,
|
||||
SkillRoot, SkillSlashDispatch, SlashCommand,
|
||||
render_plugins_report_with_failures, render_skills_report, render_slash_command_help,
|
||||
render_slash_command_help_detail, resolve_skill_path, resume_supported_slash_commands,
|
||||
slash_command_specs, suggest_slash_commands, validate_slash_command_input,
|
||||
DefinitionSource, SkillOrigin, SkillRoot, SkillSlashDispatch, SlashCommand,
|
||||
};
|
||||
use plugins::{
|
||||
PluginError, PluginKind, PluginLoadFailure, PluginManager, PluginManagerConfig,
|
||||
PluginMetadata, PluginSummary,
|
||||
};
|
||||
use plugins::{PluginKind, PluginManager, PluginManagerConfig, PluginMetadata, PluginSummary};
|
||||
use runtime::{
|
||||
CompactionConfig, ConfigLoader, ContentBlock, ConversationMessage, MessageRole, Session,
|
||||
};
|
||||
@@ -4884,6 +4887,36 @@ mod tests {
|
||||
assert!(rendered.contains("disabled"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_plugins_report_with_broken_plugin_warnings() {
|
||||
let rendered = render_plugins_report_with_failures(
|
||||
&[PluginSummary {
|
||||
metadata: PluginMetadata {
|
||||
id: "demo@external".to_string(),
|
||||
name: "demo".to_string(),
|
||||
version: "1.2.3".to_string(),
|
||||
description: "demo plugin".to_string(),
|
||||
kind: PluginKind::External,
|
||||
source: "demo".to_string(),
|
||||
default_enabled: false,
|
||||
root: None,
|
||||
},
|
||||
enabled: true,
|
||||
}],
|
||||
&[PluginLoadFailure::new(
|
||||
PathBuf::from("/tmp/broken-plugin"),
|
||||
PluginKind::External,
|
||||
"broken".to_string(),
|
||||
PluginError::InvalidManifest("hook path `hooks/pre.sh` does not exist".to_string()),
|
||||
)],
|
||||
);
|
||||
|
||||
assert!(rendered.contains("Warnings:"));
|
||||
assert!(rendered.contains("Failed to load external plugin"));
|
||||
assert!(rendered.contains("/tmp/broken-plugin"));
|
||||
assert!(rendered.contains("does not exist"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lists_agents_from_project_and_user_roots() {
|
||||
let workspace = temp_dir("agents-workspace");
|
||||
|
||||
@@ -177,7 +177,10 @@ fn merge_prompt_with_stdin(prompt: &str, stdin_content: Option<&str>) -> String
|
||||
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<String> = env::args().skip(1).collect();
|
||||
match parse_args(&args)? {
|
||||
CliAction::DumpManifests { output_format } => dump_manifests(output_format)?,
|
||||
CliAction::DumpManifests {
|
||||
output_format,
|
||||
manifests_dir,
|
||||
} => dump_manifests(manifests_dir.as_deref(), output_format)?,
|
||||
CliAction::BootstrapPlan { output_format } => print_bootstrap_plan(output_format)?,
|
||||
CliAction::Agents {
|
||||
args,
|
||||
@@ -274,6 +277,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
enum CliAction {
|
||||
DumpManifests {
|
||||
output_format: CliOutputFormat,
|
||||
manifests_dir: Option<PathBuf>,
|
||||
},
|
||||
BootstrapPlan {
|
||||
output_format: CliOutputFormat,
|
||||
@@ -623,7 +627,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
let permission_mode = permission_mode_override.unwrap_or_else(default_permission_mode);
|
||||
|
||||
match rest[0].as_str() {
|
||||
"dump-manifests" => Ok(CliAction::DumpManifests { output_format }),
|
||||
"dump-manifests" => parse_dump_manifests_args(&rest[1..], output_format),
|
||||
"bootstrap-plan" => Ok(CliAction::BootstrapPlan { output_format }),
|
||||
"agents" => Ok(CliAction::Agents {
|
||||
args: join_optional_args(&rest[1..]),
|
||||
@@ -778,6 +782,22 @@ fn removed_auth_surface_error(command_name: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn try_resolve_bare_skill_prompt(cwd: &Path, trimmed: &str) -> Option<String> {
|
||||
let bare_first_token = trimmed.split_whitespace().next().unwrap_or_default();
|
||||
let looks_like_skill_name = !bare_first_token.is_empty()
|
||||
&& !bare_first_token.starts_with('/')
|
||||
&& bare_first_token
|
||||
.chars()
|
||||
.all(|c| c.is_alphanumeric() || c == '-' || c == '_');
|
||||
if !looks_like_skill_name {
|
||||
return None;
|
||||
}
|
||||
match resolve_skill_invocation(cwd, Some(trimmed)) {
|
||||
Ok(SkillSlashDispatch::Invoke(prompt)) => Some(prompt),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn join_optional_args(args: &[String]) -> Option<String> {
|
||||
let joined = args.join(" ");
|
||||
let trimmed = joined.trim();
|
||||
@@ -1197,6 +1217,39 @@ fn parse_export_args(args: &[String], output_format: CliOutputFormat) -> Result<
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_dump_manifests_args(
|
||||
args: &[String],
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<CliAction, String> {
|
||||
let mut manifests_dir: Option<PathBuf> = None;
|
||||
let mut index = 0;
|
||||
while index < args.len() {
|
||||
let arg = &args[index];
|
||||
if arg == "--manifests-dir" {
|
||||
let value = args
|
||||
.get(index + 1)
|
||||
.ok_or_else(|| String::from("--manifests-dir requires a path"))?;
|
||||
manifests_dir = Some(PathBuf::from(value));
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
if let Some(value) = arg.strip_prefix("--manifests-dir=") {
|
||||
if value.is_empty() {
|
||||
return Err(String::from("--manifests-dir requires a path"));
|
||||
}
|
||||
manifests_dir = Some(PathBuf::from(value));
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
return Err(format!("unknown dump-manifests option: {arg}"));
|
||||
}
|
||||
|
||||
Ok(CliAction::DumpManifests {
|
||||
output_format,
|
||||
manifests_dir,
|
||||
})
|
||||
}
|
||||
|
||||
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), &[]),
|
||||
@@ -1899,32 +1952,58 @@ fn looks_like_slash_command_token(token: &str) -> bool {
|
||||
.any(|spec| spec.name == name || spec.aliases.contains(&name))
|
||||
}
|
||||
|
||||
fn dump_manifests(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn dump_manifests(
|
||||
manifests_dir: Option<&Path>,
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let workspace_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
dump_manifests_at_path(&workspace_dir, output_format)
|
||||
dump_manifests_at_path(&workspace_dir, manifests_dir, output_format)
|
||||
}
|
||||
|
||||
const DUMP_MANIFESTS_OVERRIDE_HINT: &str =
|
||||
"Hint: set CLAUDE_CODE_UPSTREAM=/path/to/upstream or pass `claw dump-manifests --manifests-dir /path/to/upstream`.";
|
||||
|
||||
// Internal function for testing that accepts a workspace directory path.
|
||||
fn dump_manifests_at_path(
|
||||
workspace_dir: &std::path::Path,
|
||||
manifests_dir: Option<&Path>,
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Surface the resolved path in the error so users can diagnose missing
|
||||
// manifest files without guessing what path the binary expected.
|
||||
// ROADMAP #45: this path is only correct when running from the build tree;
|
||||
// a proper fix would ship manifests alongside the binary.
|
||||
let resolved = workspace_dir
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| workspace_dir.to_path_buf());
|
||||
let paths = if let Some(dir) = manifests_dir {
|
||||
let resolved = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
|
||||
UpstreamPaths::from_repo_root(resolved)
|
||||
} else {
|
||||
// Surface the resolved path in the error so users can diagnose missing
|
||||
// manifest files without guessing what path the binary expected.
|
||||
let resolved = workspace_dir
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| workspace_dir.to_path_buf());
|
||||
UpstreamPaths::from_workspace_dir(&resolved)
|
||||
};
|
||||
|
||||
let paths = UpstreamPaths::from_workspace_dir(&resolved);
|
||||
|
||||
// Pre-check: verify manifest directory exists
|
||||
let manifest_dir = paths.repo_root();
|
||||
if !manifest_dir.exists() {
|
||||
let source_root = paths.repo_root();
|
||||
if !source_root.exists() {
|
||||
return Err(format!(
|
||||
"Manifest files (commands.ts, tools.ts) define CLI commands and tools.\n Expected at: {}\n Run `claw init` to create them or specify --manifests-dir.",
|
||||
manifest_dir.display()
|
||||
"Manifest source directory does not exist.\n looked in: {}\n {DUMP_MANIFESTS_OVERRIDE_HINT}",
|
||||
source_root.display(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let required_paths = [
|
||||
("src/commands.ts", paths.commands_path()),
|
||||
("src/tools.ts", paths.tools_path()),
|
||||
("src/entrypoints/cli.tsx", paths.cli_path()),
|
||||
];
|
||||
let missing = required_paths
|
||||
.iter()
|
||||
.filter_map(|(label, path)| (!path.is_file()).then_some(*label))
|
||||
.collect::<Vec<_>>();
|
||||
if !missing.is_empty() {
|
||||
return Err(format!(
|
||||
"Manifest source files are missing.\n repo root: {}\n missing: {}\n {DUMP_MANIFESTS_OVERRIDE_HINT}",
|
||||
source_root.display(),
|
||||
missing.join(", "),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
@@ -1950,7 +2029,7 @@ fn dump_manifests_at_path(
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => Err(format!(
|
||||
"failed to extract manifests: {error}\n looked in: {path}",
|
||||
"failed to extract manifests: {error}\n looked in: {path}\n {DUMP_MANIFESTS_OVERRIDE_HINT}",
|
||||
path = paths.repo_root().display()
|
||||
)
|
||||
.into()),
|
||||
@@ -2977,22 +3056,12 @@ fn run_repl(
|
||||
// Bare-word skill dispatch: if the first token of the input
|
||||
// matches a known skill name, invoke it as `/skills <input>`
|
||||
// rather than forwarding raw text to the LLM (ROADMAP #36).
|
||||
let bare_first_token = trimmed.split_whitespace().next().unwrap_or_default();
|
||||
let looks_like_skill_name = !bare_first_token.is_empty()
|
||||
&& !bare_first_token.starts_with('/')
|
||||
&& bare_first_token
|
||||
.chars()
|
||||
.all(|c| c.is_alphanumeric() || c == '-' || c == '_');
|
||||
if looks_like_skill_name {
|
||||
let cwd = std::env::current_dir().unwrap_or_default();
|
||||
if let Ok(SkillSlashDispatch::Invoke(prompt)) =
|
||||
resolve_skill_invocation(&cwd, Some(&trimmed))
|
||||
{
|
||||
editor.push_history(input);
|
||||
cli.record_prompt_history(&trimmed);
|
||||
cli.run_turn(&prompt)?;
|
||||
continue;
|
||||
}
|
||||
let cwd = std::env::current_dir().unwrap_or_default();
|
||||
if let Some(prompt) = try_resolve_bare_skill_prompt(&cwd, &trimmed) {
|
||||
editor.push_history(input);
|
||||
cli.record_prompt_history(&trimmed);
|
||||
cli.run_turn(&prompt)?;
|
||||
continue;
|
||||
}
|
||||
editor.push_history(input);
|
||||
cli.record_prompt_history(&trimmed);
|
||||
@@ -8042,7 +8111,7 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
out,
|
||||
" Diagnose local auth, config, workspace, and sandbox health"
|
||||
)?;
|
||||
writeln!(out, " claw dump-manifests")?;
|
||||
writeln!(out, " claw dump-manifests [--manifests-dir PATH]")?;
|
||||
writeln!(out, " claw bootstrap-plan")?;
|
||||
writeln!(out, " claw agents")?;
|
||||
writeln!(out, " claw mcp")?;
|
||||
@@ -8176,10 +8245,11 @@ mod tests {
|
||||
resolve_repl_model, resolve_session_reference, response_to_events,
|
||||
resume_supported_slash_commands, run_resume_command, short_tool_id,
|
||||
slash_command_completion_candidates_with_sessions, status_context,
|
||||
summarize_tool_payload_for_markdown, validate_no_args, write_mcp_server_fixture, CliAction,
|
||||
CliOutputFormat, CliToolExecutor, GitWorkspaceSummary, InternalPromptProgressEvent,
|
||||
InternalPromptProgressState, LiveCli, LocalHelpTopic, PromptHistoryEntry, SlashCommand,
|
||||
StatusUsage, DEFAULT_MODEL, LATEST_SESSION_REFERENCE, STUB_COMMANDS,
|
||||
summarize_tool_payload_for_markdown, try_resolve_bare_skill_prompt, validate_no_args,
|
||||
write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor, GitWorkspaceSummary,
|
||||
InternalPromptProgressEvent, InternalPromptProgressState, LiveCli, LocalHelpTopic,
|
||||
PromptHistoryEntry, SlashCommand, StatusUsage, DEFAULT_MODEL, LATEST_SESSION_REFERENCE,
|
||||
STUB_COMMANDS,
|
||||
};
|
||||
use api::{ApiError, MessageResponse, OutputContentBlock, Usage};
|
||||
use plugins::{
|
||||
@@ -8420,6 +8490,16 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_skill_fixture(root: &Path, name: &str, description: &str) {
|
||||
let skill_dir = root.join(name);
|
||||
fs::create_dir_all(&skill_dir).expect("skill dir should exist");
|
||||
fs::write(
|
||||
skill_dir.join("SKILL.md"),
|
||||
format!("---\nname: {name}\ndescription: {description}\n---\n\n# {name}\n"),
|
||||
)
|
||||
.expect("skill file should write");
|
||||
}
|
||||
|
||||
fn write_plugin_fixture(root: &Path, name: &str, include_hooks: bool, include_lifecycle: bool) {
|
||||
fs::create_dir_all(root.join(".claude-plugin")).expect("manifest dir");
|
||||
if include_hooks {
|
||||
@@ -9046,6 +9126,33 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dump_manifests_subcommand_accepts_explicit_manifest_dir() {
|
||||
assert_eq!(
|
||||
parse_args(&[
|
||||
"dump-manifests".to_string(),
|
||||
"--manifests-dir".to_string(),
|
||||
"/tmp/upstream".to_string(),
|
||||
])
|
||||
.expect("dump-manifests should parse"),
|
||||
CliAction::DumpManifests {
|
||||
output_format: CliOutputFormat::Text,
|
||||
manifests_dir: Some(PathBuf::from("/tmp/upstream")),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&[
|
||||
"dump-manifests".to_string(),
|
||||
"--manifests-dir=/tmp/upstream".to_string()
|
||||
])
|
||||
.expect("inline dump-manifests flag should parse"),
|
||||
CliAction::DumpManifests {
|
||||
output_format: CliOutputFormat::Text,
|
||||
manifests_dir: Some(PathBuf::from("/tmp/upstream")),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_command_help_flags_stay_on_the_local_parser_path() {
|
||||
assert_eq!(
|
||||
@@ -9713,6 +9820,38 @@ mod tests {
|
||||
assert!(help.contains("works with --resume SESSION.jsonl"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_skill_dispatch_resolves_known_project_skill_to_prompt() {
|
||||
let _guard = env_lock();
|
||||
let workspace = temp_dir();
|
||||
write_skill_fixture(
|
||||
&workspace.join(".codex").join("skills"),
|
||||
"caveman",
|
||||
"Project skill fixture",
|
||||
);
|
||||
|
||||
let prompt = try_resolve_bare_skill_prompt(&workspace, "caveman sharpen club")
|
||||
.expect("known bare skill should dispatch");
|
||||
assert_eq!(prompt, "$caveman sharpen club");
|
||||
|
||||
fs::remove_dir_all(workspace).expect("workspace should clean up");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_skill_dispatch_ignores_unknown_or_non_skill_input() {
|
||||
let _guard = env_lock();
|
||||
let workspace = temp_dir();
|
||||
fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||
|
||||
assert_eq!(
|
||||
try_resolve_bare_skill_prompt(&workspace, "not-a-known-skill do thing"),
|
||||
None
|
||||
);
|
||||
assert_eq!(try_resolve_bare_skill_prompt(&workspace, "/status"), None);
|
||||
|
||||
fs::remove_dir_all(workspace).expect("workspace should clean up");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repl_help_includes_shared_commands_and_exit() {
|
||||
let help = render_repl_help();
|
||||
@@ -11505,43 +11644,78 @@ mod sandbox_report_tests {
|
||||
#[cfg(test)]
|
||||
mod dump_manifests_tests {
|
||||
use super::{dump_manifests_at_path, CliOutputFormat};
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn dump_manifests_shows_helpful_error_when_manifests_missing() {
|
||||
// Create a temp directory without manifest files
|
||||
let temp_dir = std::env::temp_dir().join(format!(
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"claw_test_missing_manifests_{}",
|
||||
std::process::id()
|
||||
));
|
||||
std::fs::create_dir_all(&temp_dir).expect("failed to create temp dir");
|
||||
let workspace = root.join("workspace");
|
||||
std::fs::create_dir_all(&workspace).expect("failed to create temp workspace");
|
||||
|
||||
// Clean up at the end of the test
|
||||
let _cleanup = std::panic::catch_unwind(|| {
|
||||
// Call dump_manifests_at_path with the temp directory
|
||||
let result = dump_manifests_at_path(&temp_dir, CliOutputFormat::Text);
|
||||
let result = dump_manifests_at_path(&workspace, None, CliOutputFormat::Text);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"expected an error when manifests are missing"
|
||||
);
|
||||
|
||||
// Assert that the call fails
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"expected an error when manifests are missing"
|
||||
);
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
error_msg.contains("Manifest source files are missing"),
|
||||
"error message should mention missing manifest sources: {error_msg}"
|
||||
);
|
||||
assert!(
|
||||
error_msg.contains(&root.display().to_string()),
|
||||
"error message should contain the resolved repo root path: {error_msg}"
|
||||
);
|
||||
assert!(
|
||||
error_msg.contains("src/commands.ts"),
|
||||
"error message should mention missing commands.ts: {error_msg}"
|
||||
);
|
||||
assert!(
|
||||
error_msg.contains("CLAUDE_CODE_UPSTREAM"),
|
||||
"error message should explain how to supply the upstream path: {error_msg}"
|
||||
);
|
||||
|
||||
// Assert the error message contains "Manifest files (commands.ts, tools.ts)"
|
||||
assert!(
|
||||
error_msg.contains("Manifest files (commands.ts, tools.ts)"),
|
||||
"error message should mention manifest files: {error_msg}"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
// Assert the error message contains the expected path
|
||||
assert!(
|
||||
error_msg.contains(&temp_dir.display().to_string()),
|
||||
"error message should contain the expected path: {error_msg}"
|
||||
);
|
||||
});
|
||||
#[test]
|
||||
fn dump_manifests_uses_explicit_manifest_dir() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"claw_test_explicit_manifest_dir_{}",
|
||||
std::process::id()
|
||||
));
|
||||
let workspace = root.join("workspace");
|
||||
let upstream = root.join("upstream");
|
||||
fs::create_dir_all(workspace.join("nested")).expect("workspace should exist");
|
||||
fs::create_dir_all(upstream.join("src/entrypoints"))
|
||||
.expect("upstream fixture should exist");
|
||||
fs::write(
|
||||
upstream.join("src/commands.ts"),
|
||||
"import FooCommand from './commands/foo'\n",
|
||||
)
|
||||
.expect("commands fixture should write");
|
||||
fs::write(
|
||||
upstream.join("src/tools.ts"),
|
||||
"import ReadTool from './tools/read'\n",
|
||||
)
|
||||
.expect("tools fixture should write");
|
||||
fs::write(
|
||||
upstream.join("src/entrypoints/cli.tsx"),
|
||||
"startupProfiler()\n",
|
||||
)
|
||||
.expect("cli fixture should write");
|
||||
|
||||
// Clean up temp directory
|
||||
let _ = std::fs::remove_dir_all(&temp_dir);
|
||||
let result = dump_manifests_at_path(&workspace, Some(&upstream), CliOutputFormat::Text);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"explicit manifest dir should succeed: {result:?}"
|
||||
);
|
||||
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,13 +174,15 @@ fn dump_manifests_and_init_emit_json_when_requested() {
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
|
||||
let upstream = write_upstream_fixture(&root);
|
||||
let manifests = assert_json_command_with_env(
|
||||
let manifests = assert_json_command(
|
||||
&root,
|
||||
&["--output-format", "json", "dump-manifests"],
|
||||
&[(
|
||||
"CLAUDE_CODE_UPSTREAM",
|
||||
&[
|
||||
"--output-format",
|
||||
"json",
|
||||
"dump-manifests",
|
||||
"--manifests-dir",
|
||||
upstream.to_str().expect("utf8 upstream"),
|
||||
)],
|
||||
],
|
||||
);
|
||||
assert_eq!(manifests["kind"], "dump-manifests");
|
||||
assert_eq!(manifests["commands"], 1);
|
||||
|
||||
Reference in New Issue
Block a user