Commit Graph

700 Commits

Author SHA1 Message Date
YeonGyu-Kim
54269da157 fix(cli): claw state exits 1 when no worker state file exists
Previously 'claw state' printed an error message but exited 0, making it
impossible for scripts/CI to detect the absence of state without parsing
prose. Now propagates Err() to main() which exits 1 and formats the error
correctly for both text and --output-format json modes.

Text: 'error: no worker state file found at ... — run a worker first'
JSON: {"type":"error","error":"no worker state file found at ..."}
2026-04-09 18:34:41 +09:00
YeonGyu-Kim
f741a42507 test(cli): add regression coverage for reasoning-effort validation and stub-command filtering
3 new tests in mod tests:
- rejects_invalid_reasoning_effort_value: confirms 'turbo' etc rejected at parse time
- accepts_valid_reasoning_effort_values: confirms low/medium/high accepted and threaded
- stub_commands_absent_from_repl_completions: asserts STUB_COMMANDS are not in completions

156 -> 159 CLI tests pass.
2026-04-09 18:06:32 +09:00
YeonGyu-Kim
6b3e2d8854 docs(roadmap): file hook ingress opacity as ROADMAP #43 2026-04-09 17:34:15 +09:00
YeonGyu-Kim
1a8f73da01 fix(cli): emit JSON error on --output-format json — ROADMAP #42
When claw --output-format json hits an error, the error was previously
printed as plain prose to stderr, making it invisible to downstream tooling
that parses JSON output. Now:

  {"type":"error","error":"api returned 401 ..."}

Detection: scan argv at process exit for --output-format json or
--output-format=json. Non-JSON error path unchanged. 156 CLI tests pass.
2026-04-09 16:33:20 +09:00
YeonGyu-Kim
7d9f11b91f docs(roadmap): track community-support plugin-test-sealing as #41 2026-04-09 16:18:48 +09:00
YeonGyu-Kim
8e1bca6b99 docs(roadmap): track community-support plugin-list-load-failures as #40 2026-04-09 16:17:28 +09:00
YeonGyu-Kim
8d0308eecb fix(cli): dispatch bare skill names to skill invoker in REPL — ROADMAP #36
Users were typing skill names (e.g. 'caveman', 'find-skills') directly in
the REPL and getting LLM responses instead of skill invocation. Only
'/skills <name>' triggered dispatch; bare names fell through to run_turn.

Fix: after slash-command parse returns None (bare text), check if the first
token looks like a skill name (alphanumeric/dash/underscore, no slash).
If resolve_skill_invocation() confirms the skill exists, dispatch the full
input as a skill prompt. Unknown words fall through unchanged.

156 CLI tests pass, fmt clean.
2026-04-09 16:01:18 +09:00
YeonGyu-Kim
4d10caebc6 fix(cli): validate --reasoning-effort accepts only low|medium|high
Previously any string was accepted and silently forwarded to the API,
which would fail at the provider with an unhelpful error. Now invalid
values produce a clear error at parse time:

  invalid value for --reasoning-effort: 'xyz'; must be low, medium, or high

156 CLI tests pass, fmt clean.
2026-04-09 15:03:36 +09:00
YeonGyu-Kim
414526c1bd fix(cli): exclude stub slash commands from help output — ROADMAP #39
The --help slash-command section was listing ~35 unimplemented commands
alongside working ones. Combined with the completions fix (c55c510), the
discovery surface now consistently shows only implemented commands.

Changes:
- commands crate: add render_slash_command_help_filtered(exclude: &[&str])
- move STUB_COMMANDS to module-level const in main.rs (reused by both
  completions and help rendering)
- replace render_slash_command_help() with filtered variant at all
  help-rendering call sites

