mirror of
https://github.com/instructkr/claw-code.git
synced 2026-06-04 21:47:10 +08:00
fix: load Claw and Agents memory files
Generated with https://github.com/Yeachan-Heo/gajae-code Co-authored-by: Gajae Code <dev@gajae-code.com>
This commit is contained in:
@@ -87,7 +87,7 @@ Primary artifacts:
|
||||
| Sub-agent / agent surfaces | ✅ |
|
||||
| Todo tracking | ✅ |
|
||||
| Notebook editing | ✅ |
|
||||
| CLAUDE.md / project memory | ✅ |
|
||||
| CLAUDE.md / CLAW.md / AGENTS.md project memory | ✅ |
|
||||
| Config file hierarchy (`.claw.json` + merged config sections) | ✅ |
|
||||
| Permission system | ✅ |
|
||||
| MCP server lifecycle + inspection | ✅ |
|
||||
@@ -149,6 +149,7 @@ Top-level commands:
|
||||
`claw acp` is a local discoverability surface for editor-first users: it reports the current ACP/Zed status without starting the runtime. As of April 16, 2026, claw-code does **not** ship an ACP/Zed daemon or JSON-RPC entrypoint yet, and `claw acp serve` is only a status alias until the real protocol surface lands. Status queries exit 0 and expose the same machine-readable contract via `--output-format json`; malformed ACP invocations exit 1 with `kind: unsupported_acp_invocation`.
|
||||
`--output-format` accepts `text` or `json` in any casing. `CLAW_OUTPUT_FORMAT=json` selects JSON as the default for non-interactive commands, explicit flags override it, repeated flags warn on stderr, and status JSON exposes `format_source`, `format_raw`, and `format_overridden`. Help and doctor output also surface `CLAW_LOG` / `RUST_LOG` as the logging environment knobs.
|
||||
`claw version --output-format json` is the provenance probe for automation: it reports full `git_sha`, derived `git_sha_short`, `is_dirty`, `branch`, `commit_date`, `commit_timestamp`, `rustc_version`, runtime `executable_path`, and `binary_provenance`; the text report is available as `human_readable` instead of a duplicate `message` field.
|
||||
`status --output-format json` reports loaded project memory files under `workspace.memory_files[]` with each file's `path`, `source` (`claude_md`, `claw_md`, `agents_md`, or scoped/rule sources), `chars`, and `contributes`; `claw doctor --output-format json` includes a dedicated `memory` check. Root instruction-file priority is `CLAUDE.md`, then `CLAW.md`, then `AGENTS.md`, and all non-duplicate loaded files contribute to the rendered system prompt.
|
||||
Shorthand prompt mode honors the POSIX `--` end-of-flags separator, so `claw -- "-prompt-with-dash"` and unknown dash-prefixed non-flag text stay on the prompt path instead of being treated as CLI options.
|
||||
`claw dump-manifests` is self-contained: it emits the Rust resolver inventory for the selected workspace (commands, tools, agents, skills, and bootstrap phases) without requiring an upstream Claude Code TypeScript checkout. Use `--manifests-dir PATH` only to scope resolver discovery to another directory.
|
||||
|
||||
|
||||
@@ -142,8 +142,9 @@ pub use policy_engine::{
|
||||
PolicyEvaluation, PolicyRule, ReconcileReason, ReviewStatus,
|
||||
};
|
||||
pub use prompt::{
|
||||
load_system_prompt, prepend_bullets, ContextFile, ModelFamilyIdentity, ProjectContext,
|
||||
PromptBuildError, SystemPromptBuilder, FRONTIER_MODEL_NAME, SYSTEM_PROMPT_DYNAMIC_BOUNDARY,
|
||||
load_system_prompt, load_system_prompt_with_context, prepend_bullets, ContextFile,
|
||||
ModelFamilyIdentity, ProjectContext, PromptBuildError, SystemPromptBuilder,
|
||||
FRONTIER_MODEL_NAME, SYSTEM_PROMPT_DYNAMIC_BOUNDARY,
|
||||
};
|
||||
pub use recovery_recipes::{
|
||||
attempt_recovery, recipe_for, EscalationPolicy, FailureScenario, RecoveryAttemptState,
|
||||
|
||||
@@ -69,6 +69,18 @@ pub struct ContextFile {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl ContextFile {
|
||||
#[must_use]
|
||||
pub fn source(&self) -> &'static str {
|
||||
instruction_file_source(&self.path)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn char_count(&self) -> usize {
|
||||
self.content.chars().count()
|
||||
}
|
||||
}
|
||||
|
||||
/// Project-local context injected into the rendered system prompt.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct ProjectContext {
|
||||
@@ -256,6 +268,24 @@ pub fn prepend_bullets(items: Vec<String>) -> Vec<String> {
|
||||
items.into_iter().map(|item| format!(" - {item}")).collect()
|
||||
}
|
||||
|
||||
fn instruction_file_source(path: &Path) -> &'static str {
|
||||
let file_name = path.file_name().and_then(|name| name.to_str());
|
||||
let parent_name = path
|
||||
.parent()
|
||||
.and_then(|parent| parent.file_name())
|
||||
.and_then(|name| name.to_str());
|
||||
|
||||
match (parent_name, file_name) {
|
||||
(Some(".claw"), Some("CLAUDE.md")) => "claw_claude_md",
|
||||
(Some(".claude"), Some("CLAUDE.md")) => "claude_claude_md",
|
||||
(_, Some("CLAUDE.md")) => "claude_md",
|
||||
(_, Some("CLAW.md")) => "claw_md",
|
||||
(_, Some("AGENTS.md")) => "agents_md",
|
||||
(_, Some("CLAUDE.local.md")) => "claude_local_md",
|
||||
(Some(".claw"), Some("instructions.md")) => "claw_instructions",
|
||||
_ => "rule_file",
|
||||
}
|
||||
}
|
||||
fn discover_instruction_files(
|
||||
cwd: &Path,
|
||||
rules_import: &RulesImportConfig,
|
||||
@@ -272,6 +302,7 @@ fn discover_instruction_files(
|
||||
for dir in directories {
|
||||
for candidate in [
|
||||
dir.join("CLAUDE.md"),
|
||||
dir.join("CLAW.md"),
|
||||
dir.join("AGENTS.md"),
|
||||
dir.join("CLAUDE.local.md"),
|
||||
dir.join(".claw").join("CLAUDE.md"),
|
||||
@@ -430,7 +461,7 @@ fn render_project_context(project_context: &ProjectContext) -> String {
|
||||
];
|
||||
if !project_context.instruction_files.is_empty() {
|
||||
bullets.push(format!(
|
||||
"Claude instruction files discovered: {}.",
|
||||
"Project instruction files discovered: {}.",
|
||||
project_context.instruction_files.len()
|
||||
));
|
||||
}
|
||||
@@ -465,7 +496,7 @@ fn render_project_context(project_context: &ProjectContext) -> String {
|
||||
}
|
||||
|
||||
fn render_instruction_files(files: &[ContextFile]) -> String {
|
||||
let mut sections = vec!["# Claude instructions".to_string()];
|
||||
let mut sections = vec!["# Project instructions".to_string()];
|
||||
let mut remaining_chars = MAX_TOTAL_INSTRUCTION_CHARS;
|
||||
for file in files {
|
||||
if remaining_chars == 0 {
|
||||
@@ -573,16 +604,31 @@ pub fn load_system_prompt(
|
||||
os_version: impl Into<String>,
|
||||
model_family: ModelFamilyIdentity,
|
||||
) -> Result<Vec<String>, PromptBuildError> {
|
||||
let cwd = cwd.into();
|
||||
let (sections, _) =
|
||||
load_system_prompt_with_context(cwd, current_date, os_name, os_version, model_family)?;
|
||||
Ok(sections)
|
||||
}
|
||||
|
||||
/// Loads config and project context, then renders the system prompt text plus metadata.
|
||||
pub fn load_system_prompt_with_context(
|
||||
cwd: impl Into<PathBuf>,
|
||||
current_date: impl Into<String>,
|
||||
os_name: impl Into<String>,
|
||||
os_version: impl Into<String>,
|
||||
model_family: ModelFamilyIdentity,
|
||||
) -> Result<(Vec<String>, ProjectContext), PromptBuildError> {
|
||||
let cwd = cwd.into();
|
||||
let config = ConfigLoader::default_for(&cwd).load()?;
|
||||
let project_context =
|
||||
discover_with_git_and_rules_import(&cwd, current_date.into(), config.rules_import())?;
|
||||
Ok(SystemPromptBuilder::new()
|
||||
let sections = SystemPromptBuilder::new()
|
||||
.with_os(os_name, os_version)
|
||||
.with_model_family(model_family)
|
||||
.with_project_context(project_context)
|
||||
.with_project_context(project_context.clone())
|
||||
.with_runtime_config(config)
|
||||
.build())
|
||||
.build();
|
||||
Ok((sections, project_context))
|
||||
}
|
||||
|
||||
fn render_config_section(config: &RuntimeConfig) -> String {
|
||||
@@ -844,10 +890,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovers_claude_agents_and_dot_claude_instruction_files_together() {
|
||||
fn discovers_claude_claw_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("CLAW.md"), "claw instructions").expect("write CLAW.md");
|
||||
fs::write(root.join("AGENTS.md"), "agents instructions").expect("write AGENTS.md");
|
||||
fs::write(
|
||||
root.join(".claude").join("CLAUDE.md"),
|
||||
@@ -857,8 +904,18 @@ mod tests {
|
||||
|
||||
let context = ProjectContext::discover(&root, "2026-03-31").expect("context should load");
|
||||
let rendered = render_instruction_files(&context.instruction_files);
|
||||
let sources = context
|
||||
.instruction_files
|
||||
.iter()
|
||||
.map(ContextFile::source)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
sources,
|
||||
vec!["claude_md", "claw_md", "agents_md", "claude_claude_md"]
|
||||
);
|
||||
assert!(rendered.contains("claude instructions"));
|
||||
assert!(rendered.contains("claw instructions"));
|
||||
assert!(rendered.contains("agents instructions"));
|
||||
assert!(rendered.contains("dot claude instructions"));
|
||||
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||
@@ -1218,7 +1275,7 @@ mod tests {
|
||||
|
||||
assert!(prompt.contains("# System"));
|
||||
assert!(prompt.contains("# Project context"));
|
||||
assert!(prompt.contains("# Claude instructions"));
|
||||
assert!(prompt.contains("# Project instructions"));
|
||||
assert!(prompt.contains("Project rules"));
|
||||
assert!(prompt.contains("permissionMode"));
|
||||
assert!(prompt.contains(SYSTEM_PROMPT_DYNAMIC_BOUNDARY));
|
||||
@@ -1263,7 +1320,7 @@ mod tests {
|
||||
path: PathBuf::from("/tmp/project/CLAUDE.md"),
|
||||
content: "Project rules".to_string(),
|
||||
}]);
|
||||
assert!(rendered.contains("# Claude instructions"));
|
||||
assert!(rendered.contains("# Project instructions"));
|
||||
assert!(rendered.contains("scope: /tmp/project"));
|
||||
assert!(rendered.contains("Project rules"));
|
||||
}
|
||||
|
||||
@@ -53,12 +53,13 @@ use plugins::{PluginHooks, PluginManager, PluginManagerConfig, PluginRegistry};
|
||||
use render::{MarkdownStreamState, Spinner, TerminalRenderer};
|
||||
use runtime::{
|
||||
check_base_commit, format_stale_base_warning, format_usd, load_oauth_credentials,
|
||||
load_system_prompt, pricing_for_model, resolve_expected_base, resolve_sandbox_status,
|
||||
ApiClient, ApiRequest, AssistantEvent, BaseCommitState, CompactionConfig, ConfigFileReport,
|
||||
ConfigLoader, ConfigSource, ContentBlock, ConversationMessage, ConversationRuntime, McpServer,
|
||||
McpServerManager, McpServerSpec, McpTool, MessageRole, ModelPricing, PermissionMode,
|
||||
PermissionPolicy, ProjectContext, PromptCacheEvent, ResolvedPermissionMode, RuntimeError,
|
||||
Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
|
||||
load_system_prompt, load_system_prompt_with_context, pricing_for_model, resolve_expected_base,
|
||||
resolve_sandbox_status, ApiClient, ApiRequest, AssistantEvent, BaseCommitState,
|
||||
CompactionConfig, ConfigFileReport, ConfigLoader, ConfigSource, ContentBlock, ContextFile,
|
||||
ConversationMessage, ConversationRuntime, McpServer, McpServerManager, McpServerSpec, McpTool,
|
||||
MessageRole, ModelPricing, PermissionMode, PermissionPolicy, ProjectContext, PromptCacheEvent,
|
||||
ResolvedPermissionMode, RuntimeError, Session, TokenUsage, ToolError, ToolExecutor,
|
||||
UsageTracker,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Map, Value};
|
||||
@@ -3501,6 +3502,11 @@ fn render_doctor_report(
|
||||
.map_or(0, |runtime_config| runtime_config.loaded_entries().len()),
|
||||
discovered_config_files: discovered_config.len(),
|
||||
memory_file_count: project_context.instruction_files.len(),
|
||||
memory_files: memory_file_summaries(&project_context.instruction_files),
|
||||
unloaded_memory_files: unloaded_memory_candidates(
|
||||
&cwd,
|
||||
&memory_file_summaries(&project_context.instruction_files),
|
||||
),
|
||||
project_root,
|
||||
git_branch,
|
||||
git_summary,
|
||||
@@ -3521,6 +3527,7 @@ fn render_doctor_report(
|
||||
check_config_health(&config_loader, config.as_ref()),
|
||||
check_install_source_health(),
|
||||
check_workspace_health(&context),
|
||||
check_memory_health(&context),
|
||||
check_boot_preflight_health(&context),
|
||||
check_sandbox_health(&context.sandbox_status),
|
||||
check_permission_health(permission_mode),
|
||||
@@ -3975,6 +3982,19 @@ fn check_workspace_health(context: &StatusContext) -> DiagnosticCheck {
|
||||
"Memory files {} · config files loaded {}/{}",
|
||||
context.memory_file_count, context.loaded_config_files, context.discovered_config_files
|
||||
),
|
||||
format!(
|
||||
"Loaded memory {}",
|
||||
if context.memory_files.is_empty() {
|
||||
"<none>".to_string()
|
||||
} else {
|
||||
context
|
||||
.memory_files
|
||||
.iter()
|
||||
.map(|file| format!("{}:{}", file.source, file.path))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
),
|
||||
format!(
|
||||
"Stale base {}",
|
||||
stale_base_warning.as_deref().unwrap_or("ok")
|
||||
@@ -4003,6 +4023,14 @@ fn check_workspace_health(context: &StatusContext) -> DiagnosticCheck {
|
||||
"memory_file_count".to_string(),
|
||||
json!(context.memory_file_count),
|
||||
),
|
||||
(
|
||||
"memory_files".to_string(),
|
||||
Value::Array(memory_files_json(&context.memory_files)),
|
||||
),
|
||||
(
|
||||
"unloaded_memory_files".to_string(),
|
||||
json!(context.unloaded_memory_files),
|
||||
),
|
||||
(
|
||||
"loaded_config_files".to_string(),
|
||||
json!(context.loaded_config_files),
|
||||
@@ -4018,6 +4046,57 @@ fn check_workspace_health(context: &StatusContext) -> DiagnosticCheck {
|
||||
]))
|
||||
}
|
||||
|
||||
fn check_memory_health(context: &StatusContext) -> DiagnosticCheck {
|
||||
let has_unloaded = !context.unloaded_memory_files.is_empty();
|
||||
let mut details = vec![format!("Loaded files {}", context.memory_file_count)];
|
||||
details.extend(context.memory_files.iter().map(|file| {
|
||||
format!(
|
||||
"Loaded {} ({}, chars={})",
|
||||
file.path, file.source, file.chars
|
||||
)
|
||||
}));
|
||||
details.extend(
|
||||
context
|
||||
.unloaded_memory_files
|
||||
.iter()
|
||||
.map(|path| format!("Unloaded {path}")),
|
||||
);
|
||||
|
||||
DiagnosticCheck::new(
|
||||
"Memory",
|
||||
if has_unloaded {
|
||||
DiagnosticLevel::Warn
|
||||
} else {
|
||||
DiagnosticLevel::Ok
|
||||
},
|
||||
if has_unloaded {
|
||||
"some workspace memory files exist but were not loaded".to_string()
|
||||
} else {
|
||||
format!("{} workspace memory files loaded", context.memory_file_count)
|
||||
},
|
||||
)
|
||||
.with_hint(if has_unloaded {
|
||||
"Move instructions into CLAUDE.md, CLAW.md, or AGENTS.md within the current workspace ancestry, or inspect workspace.memory_files in `claw status --output-format json`."
|
||||
} else {
|
||||
""
|
||||
})
|
||||
.with_details(details)
|
||||
.with_data(Map::from_iter([
|
||||
(
|
||||
"memory_file_count".to_string(),
|
||||
json!(context.memory_file_count),
|
||||
),
|
||||
(
|
||||
"memory_files".to_string(),
|
||||
Value::Array(memory_files_json(&context.memory_files)),
|
||||
),
|
||||
(
|
||||
"unloaded_memory_files".to_string(),
|
||||
json!(context.unloaded_memory_files),
|
||||
),
|
||||
]))
|
||||
}
|
||||
|
||||
fn check_boot_preflight_health(context: &StatusContext) -> DiagnosticCheck {
|
||||
let preflight = &context.boot_preflight;
|
||||
let missing_binaries = preflight
|
||||
@@ -4413,13 +4492,14 @@ fn print_system_prompt(
|
||||
model: &str,
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let sections = load_system_prompt(
|
||||
let (sections, project_context) = load_system_prompt_with_context(
|
||||
cwd,
|
||||
date,
|
||||
env::consts::OS,
|
||||
"unknown",
|
||||
model_family_identity_for(model),
|
||||
)?;
|
||||
let memory_files = memory_file_summaries(&project_context.instruction_files);
|
||||
let message = sections.join(
|
||||
"
|
||||
|
||||
@@ -4435,6 +4515,8 @@ fn print_system_prompt(
|
||||
"status": "ok",
|
||||
"message": message,
|
||||
"sections": sections,
|
||||
"memory_file_count": memory_files.len(),
|
||||
"memory_files": memory_files_json(&memory_files),
|
||||
}))?
|
||||
),
|
||||
}
|
||||
@@ -4672,6 +4754,63 @@ struct ResumeCommandOutcome {
|
||||
json: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct MemoryFileSummary {
|
||||
path: String,
|
||||
source: String,
|
||||
chars: usize,
|
||||
contributes: bool,
|
||||
}
|
||||
|
||||
impl MemoryFileSummary {
|
||||
fn json_value(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"path": self.path,
|
||||
"source": self.source,
|
||||
"chars": self.chars,
|
||||
"contributes": self.contributes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn memory_file_summaries(files: &[ContextFile]) -> Vec<MemoryFileSummary> {
|
||||
files
|
||||
.iter()
|
||||
.map(|file| MemoryFileSummary {
|
||||
path: file.path.display().to_string(),
|
||||
source: file.source().to_string(),
|
||||
chars: file.char_count(),
|
||||
contributes: true,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn memory_files_json(files: &[MemoryFileSummary]) -> Vec<serde_json::Value> {
|
||||
files.iter().map(MemoryFileSummary::json_value).collect()
|
||||
}
|
||||
|
||||
fn unloaded_memory_candidates(cwd: &Path, files: &[MemoryFileSummary]) -> Vec<String> {
|
||||
let mut loaded = files
|
||||
.iter()
|
||||
.map(|file| PathBuf::from(&file.path))
|
||||
.collect::<Vec<_>>();
|
||||
loaded.sort();
|
||||
|
||||
let mut missing = Vec::new();
|
||||
let mut cursor = Some(cwd);
|
||||
while let Some(dir) = cursor {
|
||||
for name in ["CLAW.md", "AGENTS.md"] {
|
||||
let candidate = dir.join(name);
|
||||
if candidate.is_file() && !loaded.iter().any(|path| path == &candidate) {
|
||||
missing.push(candidate.display().to_string());
|
||||
}
|
||||
}
|
||||
cursor = dir.parent();
|
||||
}
|
||||
missing.sort();
|
||||
missing.dedup();
|
||||
missing
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
struct StatusContext {
|
||||
cwd: PathBuf,
|
||||
@@ -4679,6 +4818,8 @@ struct StatusContext {
|
||||
loaded_config_files: usize,
|
||||
discovered_config_files: usize,
|
||||
memory_file_count: usize,
|
||||
memory_files: Vec<MemoryFileSummary>,
|
||||
unloaded_memory_files: Vec<String>,
|
||||
project_root: Option<PathBuf>,
|
||||
git_branch: Option<String>,
|
||||
git_summary: GitWorkspaceSummary,
|
||||
@@ -8677,6 +8818,8 @@ fn status_json_value(
|
||||
"loaded_config_files": context.loaded_config_files,
|
||||
"discovered_config_files": context.discovered_config_files,
|
||||
"memory_file_count": context.memory_file_count,
|
||||
"memory_files": memory_files_json(&context.memory_files),
|
||||
"unloaded_memory_files": context.unloaded_memory_files,
|
||||
},
|
||||
"sandbox": {
|
||||
"enabled": context.sandbox_status.enabled,
|
||||
@@ -8751,6 +8894,11 @@ fn status_context(
|
||||
loaded_config_files,
|
||||
discovered_config_files,
|
||||
memory_file_count: project_context.instruction_files.len(),
|
||||
memory_files: memory_file_summaries(&project_context.instruction_files),
|
||||
unloaded_memory_files: unloaded_memory_candidates(
|
||||
&cwd,
|
||||
&memory_file_summaries(&project_context.instruction_files),
|
||||
),
|
||||
project_root,
|
||||
git_branch,
|
||||
git_summary,
|
||||
@@ -8866,6 +9014,7 @@ fn format_status_report(
|
||||
Boot preflight {}
|
||||
Config files loaded {}/{}
|
||||
Memory files {}
|
||||
Loaded memory {}
|
||||
Suggested flow /status → /diff → /commit",
|
||||
context.cwd.display(),
|
||||
context
|
||||
@@ -8892,6 +9041,16 @@ fn format_status_report(
|
||||
context.loaded_config_files,
|
||||
context.discovered_config_files,
|
||||
context.memory_file_count,
|
||||
if context.memory_files.is_empty() {
|
||||
"<none>".to_string()
|
||||
} else {
|
||||
context
|
||||
.memory_files
|
||||
.iter()
|
||||
.map(|file| format!("{}:{}", file.source, file.path))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
},
|
||||
),
|
||||
format_sandbox_report(&context.sandbox_status),
|
||||
]);
|
||||
@@ -9325,7 +9484,7 @@ fn render_doctor_help_json() -> serde_json::Value {
|
||||
"command": "doctor",
|
||||
"schema_version": "1.0",
|
||||
"usage": "claw doctor [--output-format <format>]",
|
||||
"purpose": "diagnose local auth, config, workspace, permissions, sandbox, boot preflight, and build metadata",
|
||||
"purpose": "diagnose local auth, config, workspace memory, permissions, sandbox, boot preflight, and build metadata",
|
||||
"formats": ["text", "json"],
|
||||
"local_only": true,
|
||||
"requires_credentials": false,
|
||||
@@ -9333,7 +9492,7 @@ fn render_doctor_help_json() -> serde_json::Value {
|
||||
"requires_session_resume": false,
|
||||
"mutates_workspace": false,
|
||||
"output_fields": ["kind", "action", "status", "message", "report", "has_failures", "summary", "checks", "allowed_tools"],
|
||||
"check_names": ["auth", "config", "install source", "workspace", "boot preflight", "sandbox", "permissions", "system"],
|
||||
"check_names": ["auth", "config", "install source", "workspace", "memory", "boot preflight", "sandbox", "permissions", "system"],
|
||||
"status_values": ["ok", "warn", "fail"],
|
||||
"options": [
|
||||
{
|
||||
@@ -9745,7 +9904,7 @@ fn render_memory_report() -> Result<String, Box<dyn std::error::Error>> {
|
||||
if project_context.instruction_files.is_empty() {
|
||||
lines.push("Discovered files".to_string());
|
||||
lines.push(
|
||||
" No CLAUDE instruction files discovered in the current directory ancestry."
|
||||
" No CLAUDE.md, CLAW.md, AGENTS.md, or scoped instruction files discovered in the current directory ancestry."
|
||||
.to_string(),
|
||||
);
|
||||
} else {
|
||||
@@ -9759,8 +9918,10 @@ fn render_memory_report() -> Result<String, Box<dyn std::error::Error>> {
|
||||
};
|
||||
lines.push(format!(" {}. {}", index + 1, file.path.display(),));
|
||||
lines.push(format!(
|
||||
" lines={} preview={}",
|
||||
" source={} lines={} chars={} preview={}",
|
||||
file.source(),
|
||||
file.content.lines().count(),
|
||||
file.char_count(),
|
||||
preview
|
||||
));
|
||||
}
|
||||
@@ -16283,6 +16444,13 @@ mod tests {
|
||||
loaded_config_files: 2,
|
||||
discovered_config_files: 3,
|
||||
memory_file_count: 4,
|
||||
memory_files: vec![super::MemoryFileSummary {
|
||||
path: "/tmp/project/CLAUDE.md".to_string(),
|
||||
source: "claude_md".to_string(),
|
||||
chars: 42,
|
||||
contributes: true,
|
||||
}],
|
||||
unloaded_memory_files: Vec::new(),
|
||||
project_root: Some(PathBuf::from("/tmp")),
|
||||
git_branch: Some("main".to_string()),
|
||||
git_summary: GitWorkspaceSummary {
|
||||
@@ -16327,6 +16495,7 @@ mod tests {
|
||||
status.contains("Git state dirty · 3 files · 1 staged, 1 unstaged, 1 untracked")
|
||||
);
|
||||
assert!(status.contains("Changed files 3"));
|
||||
assert!(status.contains("Loaded memory claude_md:/tmp/project/CLAUDE.md"));
|
||||
assert!(status.contains("Staged 1"));
|
||||
assert!(status.contains("Unstaged 1"));
|
||||
assert!(status.contains("Untracked 1"));
|
||||
@@ -16433,6 +16602,8 @@ mod tests {
|
||||
loaded_config_files: 0,
|
||||
discovered_config_files: 0,
|
||||
memory_file_count: 0,
|
||||
memory_files: Vec::new(),
|
||||
unloaded_memory_files: Vec::new(),
|
||||
project_root: Some(PathBuf::from("/tmp/project")),
|
||||
git_branch: Some("feature/stale-base".to_string()),
|
||||
git_summary: GitWorkspaceSummary::default(),
|
||||
@@ -16467,6 +16638,52 @@ mod tests {
|
||||
.any(|detail| detail.contains("stale codebase")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_health_surfaces_loaded_and_unloaded_files_438() {
|
||||
let context = super::StatusContext {
|
||||
cwd: PathBuf::from("/tmp/project"),
|
||||
session_path: None,
|
||||
loaded_config_files: 0,
|
||||
discovered_config_files: 0,
|
||||
memory_file_count: 1,
|
||||
memory_files: vec![super::MemoryFileSummary {
|
||||
path: "/tmp/project/CLAUDE.md".to_string(),
|
||||
source: "claude_md".to_string(),
|
||||
chars: 12,
|
||||
contributes: true,
|
||||
}],
|
||||
unloaded_memory_files: vec!["/tmp/project/AGENTS.md".to_string()],
|
||||
project_root: Some(PathBuf::from("/tmp/project")),
|
||||
git_branch: Some("main".to_string()),
|
||||
git_summary: GitWorkspaceSummary::default(),
|
||||
branch_freshness: test_branch_freshness(),
|
||||
stale_base_state: super::BaseCommitState::NoExpectedBase,
|
||||
session_lifecycle: SessionLifecycleSummary {
|
||||
kind: SessionLifecycleKind::SavedOnly,
|
||||
pane_id: None,
|
||||
pane_command: None,
|
||||
pane_path: None,
|
||||
workspace_dirty: false,
|
||||
abandoned: false,
|
||||
},
|
||||
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,
|
||||
};
|
||||
|
||||
let check = super::check_memory_health(&context);
|
||||
|
||||
assert_eq!(check.level, super::DiagnosticLevel::Warn);
|
||||
assert_eq!(check.data["memory_file_count"], 1);
|
||||
assert_eq!(check.data["memory_files"][0]["source"], "claude_md");
|
||||
assert_eq!(
|
||||
check.data["unloaded_memory_files"][0],
|
||||
"/tmp/project/AGENTS.md"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_json_surfaces_session_lifecycle_for_clawhip() {
|
||||
let context = super::StatusContext {
|
||||
@@ -16475,6 +16692,8 @@ mod tests {
|
||||
loaded_config_files: 0,
|
||||
discovered_config_files: 0,
|
||||
memory_file_count: 0,
|
||||
memory_files: Vec::new(),
|
||||
unloaded_memory_files: Vec::new(),
|
||||
project_root: Some(PathBuf::from("/tmp/project")),
|
||||
git_branch: Some("feature/session-lifecycle".to_string()),
|
||||
git_summary: GitWorkspaceSummary::default(),
|
||||
|
||||
@@ -110,6 +110,7 @@ fn assert_doctor_help_json_contract(parsed: &Value) {
|
||||
let checks = parsed["check_names"].as_array().expect("check_names");
|
||||
assert!(checks.iter().any(|check| check == "auth"));
|
||||
assert!(checks.iter().any(|check| check == "boot preflight"));
|
||||
assert!(checks.iter().any(|check| check == "memory"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1270,6 +1271,70 @@ fn bootstrap_and_system_prompt_emit_json_when_requested() {
|
||||
.contains("interactive agent"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_files_load_claude_claw_agents_and_surface_json_438() {
|
||||
let root = unique_temp_dir("memory-files-438");
|
||||
let config_home = root.join("config-home");
|
||||
let home = root.join("home");
|
||||
fs::create_dir_all(&root).expect("temp dir should exist");
|
||||
fs::create_dir_all(&config_home).expect("config home should exist");
|
||||
fs::create_dir_all(&home).expect("home should exist");
|
||||
fs::write(root.join("CLAUDE.md"), "MARKER-FROM-CLAUDE-MD\n").expect("write CLAUDE.md");
|
||||
fs::write(root.join("CLAW.md"), "MARKER-FROM-CLAW-MD\n").expect("write CLAW.md");
|
||||
fs::write(root.join("AGENTS.md"), "MARKER-FROM-AGENTS-MD\n").expect("write AGENTS.md");
|
||||
let envs = [
|
||||
(
|
||||
"CLAW_CONFIG_HOME",
|
||||
config_home.to_str().expect("utf8 config home"),
|
||||
),
|
||||
("HOME", home.to_str().expect("utf8 home")),
|
||||
];
|
||||
|
||||
let status = assert_json_command_with_env(&root, &["--output-format", "json", "status"], &envs);
|
||||
assert_eq!(status["workspace"]["memory_file_count"], 3);
|
||||
let memory_files = status["workspace"]["memory_files"]
|
||||
.as_array()
|
||||
.expect("status memory files");
|
||||
let sources = memory_files
|
||||
.iter()
|
||||
.map(|file| file["source"].as_str().expect("memory source"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(sources, vec!["claude_md", "claw_md", "agents_md"]);
|
||||
assert!(memory_files
|
||||
.iter()
|
||||
.all(|file| file["path"].as_str().is_some()));
|
||||
assert!(memory_files
|
||||
.iter()
|
||||
.all(|file| file["chars"].as_u64().unwrap_or(0) > 0));
|
||||
assert!(memory_files
|
||||
.iter()
|
||||
.all(|file| file["contributes"].as_bool() == Some(true)));
|
||||
|
||||
let prompt =
|
||||
assert_json_command_with_env(&root, &["--output-format", "json", "system-prompt"], &envs);
|
||||
let message = prompt["message"].as_str().expect("prompt message");
|
||||
assert!(message.contains("MARKER-FROM-CLAUDE-MD"));
|
||||
assert!(message.contains("MARKER-FROM-CLAW-MD"));
|
||||
assert!(message.contains("MARKER-FROM-AGENTS-MD"));
|
||||
assert_eq!(prompt["memory_file_count"], 3);
|
||||
assert_eq!(prompt["memory_files"][1]["source"], "claw_md");
|
||||
|
||||
let doctor = assert_json_command_with_env(&root, &["--output-format", "json", "doctor"], &envs);
|
||||
let memory = doctor["checks"]
|
||||
.as_array()
|
||||
.expect("doctor checks")
|
||||
.iter()
|
||||
.find(|check| check["name"] == "memory")
|
||||
.expect("memory check");
|
||||
assert_eq!(memory["status"], "ok");
|
||||
assert_eq!(memory["memory_file_count"], 3);
|
||||
assert_eq!(memory["memory_files"][2]["source"], "agents_md");
|
||||
assert!(memory["unloaded_memory_files"]
|
||||
.as_array()
|
||||
.expect("unloaded memory files")
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dump_manifests_and_init_emit_json_when_requested() {
|
||||
let root = unique_temp_dir("manifest-init-json");
|
||||
@@ -1325,7 +1390,7 @@ fn doctor_and_resume_status_emit_json_when_requested() {
|
||||
.is_some_and(|available| available.iter().any(|name| name == "web_fetch")));
|
||||
|
||||
let checks = doctor["checks"].as_array().expect("doctor checks");
|
||||
assert_eq!(checks.len(), 8);
|
||||
assert_eq!(checks.len(), 9);
|
||||
let check_names = checks
|
||||
.iter()
|
||||
.map(|check| {
|
||||
@@ -1348,6 +1413,7 @@ fn doctor_and_resume_status_emit_json_when_requested() {
|
||||
"config",
|
||||
"install source",
|
||||
"workspace",
|
||||
"memory",
|
||||
"boot preflight",
|
||||
"sandbox",
|
||||
"permissions",
|
||||
|
||||
Reference in New Issue
Block a user