mirror of
https://github.com/instructkr/claw-code.git
synced 2026-06-04 05:34:11 +08:00
fix: expose binary provenance in local JSON
This commit is contained in:
@@ -7759,7 +7759,11 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
|
||||
795. **`claw skills install /nonexistent` returned `skill_not_found + hint:null` and `claw skills uninstall x` returned `unsupported_skills_action + hint:null`** — dogfooded 2026-05-27 on `491f179a`. Both error kinds were missing from `fallback_hint_for_error_kind` table, so even though classify returned a typed kind, the hint field was always null. Fix: added `"skill_not_found"` → hint suggesting `claw skills list` / `claw skills install`; added `"unsupported_skills_action"` → hint listing supported actions. Integration test `skills_install_not_found_and_unsupported_action_have_hints_795` covers both paths. 57 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori skills lifecycle probe on `491f179a`, 2026-05-27.
|
||||
|
||||
796. **`claw agents show <name> <extra>` and `claw skills show <name> <extra>` returned confusing `agent_not_found`/`skill_not_found` for the concatenated "name extra" string** — dogfooded 2026-05-27 on `18b4cee5`. `join_optional_args` passes all tokens as a space-joined string; both `show` handlers called `split_once(' ')` to extract the name but did not check if the remainder (after the first split) contained additional tokens. Extra positional args (including `--flags`) became part of the "name", silently mangling the lookup. Fix: added second `split_once(' ')` on the extracted name; if the result has two parts, return `unexpected_extra_args` with a usage hint. Valid single-name lookups are unaffected. Two new integration tests `agents_show_extra_positional_arg_returns_unexpected_extra_796`, `skills_show_extra_positional_arg_returns_unexpected_extra_796`. 59 CLI contract tests pass. [SCOPE: claw-code] Source: Jobdori agents/skills show extra-arg probe on `18b4cee5`, 2026-05-27.
|
||||
797. **Installed `claw version --output-format json` reports `git_sha:null` / `Git SHA unknown`, so dogfood cannot tie the binary under test to a source revision** — dogfooded 2026-05-27 from `#clawcode-building-in-public` using the installed `/home/bellman/.cargo/bin/claw` binary in a clean `ultraworkers/claw-code` checkout. `claw version --output-format json` returned `{"kind":"version","version":"0.1.0","git_sha":null,"target":null,"build_date":"2026-03-31"...}` while `claw status --output-format json` only reported workspace state (`git_branch`, clean/dirty counts) and did not provide any executable-vs-workspace provenance comparison. This is a clawability gap in event/log opacity and stale-binary confusion: an operator can run `doctor/status/version` successfully but still cannot prove which commit the installed CLI came from, whether it matches `origin/main`, or whether the observed behavior is from a stale packaged binary. **Required fix shape:** (a) embed build git SHA/target/build provenance in installed/release binaries whenever the source tree is available; (b) when provenance is missing, emit a typed `binary_provenance.status:"unknown"` rather than only `git_sha:null`; (c) have `status`/`doctor` include a redaction-safe comparison between executable provenance and workspace HEAD when running inside a git checkout; (d) add regression/packaging coverage proving release/local install paths preserve or explicitly classify provenance. **Why this matters:** dogfood reports and automation need to distinguish current-source failures from stale or unknown binary lineage before opening/rebasing/closing PRs. Source: gaebal-gajae live dogfood on 2026-05-27; active repo checkout had open PR #3124 DIRTY with no checks and PR #3125 CLEAN, but the installed binary itself could not identify its source revision.
|
||||
797. **DONE — Installed `claw version --output-format json` reports `git_sha:null` / `Git SHA unknown`, so dogfood cannot tie the binary under test to a source revision** — dogfooded 2026-05-27 from `#clawcode-building-in-public` using an installed binary in a clean `ultraworkers/claw-code` checkout. The gap was that version/status/doctor did not provide a structured executable-vs-workspace provenance object when build metadata was missing or stale. [SCOPE: claw-code]
|
||||
|
||||
**Fix applied.** `version --output-format json` now includes a `binary_provenance` object with `status:"known"|"unknown"`, build git SHA, target, build date, executable path, workspace HEAD SHA, `workspace_match`, and a structured hint when provenance is missing or mismatched. `status --output-format json` exposes the same object, and `doctor --output-format json` includes it in the `system` check so dogfood reports can distinguish current-source failures from stale or unknown binary lineage.
|
||||
|
||||
**Verification.** `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli version_status_doctor_include_binary_provenance_797 -- --nocapture`; `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli version_emits_json_when_requested -- --nocapture`; `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli doctor_and_resume_status_emit_json_when_requested -- --nocapture`; `cargo test --manifest-path rust/Cargo.toml -p rusty-claude-cli --test output_format_contract -- --nocapture`; direct probes `cargo run --manifest-path rust/Cargo.toml -q -p rusty-claude-cli -- --output-format json version` and `cargo run --manifest-path rust/Cargo.toml -q -p rusty-claude-cli -- --output-format json status`; `cargo build --manifest-path rust/Cargo.toml --workspace --locked`.
|
||||
|
||||
798. **`claw plugins show <name> <extra-arg>` returned `unexpected_extra_args` + `hint:null`** — dogfooded 2026-05-27 on `9976585f`. The plugins arg parser at the top level emitted `"unexpected extra arguments after 'claw plugins show ...': ..."` with no `\n` delimiter (parity gap with #791 config fix). Fix: appended `\nUsage: claw plugins [list|show <id>|...]` to the error format string. Integration test `plugins_extra_args_have_non_null_hint_797`. Committed as `bff37000`. 60 CLI contract tests pass. [SCOPE: claw-code]
|
||||
|
||||
|
||||
2
USAGE.md
2
USAGE.md
@@ -349,6 +349,8 @@ These are the models registered in the built-in alias table with known token lim
|
||||
| `grok-mini` / `grok-3-mini` | `grok-3-mini` | xAI | 64 000 | 131 072 |
|
||||
| `grok-2` | `grok-2` | xAI | — | — |
|
||||
| `kimi` | `kimi-k2.5` | DashScope | 16 384 | 256 000 |
|
||||
| `qwen-max` | `qwen-max` | DashScope | 8 192 | 131 072 |
|
||||
| `qwen-plus` | `qwen-plus` | DashScope | 8 192 | 131 072 |
|
||||
| `gpt-4.1` / `gpt-4.1-mini` / `gpt-4.1-nano` | same | OpenAI-compatible | 32 768 | 1 047 576 |
|
||||
| `gpt-5.4` / `gpt-5.4-mini` / `gpt-5.4-nano` | same | OpenAI-compatible | 128 000 | 1 000 000 / 400 000 |
|
||||
|
||||
|
||||
@@ -2680,6 +2680,7 @@ fn render_doctor_report(
|
||||
session_lifecycle: classify_session_lifecycle_for(&cwd),
|
||||
boot_preflight,
|
||||
sandbox_status: resolve_sandbox_status(sandbox_config.sandbox(), &cwd),
|
||||
binary_provenance: binary_provenance_for(Some(&cwd)),
|
||||
// Doctor path has its own config check; StatusContext here is only
|
||||
// fed into health renderers that don't read config_load_error.
|
||||
config_load_error: config.as_ref().err().map(ToString::to_string),
|
||||
@@ -3297,6 +3298,14 @@ fn check_system_health(cwd: &Path, config: Option<&runtime::RuntimeConfig>) -> D
|
||||
if let Some(model) = default_model {
|
||||
details.push(format!("Default model {model}"));
|
||||
}
|
||||
let binary_provenance = binary_provenance_for(Some(cwd));
|
||||
details.push(format!(
|
||||
"Binary provenance status={} workspace_match={}",
|
||||
binary_provenance.status(),
|
||||
binary_provenance
|
||||
.workspace_match
|
||||
.map_or_else(|| "unknown".to_string(), |matches| matches.to_string())
|
||||
));
|
||||
DiagnosticCheck::new(
|
||||
"System",
|
||||
DiagnosticLevel::Ok,
|
||||
@@ -3310,6 +3319,10 @@ fn check_system_health(cwd: &Path, config: Option<&runtime::RuntimeConfig>) -> D
|
||||
("version".to_string(), json!(VERSION)),
|
||||
("build_target".to_string(), json!(BUILD_TARGET)),
|
||||
("git_sha".to_string(), json!(GIT_SHA)),
|
||||
(
|
||||
"binary_provenance".to_string(),
|
||||
binary_provenance.json_value(),
|
||||
),
|
||||
("default_model".to_string(), json!(default_model)),
|
||||
]))
|
||||
}
|
||||
@@ -3493,17 +3506,19 @@ fn print_version(output_format: CliOutputFormat) -> Result<(), Box<dyn std::erro
|
||||
}
|
||||
|
||||
fn version_json_value() -> serde_json::Value {
|
||||
let executable_path = env::current_exe().ok().map(|p| p.display().to_string());
|
||||
let cwd = env::current_dir().ok();
|
||||
let binary_provenance = binary_provenance_for(cwd.as_deref());
|
||||
json!({
|
||||
"kind": "version",
|
||||
"action": "show",
|
||||
"status": "ok",
|
||||
"message": render_version_report(),
|
||||
"version": VERSION,
|
||||
"git_sha": GIT_SHA,
|
||||
"target": BUILD_TARGET,
|
||||
"build_date": DEFAULT_DATE,
|
||||
"executable_path": executable_path,
|
||||
"git_sha": binary_provenance.git_sha,
|
||||
"target": binary_provenance.target,
|
||||
"build_date": binary_provenance.build_date,
|
||||
"executable_path": binary_provenance.executable_path,
|
||||
"binary_provenance": binary_provenance.json_value(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3718,6 +3733,7 @@ struct StatusContext {
|
||||
session_lifecycle: SessionLifecycleSummary,
|
||||
boot_preflight: BootPreflightSnapshot,
|
||||
sandbox_status: runtime::SandboxStatus,
|
||||
binary_provenance: BinaryProvenance,
|
||||
/// #143: when `.claw.json` (or another loaded config file) fails to parse,
|
||||
/// we capture the parse error here and still populate every field that
|
||||
/// doesn't depend on runtime config (workspace, git, sandbox defaults,
|
||||
@@ -3732,6 +3748,87 @@ struct StatusContext {
|
||||
config_load_error_kind: Option<&'static str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct BinaryProvenance {
|
||||
git_sha: Option<String>,
|
||||
target: Option<String>,
|
||||
build_date: String,
|
||||
executable_path: Option<String>,
|
||||
workspace_git_sha: Option<String>,
|
||||
workspace_match: Option<bool>,
|
||||
hint: Option<String>,
|
||||
}
|
||||
|
||||
impl BinaryProvenance {
|
||||
fn status(&self) -> &'static str {
|
||||
if self.git_sha.is_some() {
|
||||
"known"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
}
|
||||
|
||||
fn json_value(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"status": self.status(),
|
||||
"git_sha": self.git_sha,
|
||||
"target": self.target,
|
||||
"build_date": self.build_date,
|
||||
"executable_path": self.executable_path,
|
||||
"workspace_git_sha": self.workspace_git_sha,
|
||||
"workspace_match": self.workspace_match,
|
||||
"hint": self.hint,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn known_build_metadata(value: Option<&str>) -> Option<String> {
|
||||
let value = value?.trim();
|
||||
if value.is_empty() || value == "unknown" {
|
||||
None
|
||||
} else {
|
||||
Some(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn binary_provenance_for(cwd: Option<&Path>) -> BinaryProvenance {
|
||||
let git_sha = known_build_metadata(GIT_SHA);
|
||||
let target = known_build_metadata(BUILD_TARGET);
|
||||
let workspace_git_sha = cwd.and_then(|cwd| {
|
||||
run_git_capture_in(cwd, &["rev-parse", "--short", "HEAD"])
|
||||
.map(|sha| sha.trim().to_string())
|
||||
.filter(|sha| !sha.is_empty())
|
||||
});
|
||||
let workspace_match = git_sha
|
||||
.as_deref()
|
||||
.zip(workspace_git_sha.as_deref())
|
||||
.map(|(binary, workspace)| binary.starts_with(workspace) || workspace.starts_with(binary));
|
||||
let hint = if git_sha.is_none() {
|
||||
Some(
|
||||
"Build metadata did not include a git SHA; rebuild from a git checkout before filing provenance-sensitive dogfood reports."
|
||||
.to_string(),
|
||||
)
|
||||
} else if workspace_match == Some(false) {
|
||||
Some(
|
||||
"The running binary was built from a different commit than the current workspace HEAD; rebuild or switch binaries before attributing behavior to this checkout."
|
||||
.to_string(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
BinaryProvenance {
|
||||
git_sha,
|
||||
target,
|
||||
build_date: DEFAULT_DATE.to_string(),
|
||||
executable_path: env::current_exe()
|
||||
.ok()
|
||||
.map(|path| path.display().to_string()),
|
||||
workspace_git_sha,
|
||||
workspace_match,
|
||||
hint,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct BranchFreshness {
|
||||
upstream: Option<String>,
|
||||
@@ -7500,6 +7597,7 @@ fn status_json_value(
|
||||
"restricted": allowed_tools.is_some(),
|
||||
"entries": allowed_tool_entries,
|
||||
},
|
||||
"binary_provenance": context.binary_provenance.json_value(),
|
||||
"usage": {
|
||||
"messages": usage.message_count,
|
||||
"turns": usage.turns,
|
||||
@@ -7627,6 +7725,7 @@ fn status_context(
|
||||
session_lifecycle: classify_session_lifecycle_for(&cwd),
|
||||
boot_preflight,
|
||||
sandbox_status,
|
||||
binary_provenance: binary_provenance_for(Some(&cwd)),
|
||||
config_load_error,
|
||||
config_load_error_kind,
|
||||
})
|
||||
@@ -14801,6 +14900,7 @@ mod tests {
|
||||
},
|
||||
boot_preflight: test_boot_preflight(),
|
||||
sandbox_status: runtime::SandboxStatus::default(),
|
||||
binary_provenance: super::binary_provenance_for(None),
|
||||
config_load_error: None,
|
||||
config_load_error_kind: None,
|
||||
},
|
||||
@@ -14946,6 +15046,7 @@ mod tests {
|
||||
},
|
||||
boot_preflight: test_boot_preflight(),
|
||||
sandbox_status: runtime::SandboxStatus::default(),
|
||||
binary_provenance: super::binary_provenance_for(None),
|
||||
config_load_error: None,
|
||||
config_load_error_kind: None,
|
||||
};
|
||||
@@ -14984,6 +15085,7 @@ mod tests {
|
||||
},
|
||||
boot_preflight: test_boot_preflight(),
|
||||
sandbox_status: runtime::SandboxStatus::default(),
|
||||
binary_provenance: super::binary_provenance_for(None),
|
||||
config_load_error: None,
|
||||
config_load_error_kind: None,
|
||||
};
|
||||
|
||||
@@ -145,6 +145,80 @@ fn version_emits_json_when_requested() {
|
||||
parsed["executable_path"].is_string(),
|
||||
"executable_path must be a string in version JSON so callers can identify which binary is running"
|
||||
);
|
||||
let binary_provenance = parsed["binary_provenance"]
|
||||
.as_object()
|
||||
.expect("version JSON must include binary_provenance object (#797)");
|
||||
assert!(matches!(
|
||||
binary_provenance["status"].as_str(),
|
||||
Some("known" | "unknown")
|
||||
));
|
||||
assert_eq!(binary_provenance["git_sha"], parsed["git_sha"]);
|
||||
assert_eq!(binary_provenance["target"], parsed["target"]);
|
||||
assert_eq!(binary_provenance["build_date"], parsed["build_date"]);
|
||||
assert_eq!(
|
||||
binary_provenance["executable_path"],
|
||||
parsed["executable_path"]
|
||||
);
|
||||
assert!(
|
||||
binary_provenance["hint"].is_string() || binary_provenance["hint"].is_null(),
|
||||
"binary provenance must classify missing/stale lineage with a structured hint field"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_status_doctor_include_binary_provenance_797() {
|
||||
let root = git_temp_dir("binary-provenance-797");
|
||||
fs::write(root.join("tracked.txt"), "v1").expect("write tracked file");
|
||||
let git_commands: &[&[&str]] = &[
|
||||
&["config", "user.email", "test@claw.test"],
|
||||
&["config", "user.name", "Test"],
|
||||
&["add", "tracked.txt"],
|
||||
&["commit", "-m", "init"],
|
||||
];
|
||||
for args in git_commands {
|
||||
let output = Command::new("git")
|
||||
.args(*args)
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.expect("git fixture command should launch");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"git fixture command failed: {args:?}\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
let version = assert_json_command(&root, &["--output-format", "json", "version"]);
|
||||
assert_eq!(version["kind"], "version");
|
||||
assert!(matches!(
|
||||
version["binary_provenance"]["status"].as_str(),
|
||||
Some("known" | "unknown")
|
||||
));
|
||||
assert!(version["binary_provenance"]["workspace_git_sha"].is_string());
|
||||
assert!(
|
||||
version["binary_provenance"]["workspace_match"].is_boolean()
|
||||
|| version["binary_provenance"]["workspace_match"].is_null()
|
||||
);
|
||||
|
||||
let status = assert_json_command(&root, &["--output-format", "json", "status"]);
|
||||
assert_eq!(status["kind"], "status");
|
||||
assert_eq!(
|
||||
status["binary_provenance"]["workspace_git_sha"],
|
||||
version["binary_provenance"]["workspace_git_sha"]
|
||||
);
|
||||
|
||||
let doctor = assert_json_command(&root, &["--output-format", "json", "doctor"]);
|
||||
let system = doctor["checks"]
|
||||
.as_array()
|
||||
.expect("doctor checks")
|
||||
.iter()
|
||||
.find(|check| check["name"] == "system")
|
||||
.expect("system check");
|
||||
assert_eq!(
|
||||
system["binary_provenance"]["workspace_git_sha"],
|
||||
version["binary_provenance"]["workspace_git_sha"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -767,6 +841,17 @@ fn doctor_and_resume_status_emit_json_when_requested() {
|
||||
.expect("workspace check");
|
||||
assert!(workspace["cwd"].as_str().is_some());
|
||||
assert!(workspace["in_git_repo"].is_boolean());
|
||||
let status = assert_json_command(&root, &["--output-format", "json", "status"]);
|
||||
assert_eq!(status["kind"], "status");
|
||||
assert!(matches!(
|
||||
status["binary_provenance"]["status"].as_str(),
|
||||
Some("known" | "unknown")
|
||||
));
|
||||
assert!(status["binary_provenance"]["executable_path"].is_string());
|
||||
assert!(
|
||||
status["binary_provenance"]["workspace_match"].is_boolean()
|
||||
|| status["binary_provenance"]["workspace_match"].is_null()
|
||||
);
|
||||
|
||||
let boot_preflight = checks
|
||||
.iter()
|
||||
@@ -800,6 +885,14 @@ fn doctor_and_resume_status_emit_json_when_requested() {
|
||||
assert!(sandbox["enabled"].is_boolean());
|
||||
assert!(sandbox["fallback_reason"].is_null() || sandbox["fallback_reason"].is_string());
|
||||
|
||||
let system = checks
|
||||
.iter()
|
||||
.find(|check| check["name"] == "system")
|
||||
.expect("system check");
|
||||
assert!(matches!(
|
||||
system["binary_provenance"]["status"].as_str(),
|
||||
Some("known" | "unknown")
|
||||
));
|
||||
let session_path = write_session_fixture(&root, "resume-json", Some("hello"));
|
||||
let resumed = assert_json_command(
|
||||
&root,
|
||||
|
||||
Reference in New Issue
Block a user