mirror of
https://github.com/instructkr/claude-code.git
synced 2026-06-04 11:36:44 +00:00
fix: load common instruction files and typed unknown commands
This commit is contained in:
12
ROADMAP.md
12
ROADMAP.md
@@ -7827,12 +7827,16 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
|
|||||||
|
|
||||||
**Acceptance.** `claw --output-format json plugins list --` exits 1, stdout parses from byte 0 as the existing JSON error envelope, stderr is empty, and text mode still reports the parse error to stderr. [SCOPE: claw-code]
|
**Acceptance.** `claw --output-format json plugins list --` exits 1, stdout parses from byte 0 as the existing JSON error envelope, stderr is empty, and text mode still reports the parse error to stderr. [SCOPE: claw-code]
|
||||||
|
|
||||||
818. **`AGENTS.md` and `.claude/CLAUDE.md` silently omitted from instruction file cascade** — dogfooded 2026-05-29 08:00. When a repo contains `AGENTS.md` (OpenAI Codex / multi-agent convention) or `.claude/CLAUDE.md` (scoped Claude Code convention), claw-code does not load either file as part of the instruction/context cascade on startup. Users following either convention discover this only by noticing their persona/context instructions have no effect — no warning, no missing-file diagnostic, no documentation note. This is a friction gap for any team migrating to or simultaneously using claw-code alongside Claude Code or Codex workflows, since the two most common non-CLAUDE.md instruction files are silently ignored.
|
818. **DONE — `AGENTS.md` and `.claude/CLAUDE.md` silently omitted from instruction file cascade** — dogfooded 2026-05-29 08:00. When a repo contains `AGENTS.md` (OpenAI Codex / multi-agent convention) or `.claude/CLAUDE.md` (scoped Claude Code convention), claw-code does not load either file as part of the instruction/context cascade on startup. Users following either convention discover this only by noticing their persona/context instructions have no effect — no warning, no missing-file diagnostic, no documentation note. This is a friction gap for any team migrating to or simultaneously using claw-code alongside Claude Code or Codex workflows, since the two most common non-CLAUDE.md instruction files are silently ignored.
|
||||||
|
|
||||||
**Required fix shape.** Add `AGENTS.md` (project root) and `.claude/CLAUDE.md` (`.claude/` subdirectory) to the instruction file cascade that already loads `CLAUDE.md`. Apply the same merge-and-precedence semantics as existing instruction files. Log a debug trace (not stderr noise) when either file is loaded. Add test coverage: a fixture repo with `AGENTS.md` only, `.claude/CLAUDE.md` only, and both present alongside `CLAUDE.md` should each have the relevant content visible in the resolved instruction context.
|
**Required fix shape.** Add `AGENTS.md` (project root) and `.claude/CLAUDE.md` (`.claude/` subdirectory) to the instruction file cascade that already loads `CLAUDE.md`. Apply the same merge-and-precedence semantics as existing instruction files. Log a debug trace (not stderr noise) when either file is loaded. Add test coverage: a fixture repo with `AGENTS.md` only, `.claude/CLAUDE.md` only, and both present alongside `CLAUDE.md` should each have the relevant content visible in the resolved instruction context.
|
||||||
|
|
||||||
**Acceptance.** `claw` launched in a repo containing `AGENTS.md` or `.claude/CLAUDE.md` loads those files into the instruction context. No warning emitted for absent optional files. Existing `CLAUDE.md`-only repos unaffected. PR #3195. [SCOPE: claw-code]
|
**Acceptance.** `claw` launched in a repo containing `AGENTS.md` or `.claude/CLAUDE.md` loads those files into the instruction context. No warning emitted for absent optional files. Existing `CLAUDE.md`-only repos unaffected. PR #3195. [SCOPE: claw-code]
|
||||||
|
|
||||||
|
**Fix applied.** The instruction cascade now loads `AGENTS.md` and `.claude/CLAUDE.md` from the same ancestor walk that already loads `CLAUDE.md`, `CLAUDE.local.md`, `.claw/CLAUDE.md`, and `.claw/instructions.md`, preserving the existing merge/dedupe semantics and avoiding warnings for absent optional files.
|
||||||
|
|
||||||
|
**Verification.** `cargo test --manifest-path rust/Cargo.toml -p runtime discovers_agents_markdown_instruction_file -- --nocapture`; `cargo test --manifest-path rust/Cargo.toml -p runtime discovers_scoped_dot_claude_claude_markdown_instruction_file -- --nocapture`; `cargo test --manifest-path rust/Cargo.toml -p runtime discovers_claude_agents_and_dot_claude_instruction_files_together -- --nocapture`.
|
||||||
|
|
||||||
819. **`claw --output-format json export --session <missing>` writes JSON error envelope to stderr, stdout empty** — dogfooded 2026-05-29 09:30 on `main` `37a9a543`. `claw --output-format json export --session does-not-exist` exits rc=1 with stdout length 0 and the full JSON error envelope on stderr: `{"action":"abort","error":"session not found: does-not-exist","error_kind":"session_not_found",...}`. This is the same channel-routing inconsistency class as #817 (plugins list trailing-dash, fixed in #3194): handled errors in JSON mode should go to stdout, not stderr, so machine consumers can parse the envelope from stdout byte 0 regardless of which surface triggered the error.
|
819. **`claw --output-format json export --session <missing>` writes JSON error envelope to stderr, stdout empty** — dogfooded 2026-05-29 09:30 on `main` `37a9a543`. `claw --output-format json export --session does-not-exist` exits rc=1 with stdout length 0 and the full JSON error envelope on stderr: `{"action":"abort","error":"session not found: does-not-exist","error_kind":"session_not_found",...}`. This is the same channel-routing inconsistency class as #817 (plugins list trailing-dash, fixed in #3194): handled errors in JSON mode should go to stdout, not stderr, so machine consumers can parse the envelope from stdout byte 0 regardless of which surface triggered the error.
|
||||||
|
|
||||||
**Required fix shape.** Align `export --session <missing>` error routing with the inventory surfaces fixed in #817: in JSON mode, write the `session_not_found` error envelope to stdout (rc=1) and keep stderr empty. Preserve text-mode behavior (stderr message). Add regression coverage asserting rc=1, stdout parseable JSON with `error_kind:"session_not_found"`, and empty stderr.
|
**Required fix shape.** Align `export --session <missing>` error routing with the inventory surfaces fixed in #817: in JSON mode, write the `session_not_found` error envelope to stdout (rc=1) and keep stderr empty. Preserve text-mode behavior (stderr message). Add regression coverage asserting rc=1, stdout parseable JSON with `error_kind:"session_not_found"`, and empty stderr.
|
||||||
@@ -7877,12 +7881,16 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
|
|||||||
|
|
||||||
**Acceptance.** `claw --output-format json foobar` exits 1, stdout `error_kind:"command_not_found"`, stderr empty, no Anthropic call. Typo with suggestions (`claw statuz`) also gets `command_not_found` plus `hint` with suggestions. [SCOPE: claw-code]
|
**Acceptance.** `claw --output-format json foobar` exits 1, stdout `error_kind:"command_not_found"`, stderr empty, no Anthropic call. Typo with suggestions (`claw statuz`) also gets `command_not_found` plus `hint` with suggestions. [SCOPE: claw-code]
|
||||||
|
|
||||||
826. **Multi-word unknown subcommand still falls through to `missing_credentials`** — dogfooded 2026-05-29 14:38 on `main` `70d64be0`. After #825 fixed single-word unknown subcommands, multi-word invocations (`claw foobar baz`) are still undetected: the `looks_like_subcommand_typo` guard only fires when `rest.len() == 1`. When there are two or more positional args, the first word is treated as a prompt and all args join into a prompt string → provider startup → `missing_credentials`. Same misleading-error class as #825 but for multi-word cases.
|
826. **DONE — Multi-word unknown subcommand still falls through to `missing_credentials`** — dogfooded 2026-05-29 14:38 on `main` `70d64be0`. After #825 fixed single-word unknown subcommands, multi-word invocations (`claw foobar baz`) are still undetected: the `looks_like_subcommand_typo` guard only fires when `rest.len() == 1`. When there are two or more positional args, the first word is treated as a prompt and all args join into a prompt string → provider startup → `missing_credentials`. Same misleading-error class as #825 but for multi-word cases.
|
||||||
|
|
||||||
**Required fix shape.** Extend the command-not-found guard to also fire when `rest.len() > 1` and `rest[0]` passes `looks_like_subcommand_typo` but does not match any known subcommand. The multi-arg case should also emit `command_not_found` — with a note that if literal multi-word prompt was intended, use `claw prompt <text>` or `echo 'text' | claw`.
|
**Required fix shape.** Extend the command-not-found guard to also fire when `rest.len() > 1` and `rest[0]` passes `looks_like_subcommand_typo` but does not match any known subcommand. The multi-arg case should also emit `command_not_found` — with a note that if literal multi-word prompt was intended, use `claw prompt <text>` or `echo 'text' | claw`.
|
||||||
|
|
||||||
**Acceptance.** `claw --output-format json foobar baz` exits 1, stdout `error_kind:"command_not_found"`, stderr empty, no provider startup. `claw "write a haiku"` (valid prompt passthrough) is unaffected. [SCOPE: claw-code]
|
**Acceptance.** `claw --output-format json foobar baz` exits 1, stdout `error_kind:"command_not_found"`, stderr empty, no provider startup. `claw "write a haiku"` (valid prompt passthrough) is unaffected. [SCOPE: claw-code]
|
||||||
|
|
||||||
|
**Fix applied.** JSON-mode command-shaped unknown subcommands now emit `command_not_found:` before provider startup even when additional tokens follow. Text-mode multi-word prompt shorthand remains available, but JSON automation no longer turns `claw --output-format json foobar baz` into a credential-gated prompt request.
|
||||||
|
|
||||||
|
**Verification.** `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli multi_word_unknown_subcommand_json_emits_command_not_found_826 -- --nocapture`; `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli unknown_subcommand_json_emits_command_not_found -- --nocapture`; direct probe `cargo run --manifest-path rust/Cargo.toml -q -p rusty-claude-cli -- --output-format json foobar baz`.
|
||||||
|
|
||||||
827. **DONE — `--resume <session> /unknown-slash-command` emits `error_kind:"unknown"` instead of a typed kind** — dogfooded 2026-05-29 14:58 on `main` `d47b0151`. `claw --resume latest --output-format json /bogus` exits rc=2 with `{"error_kind":"unknown",...}` on stdout (correct channel, correct rc) but the opaque `"unknown"` kind gives machine consumers no way to distinguish "unrecognized slash command" from other error classes. The error has a useful `hint` with suggestions, but the `error_kind` field is `"unknown"` across all unrecognized resume slash commands.
|
827. **DONE — `--resume <session> /unknown-slash-command` emits `error_kind:"unknown"` instead of a typed kind** — dogfooded 2026-05-29 14:58 on `main` `d47b0151`. `claw --resume latest --output-format json /bogus` exits rc=2 with `{"error_kind":"unknown",...}` on stdout (correct channel, correct rc) but the opaque `"unknown"` kind gives machine consumers no way to distinguish "unrecognized slash command" from other error classes. The error has a useful `hint` with suggestions, but the `error_kind` field is `"unknown"` across all unrecognized resume slash commands.
|
||||||
|
|
||||||
**Required fix shape.** Introduce a typed `error_kind` for unrecognized slash commands (e.g. `unknown_slash_command` or `command_not_found`). Update the JSON emit in the `--resume` unknown-command handler to use the typed kind. Add regression coverage asserting the typed kind.
|
**Required fix shape.** Introduce a typed `error_kind` for unrecognized slash commands (e.g. `unknown_slash_command` or `command_not_found`). Update the JSON emit in the `--resume` unknown-command handler to use the typed kind. Add regression coverage asserting the typed kind.
|
||||||
|
|||||||
@@ -240,8 +240,10 @@ fn discover_instruction_files(cwd: &Path) -> std::io::Result<Vec<ContextFile>> {
|
|||||||
for dir in directories {
|
for dir in directories {
|
||||||
for candidate in [
|
for candidate in [
|
||||||
dir.join("CLAUDE.md"),
|
dir.join("CLAUDE.md"),
|
||||||
|
dir.join("AGENTS.md"),
|
||||||
dir.join("CLAUDE.local.md"),
|
dir.join("CLAUDE.local.md"),
|
||||||
dir.join(".claw").join("CLAUDE.md"),
|
dir.join(".claw").join("CLAUDE.md"),
|
||||||
|
dir.join(".claude").join("CLAUDE.md"),
|
||||||
dir.join(".claw").join("instructions.md"),
|
dir.join(".claw").join("instructions.md"),
|
||||||
] {
|
] {
|
||||||
push_context_file(&mut files, candidate)?;
|
push_context_file(&mut files, candidate)?;
|
||||||
@@ -636,6 +638,63 @@ mod tests {
|
|||||||
fs::remove_dir_all(root).expect("cleanup temp dir");
|
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discovers_agents_markdown_instruction_file() {
|
||||||
|
let root = temp_dir();
|
||||||
|
fs::create_dir_all(&root).expect("root dir");
|
||||||
|
fs::write(root.join("AGENTS.md"), "agents-only instructions").expect("write AGENTS.md");
|
||||||
|
|
||||||
|
let context = ProjectContext::discover(&root, "2026-03-31").expect("context should load");
|
||||||
|
|
||||||
|
assert_eq!(context.instruction_files.len(), 1);
|
||||||
|
assert!(context.instruction_files[0].path.ends_with("AGENTS.md"));
|
||||||
|
assert!(render_instruction_files(&context.instruction_files)
|
||||||
|
.contains("agents-only instructions"));
|
||||||
|
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discovers_scoped_dot_claude_claude_markdown_instruction_file() {
|
||||||
|
let root = temp_dir();
|
||||||
|
fs::create_dir_all(root.join(".claude")).expect("dot claude dir");
|
||||||
|
fs::write(
|
||||||
|
root.join(".claude").join("CLAUDE.md"),
|
||||||
|
"dot-claude-only instructions",
|
||||||
|
)
|
||||||
|
.expect("write .claude/CLAUDE.md");
|
||||||
|
|
||||||
|
let context = ProjectContext::discover(&root, "2026-03-31").expect("context should load");
|
||||||
|
|
||||||
|
assert_eq!(context.instruction_files.len(), 1);
|
||||||
|
assert!(context.instruction_files[0]
|
||||||
|
.path
|
||||||
|
.ends_with(".claude/CLAUDE.md"));
|
||||||
|
assert!(render_instruction_files(&context.instruction_files)
|
||||||
|
.contains("dot-claude-only instructions"));
|
||||||
|
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discovers_claude_agents_and_dot_claude_instruction_files_together() {
|
||||||
|
let root = temp_dir();
|
||||||
|
fs::create_dir_all(root.join(".claude")).expect("dot claude dir");
|
||||||
|
fs::write(root.join("CLAUDE.md"), "claude instructions").expect("write CLAUDE.md");
|
||||||
|
fs::write(root.join("AGENTS.md"), "agents instructions").expect("write AGENTS.md");
|
||||||
|
fs::write(
|
||||||
|
root.join(".claude").join("CLAUDE.md"),
|
||||||
|
"dot claude instructions",
|
||||||
|
)
|
||||||
|
.expect("write .claude/CLAUDE.md");
|
||||||
|
|
||||||
|
let context = ProjectContext::discover(&root, "2026-03-31").expect("context should load");
|
||||||
|
let rendered = render_instruction_files(&context.instruction_files);
|
||||||
|
|
||||||
|
assert!(rendered.contains("claude instructions"));
|
||||||
|
assert!(rendered.contains("agents instructions"));
|
||||||
|
assert!(rendered.contains("dot claude instructions"));
|
||||||
|
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dedupes_identical_instruction_content_across_scopes() {
|
fn dedupes_identical_instruction_content_across_scopes() {
|
||||||
let root = temp_dir();
|
let root = temp_dir();
|
||||||
|
|||||||
@@ -1381,13 +1381,14 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
allow_broad_cwd,
|
allow_broad_cwd,
|
||||||
),
|
),
|
||||||
other => {
|
other => {
|
||||||
if rest.len() == 1 && looks_like_subcommand_typo(other) {
|
if looks_like_subcommand_typo(other)
|
||||||
// #825: always emit a command_not_found error for
|
&& (rest.len() == 1 || output_format == CliOutputFormat::Json)
|
||||||
// single-word all-alpha/dash tokens that don't match any
|
{
|
||||||
// known subcommand — with or without close suggestions.
|
// #825/#826: emit command_not_found before provider startup for
|
||||||
// Multi-word cases fall through to CliAction::Prompt so
|
// command-shaped tokens that do not match known subcommands.
|
||||||
// natural language prompts like `claw explain this` work.
|
// Text-mode multi-word prompt shorthand remains available, but
|
||||||
// (#826 documents the multi-word gap as a known limitation.)
|
// JSON-mode automation must not turn an unknown command into a
|
||||||
|
// credential-gated prompt request.
|
||||||
let mut message = format!("command_not_found: unknown subcommand: {other}.");
|
let mut message = format!("command_not_found: unknown subcommand: {other}.");
|
||||||
if let Some(suggestions) = suggest_similar_subcommand(other) {
|
if let Some(suggestions) = suggest_similar_subcommand(other) {
|
||||||
if let Some(line) = render_suggestion_line("Did you mean", &suggestions) {
|
if let Some(line) = render_suggestion_line("Did you mean", &suggestions) {
|
||||||
|
|||||||
@@ -3972,31 +3972,30 @@ fn unknown_subcommand_typo_with_suggestions_json_emits_command_not_found() {
|
|||||||
assert!(stderr.is_empty(), "typo JSON must have empty stderr (#825)");
|
assert!(stderr.is_empty(), "typo JSON must have empty stderr (#825)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// #826: multi-word unknown subcommand is a known gap — falls through to
|
// #826: JSON-mode multi-word unknown subcommands must not fall through to
|
||||||
// CliAction::Prompt (natural language prompt passthrough like `claw explain this`).
|
// CliAction::Prompt and hit the provider credential gate.
|
||||||
// Single-word typos (#825) are caught; multi-word is documented as backlog.
|
|
||||||
// This test documents the current behaviour (not the desired fix).
|
|
||||||
#[test]
|
#[test]
|
||||||
fn multi_word_unknown_subcommand_falls_through_to_prompt_826() {
|
fn multi_word_unknown_subcommand_json_emits_command_not_found_826() {
|
||||||
let root = unique_temp_dir("multi-word-gap-826");
|
let root = unique_temp_dir("multi-word-command-not-found-826");
|
||||||
std::fs::create_dir_all(&root).expect("create temp dir");
|
std::fs::create_dir_all(&root).expect("create temp dir");
|
||||||
// "foobar baz" has no fuzzy suggestion → falls through to Prompt path
|
|
||||||
// (hits missing_credentials since no API key is set, rc=1)
|
|
||||||
let output = run_claw(&root, &["--output-format", "json", "foobar", "baz"], &[]);
|
let output = run_claw(&root, &["--output-format", "json", "foobar", "baz"], &[]);
|
||||||
assert_eq!(output.status.code(), Some(1));
|
assert_eq!(output.status.code(), Some(1));
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
// Currently emits missing_credentials (fallthrough gap documented in #826)
|
|
||||||
let j: serde_json::Value =
|
let j: serde_json::Value =
|
||||||
serde_json::from_str(stdout.trim()).expect("multi-word fallthrough must emit JSON");
|
serde_json::from_str(stdout.trim()).expect("multi-word unknown subcommand must emit JSON");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
j["status"], "error",
|
j["error_kind"], "command_not_found",
|
||||||
"multi-word fallthrough must be an error: {j}"
|
"multi-word unknown subcommand must emit command_not_found, not missing_credentials (#826): {j}"
|
||||||
|
);
|
||||||
|
let hint = j["hint"].as_str().unwrap_or_default();
|
||||||
|
assert!(
|
||||||
|
hint.contains("claw prompt") || hint.contains("--help"),
|
||||||
|
"hint should explain prompt/command recovery, got: {hint:?}"
|
||||||
);
|
);
|
||||||
// stderr must be empty regardless (JSON mode)
|
|
||||||
assert!(
|
assert!(
|
||||||
stderr.is_empty(),
|
stderr.is_empty(),
|
||||||
"multi-word fallthrough JSON must have empty stderr: {stderr:?}"
|
"multi-word command_not_found JSON must have empty stderr: {stderr:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user