Compare commits

...

13 Commits

Author SHA1 Message Date
Bellman
e17098cc70 fix: resolve cc2 wrapper tools from script root (#3176) 2026-05-28 08:36:16 +09:00
Bellman
e17936158a fix: make cc2 validator board read errors concise (#3175) 2026-05-28 08:06:28 +09:00
Bellman
760e69675c fix: make cc2 generator missing source error concise (#3174) 2026-05-28 07:36:47 +09:00
Bellman
193f11171a fix: reject extra roadmap helper paths (#3173) 2026-05-28 06:34:44 +09:00
Bellman
f11ac23e1f fix: add roadmap next-id help handling (#3172) 2026-05-28 06:06:42 +09:00
Bellman
c4770e6571 docs(roadmap): add #811 json error envelope nontty hangs (#3171) 2026-05-28 05:35:57 +09:00
Bellman
b0e94c996b docs(roadmap): add #810 json stdout warning contamination (#3169) 2026-05-28 05:05:17 +09:00
Bellman
85d63b071c docs(roadmap): add #809 help mcp plugin json hangs (#3168) 2026-05-28 04:30:30 +09:00
Bellman
db81598525 docs(roadmap): add #808 control-plane json hangs (#3166) 2026-05-28 03:32:15 +09:00
Bellman
86f45a11ef docs(roadmap): add #807 model json hang (#3163) 2026-05-28 01:33:01 +09:00
YeonGyu-Kim
87b7e74770 fix(#806): plugins show <not-found> in text mode returned empty success instead of error 2026-05-27 22:34:10 +09:00
Bellman
ae6a207d4e fix(#3129): handle trailing json format for diff errors (#3161)
Keep malformed diff invocations with trailing JSON format flags on the parser error path and lock the contract with focused output-format regressions.

Constraint: Do not touch tracked .omx state files.

Rejected: Repeating direct binary smoke loops | local auth/provider configuration intercepts those invocations and obscures parser behavior.

Confidence: high

Scope-risk: narrow

Tested: git diff --check; cargo fmt --check; cargo test -p rusty-claude-cli diff_extra_args_have_typed_error_kind_and_hint_766 --test output_format_contract; cargo test -p rusty-claude-cli diff_trailing_json_after_malformed_args_is_bounded_json_3129 --test output_format_contract; cargo test -p rusty-claude-cli diff_non_git_dir_has_error_kind_and_hint_801 --test output_format_contract
2026-05-27 21:57:26 +09:00
YeonGyu-Kim
efd34c151a fix(#805): skills show <not-found> in text mode silently returned empty success instead of error 2026-05-27 21:05:41 +09:00
9 changed files with 166 additions and 20 deletions

View File

@@ -7774,3 +7774,12 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
803. **`claw agents list --bogus`, `skills list --bogus`, and `plugins list --bogus` in text mode silently returned empty success** — dogfooded 2026-05-27 on `fcebf644`. The JSON-mode flag guards added in #792/#793 only covered the JSON branch; the text-mode path through `handle_agents_slash_command`, `handle_skills_slash_command`, and `print_plugins` still passed flag-shaped tokens as substring filters. Fix: added flag-prefix guards to all three text-mode list handlers (agents and skills in `commands/src/lib.rs`, plugins in `main.rs print_plugins`). Also removed the now-redundant JSON-only guard from print_plugins (the early guard catches both modes). Updated `plugins_list_flag_shaped_filter_returns_unknown_option_793` test to check stderr. 62 CLI contract tests pass. [SCOPE: claw-code]
804. **`claw agents show <name> <extra>` and `claw skills show <name> <extra>` in text mode returned wrong `agent_not_found`/silent empty instead of catching extra args** — dogfooded 2026-05-27 on `bad1b97f`. Parity gap with JSON-mode fix #796: the text-mode show handlers in `commands/src/lib.rs` still used single-split `split_once(' ')` without checking for spaces in the extracted name. Fix: added `contains(' ')` guard to both text-mode show arms; extra tokens now return `unexpected extra arguments` with usage hint. 62 CLI contract tests pass. [SCOPE: claw-code]
805. **`claw skills show <not-found>` in text mode silently returned "No skills found." instead of an error** — dogfooded 2026-05-27 on `2c3c0f60`. The text-mode show handler in `handle_skills_slash_command` returned `render_skills_report(&matched)` with an empty vec instead of checking for empty match and returning an error. JSON mode already returned `skill_not_found` since #706. Fix: added `matched.is_empty()` guard with `skill_not_found` error + `\n` hint suggesting `claw skills list`. 62 CLI contract tests pass. [SCOPE: claw-code]
806. **`claw plugins show <not-found>` in text mode returned "No plugins installed." instead of an error** — dogfooded 2026-05-27 on `ae6a207d`. The text-mode path in `print_plugins` printed `payload.message` (the full list render) without checking if the requested plugin existed. JSON mode correctly returned `plugin_not_found`. Fix: added show-action filtering + not-found guard to text-mode path; added `starts_with("plugin_not_found:")` arm to classifier for the new error prefix. 63 CLI contract tests pass. [SCOPE: claw-code]
807. **`claw models` / `claw model` with `--output-format json` hang with zero stdout instead of returning bounded model discovery/help JSON or a typed unsupported response** — dogfooded 2026-05-27 on `ae6a207` while checking docs/usage model-alias surface after PR #3162 opened. Both `cargo run -q -p rusty-claude-cli -- models --output-format json` and the actual rebuilt `./rust/target/debug/claw models --output-format json` timed out under an 8s outer timeout with stdout `0`; stderr only contained config deprecation warnings. The same silent timeout reproduced for `models help --output-format json`, `model --output-format json`, and `model help --output-format json`. **Required fix shape:** (a) make `model(s)` help/list/discovery commands return bounded stdout JSON without entering prompt/provider/auth paths; (b) if the command is unsupported, return a standard typed JSON error envelope with `error_kind`, non-null `hint`, and `message`; (c) ensure docs model-alias tables and CLI model discovery surfaces do not diverge; (d) add regression coverage for `models --output-format json`, `models help --output-format json`, `model --output-format json`, and `model help --output-format json` proving they do not hang or emit zero-byte stdout. **Why this matters:** model selection is a setup/control-plane surface. If the natural model discovery commands hang silently, claws cannot verify aliases like `qwen-max` / `qwen-plus`, distinguish unsupported command spelling from provider startup, or safely guide users during first-run model setup. Source: gaebal-gajae 13:30/14:00 dogfood probe; GitHub issue creation was blocked by API rate limit, so the finding was recorded directly in ROADMAP.
808. **Control-plane commands `claw config`, `claw settings`, `claw status`, and `claw doctor` with `--output-format json` hang with zero stdout instead of returning bounded JSON/help or a typed unsupported envelope** — dogfooded 2026-05-27 on `86f45a1` after ROADMAP #807 landed. Each of `./rust/target/debug/claw config --output-format json`, `config help --output-format json`, `settings --output-format json`, `settings help --output-format json`, `status --output-format json`, and `doctor --output-format json` timed out under an 8s outer timeout with stdout `0`; stderr only contained the local deprecated `enabledPlugins` settings warning. **Required fix shape:** keep non-interactive control-plane/info commands out of prompt/provider startup paths; return bounded JSON stdout for supported status/config/help surfaces, or a standard typed JSON error envelope with `error_kind`, non-null `hint`, and `message` for unsupported spellings; add timeout/nonzero-stdout regression coverage for the six repro commands. **Why this matters:** claws and users need first-run diagnostics/config/status surfaces that are safe to call from scripts. Silent hangs make setup triage indistinguishable from provider startup, auth, or model discovery failures. Source: gaebal-gajae 17:00 dogfood probe; rechecked 17:30 after `cargo build --manifest-path rust/Cargo.toml -p rusty-claude-cli` produced `claw --version` Git SHA `23a7de6`, and the same timeout reproduced for current HOME and a clean `HOME=/tmp/claw-clean-home-1730` (clean HOME produced rc 124, stdout 0, stderr 0 for `config`, `status`, and `doctor`). [SCOPE: claw-code]
809. **Top-level help/version/MCP/plugin JSON spellings hang with zero stdout in trailing `--output-format json` form instead of returning bounded JSON/help or typed unsupported envelopes** — dogfooded 2026-05-27 on rebuilt main `db81598` (`cargo build --manifest-path rust/Cargo.toml -p rusty-claude-cli`; `claw --version` Git SHA `db81598`). `help --output-format json`, `version --output-format json`, `mcp --output-format json`, `mcp help --output-format json`, `plugins --output-format json`, and `plugins help --output-format json` each timed out under an 8s outer timeout with stdout `0`; stderr only contained the local deprecated `enabledPlugins` settings warning. Leading global-style probes (`--help --output-format json`, `--version --output-format json`) fail immediately as `[error-kind: cli_parse] unknown option`, so the hang is again in the trailing subcommand-style routing/startup path. **Required fix shape:** treat help/version/MCP/plugin discovery surfaces as bounded non-interactive control-plane commands; either return JSON help/list/version payloads or standard typed JSON unsupported envelopes with `error_kind`, non-null `hint`, and `message`; add timeout/nonzero-stdout regression coverage for the six trailing repro commands and parser-envelope coverage for leading global-style spellings. **Why this matters:** claws need safe scriptable help/version/plugin/MCP discovery before provider/session startup; silent hangs hide whether a command is unsupported, misparsed, or initializing runtime state. Source: gaebal-gajae 19:00 dogfood probe. [SCOPE: claw-code]
810. **TTY JSON success for `config`/`plugins --output-format json` contaminates stdout with deprecated-settings warnings before the JSON object** — dogfooded 2026-05-27 on rebuilt main `db81598` after #809. Under pseudo-TTY (`script -q -c "./rust/target/debug/claw config --output-format json"` and `plugins --output-format json`), the commands return rc `0` and bounded JSON, but stdout begins with `warning: /home/bellman/.claw/settings.json: field "enabledPlugins" is deprecated ...` before the JSON object (`first_json_index=121`). Parsing succeeds only after manually stripping the warning/prefix; raw stdout is not valid JSON. **Required fix shape:** in JSON mode, keep diagnostics/warnings on stderr or include structured warning fields inside the JSON envelope, but never prepend human warnings to stdout; add regression coverage that raw stdout from JSON commands parses from byte 0 under TTY and non-TTY modes. **Why this matters:** even when the TTY path avoids the hang from #807/#808/#809, claws and scripts still cannot safely `json.loads(stdout)` if configuration warnings are mixed into stdout. Source: gaebal-gajae 20:00 pseudo-TTY dogfood probe. [SCOPE: claw-code]
811. **Previously typed JSON error/list surfaces hang in plain non-TTY trailing `--output-format json` form instead of emitting their JSON envelopes** — dogfooded 2026-05-27 on rebuilt main `b0e94c9` after #810. In plain non-TTY automation, `agents list --bogus --output-format json`, `skills show does-not-exist --output-format json`, `plugins show does-not-exist --output-format json`, `diff --output-format json`, `sessions show does-not-exist --output-format json`, and `resume bogus --output-format json` each timed out under an 8s outer timeout with stdout `0`; stderr only contained the local deprecated `enabledPlugins` settings warning. Several of these surfaces had prior roadmap fixes for typed JSON/text envelopes, so this is a regression-class scriptability gap: the command-specific envelope may exist, but plain non-TTY trailing JSON invocation routes into interactive startup before reaching it. **Required fix shape:** ensure trailing `--output-format json` is honored before any interactive/provider/session startup for error/list surfaces; add plain non-TTY timeout regression coverage that asserts raw stdout is a parseable typed JSON envelope for the six repro commands, including `error_kind`, non-null `hint`, and `message` where applicable. **Why this matters:** claws primarily invoke CLI checks from non-TTY automation; a fix that only works in manual/TTY mode still leaves JSON error handling unusable for agents. Source: gaebal-gajae 20:30 dogfood probe. [SCOPE: claw-code]

View File

@@ -2606,6 +2606,13 @@ pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
.into_iter()
.filter(|s| s.name.to_lowercase() == name_raw)
.collect();
// #805: text-mode show must return an error when skill not found (parity with JSON)
if matched.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("skill '{name_raw}' not found\nRun `claw skills list` to see available skills."),
));
}
Ok(render_skills_report(&matched))
}
Some("install") => Ok(render_skills_usage(Some("install"))),

View File

@@ -335,7 +335,7 @@ fn classify_error_kind(message: &str) -> &'static str {
"unknown_agents_subcommand"
} else if message.starts_with("agent not found:") {
"agent_not_found"
} else if message.contains("is not installed") {
} else if message.contains("is not installed") || message.starts_with("plugin_not_found:") {
"plugin_not_found"
} else if message.contains("plugin source") && message.contains("was not found") {
// #794: `plugins install /nonexistent/path` → "plugin source ... was not found"
@@ -1225,10 +1225,10 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
// `git diff`). No session needed to inspect the working tree.
"diff" => {
if rest.len() > 1 {
return Err(format!(
"unexpected extra arguments after `claw diff`: {}\nUsage: claw diff",
rest[1..].join(" ")
));
// #3129: keep malformed `diff ... --output-format json` on the
// parser/error path, not the prompt/TUI fallback. The newline
// before Usage is part of the JSON hint contract.
return Err(unexpected_diff_args_error(&rest[1..]));
}
Ok(CliAction::Diff { output_format })
}
@@ -1625,6 +1625,13 @@ fn removed_auth_surface_error(command_name: &str) -> String {
)
}
fn unexpected_diff_args_error(extra: &[String]) -> String {
format!(
"unexpected extra arguments after `claw diff`: {}\nUsage: claw diff",
extra.join(" ")
)
}
fn parse_acp_args(args: &[String], output_format: CliOutputFormat) -> Result<CliAction, String> {
match args {
[] => Ok(CliAction::Acp { output_format }),
@@ -6399,7 +6406,28 @@ impl LiveCli {
}
let payload = plugins_command_payload_for(&cwd, action, target)?;
match output_format {
CliOutputFormat::Text => println!("{}", payload.message),
CliOutputFormat::Text => {
// #806: text-mode show must return error when plugin not found (parity with JSON)
let action_str = action.unwrap_or("list");
if matches!(action_str, "show" | "info" | "describe") {
if let Some(name) = target {
let needle = name.to_lowercase();
let found = payload.plugins.iter().any(|p| {
p.get("id")
.and_then(|v| v.as_str())
.map(|id| id.to_lowercase() == needle)
.unwrap_or(false)
});
if !found {
return Err(format!(
"plugin_not_found: plugin '{}' not found\nRun `claw plugins list` to see available plugins.",
name
).into());
}
}
}
println!("{}", payload.message);
}
CliOutputFormat::Json => {
let action_str = action.unwrap_or("list");
// #743/#420: plugins help must return a usage envelope matching agents/mcp/skills help shape.

View File

@@ -1916,36 +1916,95 @@ fn login_logout_removed_subcommands_have_error_kind_and_hint_765() {
fn diff_extra_args_have_typed_error_kind_and_hint_766() {
// #766: `claw diff --bogus` returned error_kind:"unknown" + hint:null.
// `diff` takes no arguments; extra args were unclassified with no remediation.
let root = unique_temp_dir("diff-extra-args-766");
let root = git_temp_dir("diff-extra-args-766");
assert_diff_unexpected_extra_args_json(
&root,
&["--output-format", "json", "diff", "--bogus"],
"claw diff --bogus",
);
}
#[test]
fn diff_trailing_json_after_malformed_args_is_bounded_json_3129() {
// #3129: when --output-format json appeared after malformed `diff` args,
// the parser fell through to the interactive/prompt path and emitted zero
// JSON stdout. These forms must fail before any provider or TUI path starts.
let root = git_temp_dir("diff-trailing-json-3129");
for (args, label) in [
(
&["diff", "--bogus-flag", "--output-format", "json"][..],
"claw diff --bogus-flag --output-format json",
),
(
&["diff", "does-not-exist", "--output-format", "json"][..],
"claw diff does-not-exist --output-format json",
),
(
&[
"diff",
"--cached",
"--bogus-flag",
"--output-format",
"json",
][..],
"claw diff --cached --bogus-flag --output-format json",
),
] {
assert_diff_unexpected_extra_args_json(&root, args, label);
}
}
fn git_temp_dir(prefix: &str) -> PathBuf {
let root = unique_temp_dir(prefix);
fs::create_dir_all(&root).expect("temp dir should exist");
// Need a git repo for diff to parse past arg validation
// Need a git repo so `diff` reaches argument validation before git checks.
std::process::Command::new("git")
.args(["init", "-q"])
.current_dir(&root)
.output()
.ok();
.expect("git init should launch");
root
}
let output = run_claw(&root, &["--output-format", "json", "diff", "--bogus"], &[]);
fn assert_diff_unexpected_extra_args_json(root: &Path, args: &[&str], label: &str) {
let output = run_claw(root, args, &[]);
assert!(
!output.status.success(),
"claw diff --bogus should exit non-zero"
"{label} should exit non-zero; stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
output.stdout.is_empty(),
"{label} should not enter the spinner/prompt path; stdout:\n{}",
String::from_utf8_lossy(&output.stdout)
);
let stderr = String::from_utf8_lossy(&output.stderr);
let json_line = stderr
.lines()
.find(|l| l.trim_start().starts_with('{'))
.expect("stderr should contain a JSON error envelope");
.unwrap_or_else(|| {
panic!("{label} stderr should contain a JSON error envelope; stderr:\n{stderr}")
});
let parsed: serde_json::Value =
serde_json::from_str(json_line).expect("error envelope should be valid JSON");
assert_eq!(
parsed["error_kind"], "unexpected_extra_args",
"claw diff --bogus must return error_kind:unexpected_extra_args (#766)"
"{label} must return error_kind:unexpected_extra_args"
);
let hint = parsed["hint"].as_str().unwrap_or("");
assert!(
!hint.is_empty(),
"claw diff --bogus must return non-null hint (#766), got: {hint:?}"
"{label} must return non-null hint, got: {hint:?}"
);
assert!(
parsed["message"]
.as_str()
.is_some_and(|message| !message.is_empty()),
"{label} must return non-empty message"
);
}

View File

@@ -26,11 +26,13 @@ def main(argv: list[str] | None = None) -> int:
args = parser.parse_args(argv)
repo_root = args.repo_root.resolve()
script_root = Path(__file__).resolve().parent
tool_root = script_root.parent
board_json = repo_root / args.board_json
board_md = repo_root / args.board_md
generator = repo_root / "scripts" / "generate_cc2_board.py"
validator = repo_root / "scripts" / "validate_cc2_board.py"
renderer = repo_root / ".omx" / "cc2" / "render_board_md.py"
generator = script_root / "generate_cc2_board.py"
validator = script_root / "validate_cc2_board.py"
renderer = tool_root / ".omx" / "cc2" / "render_board_md.py"
if args.command == "generate":
rc = run([sys.executable, str(generator), "--repo-root", str(repo_root), "--out-dir", str(board_json.parent)], repo_root)

View File

@@ -504,7 +504,11 @@ def main() -> int:
repo_root = args.repo_root.resolve()
out_dir = args.out_dir or (repo_root / ".omx" / "cc2")
out_dir.mkdir(parents=True, exist_ok=True)
try:
board = build_board(repo_root)
except FileNotFoundError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
board_json = out_dir / "board.json"
board_md = out_dir / "board.md"
board_json.write_text(json.dumps(board, indent=2, sort_keys=True) + "\n", encoding="utf-8")

View File

@@ -11,6 +11,7 @@ set -euo pipefail
MIN_ID=723
ROADMAP="ROADMAP.md"
ROADMAP_PATH_SEEN=0
while [[ $# -gt 0 ]]; do
case "$1" in
@@ -31,7 +32,12 @@ while [[ $# -gt 0 ]]; do
exit 2
;;
*)
if [[ "$ROADMAP_PATH_SEEN" -ne 0 ]]; then
echo "error: unexpected extra ROADMAP path: $1" >&2
exit 2
fi
ROADMAP="$1"
ROADMAP_PATH_SEEN=1
shift
;;
esac

View File

@@ -17,7 +17,31 @@
# and resolve any append collision at git-push time.
set -euo pipefail
ROADMAP="${1:-ROADMAP.md}"
ROADMAP="ROADMAP.md"
ROADMAP_PATH_SEEN=0
while [[ $# -gt 0 ]]; do
case "$1" in
--help|-h)
sed -n '2,15p' "$0" | sed 's/^# //; s/^#//'
exit 0
;;
--*)
echo "error: unknown option: $1" >&2
exit 2
;;
*)
if [[ "$ROADMAP_PATH_SEEN" -ne 0 ]]; then
echo "error: unexpected extra ROADMAP path: $1" >&2
exit 2
fi
ROADMAP="$1"
ROADMAP_PATH_SEEN=1
shift
;;
esac
done
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
CHECKER="$SCRIPT_DIR/roadmap-check-ids.sh"

View File

@@ -44,7 +44,14 @@ def main() -> int:
args = parser.parse_args()
repo_root = args.repo_root.resolve()
board_path = args.board or (repo_root / ".omx" / "cc2" / "board.json")
try:
board = json.loads(board_path.read_text(encoding="utf-8"))
except FileNotFoundError:
print(f"error: board not found at {board_path}")
return 1
except json.JSONDecodeError as exc:
print(f"error: invalid board JSON at {board_path}: {exc}")
return 1
errors: list[str] = []
ids = set()
for index, item in enumerate(board.get("items", []), 1):