156 CLI tests pass, fmt clean.
2026-04-09 14:36:00 +09:00
YeonGyu-Kim
2a2e205414 fix(cli): intercept --help for prompt/login/logout/version subcommands before API dispatch
'claw prompt --help' was triggering an API call instead of showing help
because --help was parsed as part of the prompt args. Now '--help' after
known pass-through subcommands (prompt, login, logout, version, state,
init, export, commit, pr, issue) sets wants_help=true and shows the
top-level help page.

Subcommands that consume their own args (agents, mcp, plugins, skills)
and local help-topic subcommands (status, sandbox, doctor) are excluded
from this interception so their existing --help handling is preserved.

156 CLI tests pass, fmt clean.
2026-04-09 14:06:26 +09:00
YeonGyu-Kim
c55c510883 fix(cli): exclude stub slash commands from REPL completions — ROADMAP #39
Commands registered in the spec list but not yet implemented in this build
were appearing in REPL tab-completions, making the discovery surface
over-promise what actually works. Users (mezz2301) reported 'many features
are not supported' after discovering these through completions.

Add STUB_COMMANDS exclusion list in slash_command_completion_candidates_with_sessions.
Excluded: login logout vim upgrade stats share feedback files fast exit
summary desktop brief advisor stickers insights thinkback release-notes
security-review keybindings privacy-settings plan review tasks theme
voice usage rename copy hooks context color effort branch rewind ide
tag output-style add-dir

These commands still parse and run (with the 'not yet implemented' message
for users who type them directly), but they no longer surface as
tab-completion candidates.
2026-04-09 13:36:12 +09:00
YeonGyu-Kim
3fe0caf348 docs(roadmap): file stub slash commands as ROADMAP #39 (/branch /rewind /ide /tag /output-style /add-dir) 2026-04-09 12:31:17 +09:00
YeonGyu-Kim
47086c1c14 docs(readme): fix cold-start quick-start sequence — set API key before prompt, add claw doctor step
The previous quick start jumped from 'cargo build' to 'claw prompt' without
showing the required auth step or the health-check command. A user following
it linearly would fail because the prompt needs an API key.

