fix(#783): init JSON envelope now includes hint and already_initialized fields for orchestrator parity

This commit is contained in:
YeonGyu-Kim
2026-05-27 08:04:15 +09:00
parent 32c9276fdb
commit 81fe0ccbb7
3 changed files with 85 additions and 0 deletions

View File

@@ -8179,15 +8179,26 @@ fn init_json_value(report: &crate::init::InitReport, message: &str) -> serde_jso
// Derive top-level status: "ok" when all artifacts succeeded (created or
// skipped = idempotent); no failure path exists today so always "ok".
let status = "ok";
// #783: already_initialized lets orchestrators detect the idempotent case
// without checking created.len() == 0; hint gives a stable next-action pointer.
let already_initialized = report.artifacts_with_status(InitStatus::Created).is_empty()
&& report.artifacts_with_status(InitStatus::Updated).is_empty();
let hint = if already_initialized {
"Workspace already initialised. Run `claw doctor` to verify health, or edit CLAUDE.md to customise guidance."
} else {
"Review and tailor CLAUDE.md to your project, then run `claw doctor` to verify the workspace."
};
json!({
"kind": "init",
"action": "init",
"status": status,
"already_initialized": already_initialized,
"project_path": report.project_root.display().to_string(),
"created": report.artifacts_with_status(InitStatus::Created),
"updated": report.artifacts_with_status(InitStatus::Updated),
"skipped": report.artifacts_with_status(InitStatus::Skipped),
"artifacts": report.artifact_json_entries(),
"hint": hint,
"next_step": crate::init::InitReport::NEXT_STEP,
"message": message,
})

View File

@@ -2364,3 +2364,75 @@ fn acp_unsupported_invocation_has_hint_782() {
"hint should explain the discoverability-only status, got: {hint:?}"
);
}
#[test]
fn init_json_envelope_has_hint_and_already_initialized_783() {
// #783: claw --output-format json init was missing the hint field entirely.
// Also added already_initialized: bool so orchestrators can detect the idempotent
// case without checking created.len() == 0.
let root = unique_temp_dir("init-hint-783");
fs::create_dir_all(&root).expect("temp dir");
std::process::Command::new("git")
.args(["init", "-q"])
.current_dir(&root)
.output()
.ok();
// Fresh init — already_initialized should be false, hint should mention CLAUDE.md
let output = run_claw(&root, &["--output-format", "json", "init"], &[]);
assert!(output.status.success(), "init should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let raw = if stdout.trim_start().starts_with('{') {
&*stdout
} else {
&*stderr
};
let parsed: serde_json::Value = serde_json::from_str(raw.trim()).unwrap_or_else(|_| {
// multi-line JSON; find the whole block
serde_json::from_str(raw).expect("should emit valid JSON")
});
assert_eq!(parsed["status"], "ok", "init should succeed");
assert!(
parsed.get("already_initialized").is_some(),
"init JSON must include already_initialized field (#783)"
);
assert_eq!(
parsed["already_initialized"], false,
"first init: already_initialized must be false"
);
let hint = parsed["hint"]
.as_str()
.expect("hint must be present and non-null (#783)");
assert!(!hint.is_empty(), "hint must not be empty");
assert!(
hint.contains("CLAUDE.md") || hint.contains("doctor"),
"fresh-init hint should mention CLAUDE.md or doctor, got: {hint:?}"
);
// Idempotent re-init — already_initialized should be true
let output2 = run_claw(&root, &["--output-format", "json", "init"], &[]);
assert!(output2.status.success(), "re-init should succeed");
let stdout2 = String::from_utf8_lossy(&output2.stdout);
let stderr2 = String::from_utf8_lossy(&output2.stderr);
let raw2 = if stdout2.trim_start().starts_with('{') {
&*stdout2
} else {
&*stderr2
};
let parsed2: serde_json::Value = serde_json::from_str(raw2.trim())
.or_else(|_| serde_json::from_str(raw2))
.expect("re-init should emit valid JSON");
assert_eq!(
parsed2["already_initialized"], true,
"re-init: already_initialized must be true"
);
let hint2 = parsed2["hint"]
.as_str()
.expect("hint must be present on re-init");
assert!(
hint2.contains("already") || hint2.contains("doctor"),
"re-init hint should acknowledge workspace exists, got: {hint2:?}"
);
}