Changes:
- Numbered steps: build -> set ANTHROPIC_API_KEY -> claw doctor -> prompt
- Windows note updated to show cargo run form as alternative
- Added explicit NOTE that Claude subscription login is not supported (pre-empts #claw-code FAQ)

Source: cold-start friction observed from mezz/mukduk and kapcomunica in #claw-code 2026-04-09.
2026-04-09 12:00:59 +09:00
YeonGyu-Kim
e579902782 docs(readme): add Windows PowerShell note — binary is claw.exe not claw
User repro: mezz on Windows PowerShell tried './target/debug/claw'
which fails because the binary is 'claw.exe' on Windows.
Add a NOTE callout after the quick-start block directing Windows users
to use .\target\debug\claw.exe or cargo run -- --help.
2026-04-09 11:30:53 +09:00
YeonGyu-Kim
ca8950c26b feat(cli): wire --reasoning-effort flag end-to-end — closes ROADMAP #34
Parse --reasoning-effort <low|medium|high> in parse_args, thread through
CliAction::Prompt and CliAction::Repl, LiveCli::set_reasoning_effort(),
AnthropicRuntimeClient.reasoning_effort field, and MessageRequest.reasoning_effort.

Changes:
- parse_args: new --reasoning-effort / --reasoning-effort=VAL flag arms
- AnthropicRuntimeClient: new reasoning_effort field + set_reasoning_effort() method
- LiveCli: new set_reasoning_effort() that reaches through BuiltRuntime -> ConversationRuntime -> api_client_mut()
- runtime::ConversationRuntime: new pub api_client_mut() accessor
- MessageRequest construction: reasoning_effort: self.reasoning_effort.clone()
- run_repl(): accepts and applies reasoning_effort parameter
- parse_direct_slash_cli_action(): propagates reasoning_effort

All 156 CLI tests pass, all api tests pass, cargo fmt clean.
2026-04-09 11:08:00 +09:00
YeonGyu-Kim
b1d76983d2 docs(readme): warn that cargo install clawcode is not supported; show build-from-source path
Repeated onboarding friction in #claw-code: users try 'cargo install clawcode'
which fails because the package is not published on crates.io. Add a prominent
NOTE callout before the quick-start block directing users to build from source.

Source: gaebal-gajae pinpoint 2026-04-09 from #claw-code.
2026-04-09 10:35:50 +09:00
YeonGyu-Kim
c1b1ce465e feat(cli): add reasoning_effort field to CliAction::Prompt/Repl variants — ROADMAP #34 struct groundwork
Adds reasoning_effort: Option<String> to CliAction::Prompt and
CliAction::Repl enum variants. All constructor and pattern sites updated.
All test literals updated with reasoning_effort: None.

156 cli tests pass, fmt clean. The --reasoning-effort flag parse and
propagation to AnthropicRuntimeClient remains as follow-up work.
2026-04-09 10:34:28 +09:00
YeonGyu-Kim
8e25611064 docs(roadmap): file dead-session opacity as ROADMAP #38 2026-04-09 10:00:50 +09:00
YeonGyu-Kim
eb044f0a02 fix(api): emit max_completion_tokens for gpt-5* on OpenAI-compat path — closes ROADMAP #35
gpt-5.x models reject requests with max_tokens and require max_completion_tokens.
Detect wire model starting with 'gpt-5' and switch the JSON key accordingly.
Older models (gpt-4o etc.) continue to receive max_tokens unchanged.

Two regression tests added:
- gpt5_uses_max_completion_tokens_not_max_tokens
- non_gpt5_uses_max_tokens

140 api tests pass, cargo fmt clean.
2026-04-09 09:33:45 +09:00
YeonGyu-Kim
75476c9005 docs(roadmap): file #35 max_completion_tokens, #36 skill dispatch gap, #37 auth policy cleanup 2026-04-09 09:32:16 +09:00
Jobdori
e4c3871882 feat(api): add reasoning_effort field to MessageRequest and OpenAI-compat path
Users of OpenAI-compatible reasoning models (o4-mini, o3, deepseek-r1,
etc.) had no way to control reasoning effort — the field was missing from
MessageRequest and never emitted in the request body.

Changes:
- Add `reasoning_effort: Option<String>` to `MessageRequest` in types.rs
  - Annotated with skip_serializing_if = "Option::is_none" for clean JSON
  - Accepted values: "low", "medium", "high" (passed through verbatim)
- In `build_chat_completion_request`, emit `"reasoning_effort"` when set
- Two unit tests:
  - `reasoning_effort_is_included_when_set`: o4-mini + "high" → field present
  - `reasoning_effort_omitted_when_not_set`: gpt-4o, no field → absent

Existing callers use `..Default::default()` and are unaffected.
One struct-literal test that listed all fields explicitly updated with
`reasoning_effort: None`.

The CLI flag to expose this to users is a follow-up (ROADMAP #34 partial).
This commit lands the foundational API-layer plumbing needed for that.

Partial ROADMAP #34.
2026-04-09 04:02:59 +09:00
Jobdori
beb09df4b8 style(api): cargo fmt fix on normalize_object_schema test assertions 2026-04-09 03:43:59 +09:00
Jobdori
811b7b4c24 docs(roadmap): mark #32 verified no-bug; file reasoning_effort gap as #34 2026-04-09 03:32:22 +09:00
Jobdori
8a9300ea96 docs(roadmap): mark #33 done, dedup #32 and #33 entries 2026-04-09 03:04:36 +09:00
Jobdori
e7e0fd2dbf fix(api): strict object schema for OpenAI /responses endpoint
OpenAI /responses validates tool function schemas strictly:
- object types must have "properties" (at minimum {})
- "additionalProperties": false is required

/chat/completions is lenient and accepts schemas without these fields,
but /responses rejects them with "object schema missing properties" /
"invalid_function_parameters".

Add normalize_object_schema() which recursively walks the JSON Schema
tree and fills in missing "properties"/{} and "additionalProperties":false
on every object-type node. Existing values are not overwritten.

Call it in openai_tool_definition() before building the request payload
so both /chat/completions and /responses receive strict-validator-safe
schemas.

Add unit tests covering:
- bare object schema gets both fields injected
- nested object schemas are normalised recursively
- existing additionalProperties is not overwritten

Fixes the live repro where gpt-5.4 via OpenAI compat accepted connection
and routing but rejected every tool call with schema validation errors.

Closes ROADMAP #33.
2026-04-09 03:03:43 +09:00
Jobdori
da451c66db docs(roadmap): file /responses tool-schema compatibility bug as #33 2026-04-08 21:23:45 +09:00
Jobdori
ad38032ab8 docs(roadmap): file /responses tool-schema compatibility bug as #33 2026-04-08 21:23:37 +09:00
Jobdori
7173f2d6c6 docs(roadmap): file /responses tool-schema compatibility bug as #33 2026-04-08 21:23:28 +09:00
Jobdori
a0b4156174 docs(roadmap): file /responses tool-schema compatibility bug as #33 2026-04-08 21:23:20 +09:00
Jobdori
3bf45fc44a docs(roadmap): file /responses tool-schema compatibility bug as #33 2026-04-08 21:23:12 +09:00
Jobdori
af58b6a7c7 docs(roadmap): file /responses tool-schema compatibility bug as #33 2026-04-08 21:23:04 +09:00
Jobdori
514c3da7ad docs(roadmap): file /responses tool-schema compatibility bug as #33 2026-04-08 21:22:56 +09:00
Jobdori
5c69713158 docs(roadmap): file OpenAI-compat model-id passthrough gap as #32 2026-04-08 19:48:34 +09:00
Jobdori
939d0dbaa3 docs(roadmap): file OpenAI-compat model-id passthrough gap as #32 2026-04-08 19:48:28 +09:00
Jobdori
bfd5772716 docs(roadmap): file OpenAI-compat model-id passthrough gap as #32 2026-04-08 19:48:21 +09:00
Jobdori
e0c3ff1673 docs(roadmap): file executor-contract leaks as ROADMAP #31 2026-04-08 18:34:58 +09:00
Jobdori
252536be74 fix(tools): serialize web_search env-var tests with env_lock to prevent race
web_search_extracts_and_filters_results set CLAWD_WEB_SEARCH_BASE_URL
without holding env_lock(), while the sibling test
web_search_handles_generic_links_and_invalid_base_url always held it.
Under parallel test execution the two tests interleave set_var/remove_var
calls, pointing the search client at the wrong mock server port and
causing assertion failures.

Fix: add env_lock() guard at the top of web_search_extracts_and_filters_results,
matching the serialization pattern already used by every other env-mutating
test in this module.

Root cause of CI flake on run 24127551802.
Identified and fixed during dogfood session.
2026-04-08 18:34:06 +09:00
Jobdori
275b58546d feat(cli): populate Git SHA, target triple, and build date at compile time via build.rs
Add rust/crates/rusty-claude-cli/build.rs that:
- Captures git rev-parse --short HEAD at build time → GIT_SHA env
- Reads Cargo's TARGET env var → TARGET env
- Derives BUILD_DATE from SOURCE_DATE_EPOCH / BUILD_DATE env or
  the current date via `date +%Y-%m-%d` fallback
- Registers rerun-if-changed on .git/HEAD and .git/refs so the SHA
  stays fresh across commits

Update main.rs DEFAULT_DATE to pick up BUILD_DATE from option_env!()
instead of the hardcoded 2026-03-31 static string.

Before: `claw --version` always showed Git SHA: unknown, Target: unknown,
Build date: 2026-03-31 in local builds.
After:  e.g. Git SHA: 7f53d82, Target: aarch64-apple-darwin, Build date: 2026-04-08

Generated by droid (Kimi K2.5 Turbo) via acpx (wrote build.rs),
cleaned up by Jobdori (added BUILD_DATE step, updated main.rs const).

Co-Authored-By: Droid <noreply@factory.ai>
2026-04-08 18:11:46 +09:00
Jobdori
7f53d82b17 docs(roadmap): file DashScope routing fix as #30 (done at adcea6b) 2026-04-08 18:05:17 +09:00
Jobdori
adcea6bceb fix(api): route DashScope models to dashscope config, not openai
ProviderClient::from_model_with_anthropic_auth was dispatching every
ProviderKind::OpenAi match to OpenAiCompatConfig::openai(), which reads
OPENAI_API_KEY and points at api.openai.com. But DashScope models
(qwen-plus, qwen/qwen3-coder, etc.) also return ProviderKind::OpenAi
from detect_provider_kind because DashScope speaks the OpenAI wire
format. The metadata layer correctly identifies them as needing
DASHSCOPE_API_KEY and the DashScope compatible-mode endpoint, but that
metadata was being ignored at dispatch time.

Result: users running `claw --model qwen-plus` with DASHSCOPE_API_KEY
set would get a "missing OPENAI_API_KEY" error instead of being routed
to DashScope.

Fix: consult providers::metadata_for_model in the OpenAi dispatch arm
and pick dashscope() vs openai() based on metadata.auth_env.

Adds a regression test asserting ProviderClient::from_model("qwen-plus")
builds with the DashScope base URL. Exposes a pub base_url() accessor
on OpenAiCompatClient so the test can verify the routing.

Authored by droid (Kimi K2.5 Turbo) via acpx, cleaned up by Jobdori
(removed unsafe blocks unnecessary under edition 2021, imported
ProviderClient from super, adopted EnvVarGuard pattern from
providers/mod.rs tests).

Co-Authored-By: Droid <noreply@factory.ai>
2026-04-08 18:04:37 +09:00
YeonGyu-Kim
b1491791df docs(roadmap): mark #21 and #29 as done
#21 (Resumed /status JSON parity gap): resolved by the broader
Resumed local-command JSON parity gap work tracked as #26. Re-verified
on main HEAD 8dc6580 — the regression test passes.

#29 (CLI provider dispatch hardcoded to Anthropic): landed at 8dc6580.
ApiProviderClient dispatch now routes correctly based on
detect_provider_kind. Original filing preserved as trace record.
2026-04-08 17:43:47 +09:00
YeonGyu-Kim
8dc65805c1 fix(cli): dispatch to correct provider backend based on model prefix — closes ROADMAP #29
The CLI entry point (build_runtime_with_plugin_state in main.rs)
was hardcoded to always instantiate AnthropicRuntimeClient with an
AnthropicClient, regardless of what detect_provider_kind(model)
returned. This meant `--model openai/gpt-4` with OPENAI_API_KEY
set and no ANTHROPIC_* vars still failed with "missing Anthropic
credentials" because the CLI never dispatched to the OpenAI-compat
backend that already exists in the api crate.

Root cause: AnthropicRuntimeClient.client was typed as
AnthropicClient (concrete) rather than ApiProviderClient (enum).
The api crate already had a ProviderClient enum with Anthropic /
Xai / OpenAi variants that dispatches correctly via
detect_provider_kind, plus a unified MessageStream enum that wraps
both anthropic::MessageStream and openai_compat::MessageStream
with the same next_event() -> StreamEvent interface. The CLI just
wasn't using it.

Changes (1 file, +59 -7):
- Import api::ProviderClient as ApiProviderClient
- Change AnthropicRuntimeClient.client from AnthropicClient to
  ApiProviderClient
- In AnthropicRuntimeClient::new(), dispatch based on
  detect_provider_kind(&resolved_model):
  * Anthropic: build AnthropicClient directly with
    resolve_cli_auth_source() + api::read_base_url() +
    PromptCache (preserves ANTHROPIC_BASE_URL override for mock
    test harness and the session-scoped prompt cache)
  * xAI / OpenAi: delegate to
    ApiProviderClient::from_model_with_anthropic_auth which routes
    to OpenAiCompatClient::from_env with the matching config
    (reads OPENAI_API_KEY/XAI_API_KEY/DASHSCOPE_API_KEY and their
    BASE_URL overrides internally)
- Change push_prompt_cache_record to take &ApiProviderClient
  (ProviderClient::take_last_prompt_cache_record returns None for
  non-Anthropic variants, so the helper is a no-op on
  OpenAI-compat providers without extra branching)

What this unlocks for users:
  claw --model openai/gpt-4.1-mini prompt 'hello'  # OpenAI
  claw --model grok-3 prompt 'hello'                # xAI
  claw --model qwen-plus prompt 'hello'             # DashScope
  OPENAI_BASE_URL=https://openrouter.ai/api/v1 \
    claw --model openai/anthropic/claude-sonnet-4 prompt 'hello'  # OpenRouter

All previously broken, now routed correctly by prefix.

Verification:
- cargo build --release -p rusty-claude-cli: clean
- cargo test --release -p rusty-claude-cli: 182 tests, 0 failures
  (including compact_output tests that exercise the Anthropic mock)
- cargo fmt --all: clean
- cargo clippy --workspace: warnings-only (pre-existing)
- cargo test --release --workspace: all crates green except one
  pre-existing race in runtime::config::tests (passes in isolation)

Source: live users nicma (1491342350960562277) and Jengro
(1491345009021030533) in #claw-code on 2026-04-08.
2026-04-08 17:29:55 +09:00
YeonGyu-Kim
a9904fe693 docs(roadmap): file CLI provider dispatch bug as #29, mark #28 as partial
#28 error-copy improvements landed on ff1df4c but real users (nicma,
Jengro) hit `error: missing Anthropic credentials` within hours when
using `--model openai/gpt-4` with OPENAI_API_KEY set and all
ANTHROPIC_* env vars unset on main.

Traced root cause in build_runtime_with_plugin_state at line ~6244:
AnthropicRuntimeClient::new() is hardcoded. BuiltRuntime is
statically typed as ConversationRuntime<AnthropicRuntimeClient, ...>.
providers::detect_provider_kind() computes the right routing at the
metadata layer but the runtime client is never dispatched.

Files #29 with the detailed trace + a focused action plan:
DynamicApiClient enum wrapping Anthropic + OpenAiCompat variants,
retype BuiltRuntime, dispatch in build_runtime based on
detect_provider_kind, integration test with mock OpenAI-compat
server.

#28 is marked partial — the error-copy improvements are real and
stayed in, but the routing gap they were meant to cover is the
actual bug and needs #29 to land.
2026-04-08 17:01:14 +09:00
YeonGyu-Kim
ff1df4c7ac fix(api): auth-provider error copy — prefix-routing hints + sk-ant-* bearer detection — closes ROADMAP #28
Two live users in #claw-code on 2026-04-08 hit adjacent auth confusion:
varleg set OPENAI_API_KEY for OpenRouter but prefix routing didn't
activate without openai/ model prefix, and stanley078852 put sk-ant-*
in ANTHROPIC_AUTH_TOKEN (Bearer path) instead of ANTHROPIC_API_KEY
(x-api-key path) and got 401 Invalid bearer token.

Changes:
1. ApiError::MissingCredentials gained optional hint field (error.rs)
2. anthropic_missing_credentials_hint() sniffs OPENAI/XAI/DASHSCOPE
   env vars and suggests prefix routing when present (providers/mod.rs)
3. All 4 Anthropic auth paths wire the hint helper (anthropic.rs)
4. 401 + sk-ant-* in bearer token detected and hint appended
5. 'Which env var goes where' section added to USAGE.md

Tests: unit tests for all three improvements (no HTTP calls needed).
Workspace: all tests green, fmt clean, clippy warnings-only.

Source: live users varleg + stanley078852 in #claw-code 2026-04-08.

Co-authored-by: gaebal-gajae <gaebal-gajae@layofflabs.com>
2026-04-08 16:29:03 +09:00
YeonGyu-Kim
efa24edf21 docs(roadmap): file auth-provider truth pinpoint as backlog #28
Filed from live #claw-code dogfood on 2026-04-08 where two real users
hit adjacent auth confusion within minutes:

- varleg set OPENAI_API_KEY for OpenRouter but prefix routing didn't
  win because the model name wasn't prefixed with openai/; unsetting
  ANTHROPIC_API_KEY then hit MissingApiKey with no hint that the
  OpenAI path was already configured
- stanley078852 put an sk-ant-* key in ANTHROPIC_AUTH_TOKEN instead
  of ANTHROPIC_API_KEY, causing claw to send it as
  Authorization: Bearer sk-ant-..., which Anthropic rejects at the
  edge with 401 Invalid bearer token

Both fixes delivered live in #claw-code as direct replies, but the
pattern is structural: the error surface doesn't bridge HTTP-layer
symptoms back to env-var choice.

Action block spells out a single main-side PR with three
improvements: (a) MissingCredentials hint when an adjacent
provider's env var is already set, (b) 401-on-Anthropic hint when
bearer token starts with sk-ant-, (c) 'which env var goes where'
paragraph in both README matrices mapping sk-ant-* -> x-api-key and
OAuth access token -> Authorization: Bearer.

All three improvements are unit-testable against ApiError::fmt
output with no HTTP calls required.
2026-04-08 15:58:46 +09:00
YeonGyu-Kim
8339391611 docs(roadmap): correct #25 root cause — BrokenPipe tolerance, not chmod
The original ROADMAP #25 entry claimed the root cause was missing
exec bits on generated hook scripts. That was wrong — a chmod-only
fix (4f7b674) still failed CI. The actual bug was output_with_stdin
unconditionally propagating BrokenPipe from write_all when the child
exits before the parent finishes writing stdin.

Updated per gaebal-gajae's direction: actual fix, hygiene hardening,
and regression guard are now clearly separated. Added a meta-lesson
about Broken pipe ambiguity in fork/exec paths so future investigators
don't cargo-cult the same wrong first theory.
2026-04-08 15:53:26 +09:00
YeonGyu-Kim
172a2ad50a fix(plugins): chmod +x generated hook scripts + tolerate BrokenPipe in stdin write — closes ROADMAP #25 hotfix lane
Two bugs found in the plugin hook test harness that together caused
Linux CI to fail on 'hooks::tests::collects_and_runs_hooks_from_enabled_plugins'
with 'Broken pipe (os error 32)'. Three reproductions plus one rerun
failure on main today: 24120271422, 24120538408, 24121392171.

Root cause 1 (chmod, defense-in-depth): write_hook_plugin writes
pre.sh/post.sh/failure.sh via fs::write without setting the execute
bit. While the runtime hook runner invokes hooks via 'sh <path>' (so
the script file does not strictly need +x), missing exec perms can
cause subtle fork/exec races on Linux in edge cases.

Root cause 2 (the actual CI failure): output_with_stdin unconditionally
propagated write_all errors on the child's stdin pipe, including
BrokenPipe. A hook script that runs to completion in microseconds
(e.g. a one-line printf) can exit and close its stdin before the parent
finishes writing the JSON payload. Linux pipes surface this as EPIPE
immediately; macOS pipes happen to buffer the small payload, so the
race only shows on ubuntu CI runners. The parent's write_all raised
BrokenPipe, which output_with_stdin returned as Err, which run_command
classified as 'failed to start', making the test assertion fail.

Fix: (a) make_executable helper sets mode 0o755 via PermissionsExt on
each generated hook script, with a #[cfg(unix)] gate and a no-op
#[cfg(not(unix))] branch. (b) output_with_stdin now matches the
write_all result and swallows BrokenPipe specifically (the child still
ran; wait_with_output still captures stdout/stderr/exit code), while
propagating all other write errors. (c) New regression guard
generated_hook_scripts_are_executable under #[cfg(unix)] asserts each
generated .sh file has at least one execute bit set.

Surgical scope per gaebal-gajae's direction: chmod + pipe tolerance +
regression guard only. The deeper plugin-test sealing pass for ROADMAP
#25 + #27 stays in gaebal-gajae's OMX lane.

Verification:
- cargo test --release -p plugins → 35 passing, 0 failing
- cargo fmt -p plugins → clean
- cargo clippy -p plugins -- -D warnings → clean

Co-authored-by: gaebal-gajae <gaebal-gajae@layofflabs.com>
2026-04-08 15:48:20 +09:00
YeonGyu-Kim
647ff379a4 docs(roadmap): file dev/rust plugin-validation host-home leak as backlog #27
Filing per gaebal-gajae's status summary at message 1491322807026454579
in #clawcode-building-in-public, with corrected scope after re-running
`cargo test -p rusty-claude-cli` against main HEAD (79da4b8): the 11
deterministic failures only reproduce on dev/rust, not main, so this is
a dev/rust catchup item rather than a main regression.

Two-layered root cause documented:
1. dev/rust `parse_args` eagerly validates user plugin hook scripts
   exist on disk before returning a CliAction
2. dev/rust test harness does not redirect $HOME/XDG_CONFIG_HOME to a
   fixture (no `env_lock` equivalent — main has 30+ env_lock hits, dev
   has zero)

Together they make dev/rust `cargo test -p rusty-claude-cli` fail on
any clean clone whose owner has a half-installed user plugin in
~/.claude/plugins/installed/. main has both the env_lock test isolation
AND the parse_args/hook-validation decoupling already; dev/rust is just
behind on the merge train.

Action block in #27 spells out backporting env_lock + the parse_args
decoupling so the next dev/rust release picks this up.
2026-04-08 15:30:04 +09:00
YeonGyu-Kim
79da4b8a63 docs(roadmap): record hooks test flake as P2 backlog item #25
Linux CI keeps tripping over
`plugins::hooks::tests::collects_and_runs_hooks_from_enabled_plugins`
with `Broken pipe (os error 32)` when the hook runner tries to spawn a
child shell script that was written by `write_hook_plugin` without the
execute bit set. Fails on first attempt, passes on rerun (observed in CI
runs 24120271422 and 24120538408). Passes consistently on macOS.

Since issues are disabled on the repo, recording as ROADMAP backlog
item #25 in the Immediate Backlog P2 cluster next to the related plugin
lifecycle flake at #24. Action block spells out the chmod +755 fix in
`write_hook_plugin` plus the regression guard.
2026-04-08 15:10:13 +09:00
YeonGyu-Kim
7d90283cf9 docs(roadmap): record cascade-masking pinpoint under green-ness contract (#9)
Concrete follow-up captured from today's dogfood session:

A single hung test (oversized-request preflight, 6 minutes per attempt
after `be561bf` silently swallowed count_tokens errors) crashed the
`cargo test --workspace` job before downstream crates could run, hiding
6 separate pre-existing CLI regressions until `8c6dfe5` + `5851f2d`
restored the fast-fail path.

Two new acceptance criteria for #9:
- per-test timeouts in CI so one hang cannot mask other failures
- distinguish `test.hung` from generic test failures in worker reports
2026-04-08 15:03:30 +09:00