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:
@@ -6389,7 +6389,7 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
|
||||
437. **DONE — `version --output-format json` exposes complete build provenance without duplicating prose** — fixed 2026-06-04 in `fix: expose complete version provenance`. Build metadata now records the full 40-character `git_sha`, separate derived `git_sha_short`, `is_dirty`, `branch`, ISO-8601 `commit_date`, Unix `commit_timestamp`, `rustc_version`, target, and build date. Version JSON exposes those fields at top level and mirrors them under `binary_provenance`; `workspace_git_sha` is also a full SHA and `workspace_match` now compares full commit identities. `executable_path` is resolved at runtime with `std::env::current_exe()` instead of reporting a compile-host path. The prose report is no longer duplicated in JSON as `message`; JSON callers get the secondary text block as `human_readable`. Docs in `USAGE.md` and `rust/README.md` describe the provenance contract. Regression coverage: `version_emits_json_when_requested`, `version_status_doctor_include_binary_provenance_797`, `resumed_version_and_init_emit_structured_json_when_requested`, and `resumed_version_command_emits_structured_json`.
|
||||
|
||||
|
||||
438. **Memory file discovery only recognizes `CLAUDE.md` — `AGENTS.md` (industry convention used by OpenCode/Codex/Aider/Cursor) and `CLAW.md` (project's own brand name) are silently ignored despite being present in the workspace** — dogfooded 2026-05-11 by Jobdori on `d3a982dd` in response to Clawhip pinpoint nudge at `1503328341422244012`. Reproduction (fresh empty dir, isolated `CLAW_CONFIG_HOME`): create three files in cwd — `CLAUDE.md` (marker `MARKER-FROM-CLAUDE-MD`), `AGENTS.md` (marker `MARKER-FROM-AGENTS-MD`), `CLAW.md` (marker `MARKER-FROM-CLAW-MD`). Run `claw status --output-format json` → `workspace.memory_file_count: 1`. Run `claw system-prompt --output-format json` and search the `message` field for each marker: only `MARKER-FROM-CLAUDE-MD` is found; `MARKER-FROM-AGENTS-MD` and `MARKER-FROM-CLAW-MD` are absent. `claw-code` exclusively recognizes the Claude-branded filename inherited from upstream Claude Code; the project's own `CLAW.md` brand name and the cross-tool industry convention `AGENTS.md` are both silently dropped. **Three sibling implications:** (a) **brand-consistency gap**: a project rebranded from Claude Code to Claw Code that introduces `CLAUDE.md` as its only memory file is internally inconsistent. Users naturally expect `claw <subcommand>` to read `CLAW.md`. (b) **industry-convention gap**: `AGENTS.md` is the convergent convention for OpenCode (oh-my-opencode/sisyphus), OpenAI Codex CLI, Aider, Cursor, Continue.dev, and most ACP harnesses. Users with mixed-tool workflows maintain a shared `AGENTS.md` and expect every AI coding tool to honor it. (c) **silent failure mode**: there is no warning when `AGENTS.md` or `CLAW.md` exist but are not loaded. Users who copy-paste `AGENTS.md` from another tool's docs see `memory_file_count` stay at 0 or 1 and have to guess why their instructions aren't applied. **Required fix shape:** (a) discover and load **`CLAUDE.md`, `CLAW.md`, `AGENTS.md`** in that priority order (existing config-precedence pattern); (b) all three contribute to `memory_file_count` with `memory_files:[{path, source:"claude_md"|"claw_md"|"agents_md", chars}]` array exposed in `status --output-format json`; (c) when multiple files exist, merge or document the precedence: project-specific `CLAUDE.md`/`CLAW.md` overrides industry-shared `AGENTS.md`; (d) `claw doctor --output-format json` adds a `memory` check that warns when `AGENTS.md` exists but is not the loaded variant (alerting users that they may be relying on the wrong file); (e) regression test: workspace with all three files results in `memory_file_count >= 1` and the system prompt contains markers from at least the highest-precedence file. **Why this matters:** `AGENTS.md` is the lingua-franca instruction file for cross-tool AI coding workflows. A team using OpenCode for one project and Claw Code for another keeps their conventions in a shared `AGENTS.md`. Forcing them to also maintain a `CLAUDE.md` for claw-code (with identical content) is friction that breaks the value proposition of a fork. Cross-references #438 itself (the multi-file convention), and AGENTS.md ecosystem references in oh-my-opencode/sisyphus docs. Source: Jobdori live dogfood, `d3a982dd`, 2026-05-11.
|
||||
438. **DONE — memory discovery loads `CLAUDE.md`, `CLAW.md`, and `AGENTS.md` with structured provenance** — fixed 2026-06-04 in `fix: load Claw and Agents memory files`. Project memory discovery now checks root instruction files in `CLAUDE.md`, `CLAW.md`, then `AGENTS.md` order for each discovered directory, preserves existing scoped `.claw/CLAUDE.md`, `.claude/CLAUDE.md`, `.claw/instructions.md`, and rules-directory imports, and exposes each loaded file's `path`, `source`, `chars`, and `contributes` in `status --output-format json` as `workspace.memory_files[]`. `system-prompt --output-format json` returns the same memory metadata alongside the rendered `message`/`sections`, and all non-duplicate loaded files contribute to the prompt so CLAUDE/CLAW/AGENTS markers are visible together. `claw doctor --output-format json` now includes a dedicated `memory` check with loaded memory metadata and `unloaded_memory_files[]` warnings for present `CLAW.md`/`AGENTS.md` candidates that were skipped (for example empty or duplicate-content variants). Docs in `USAGE.md` and `rust/README.md` describe the priority and JSON contracts. Regression coverage: `discovers_claude_claw_agents_and_dot_claude_instruction_files_together`, `memory_files_load_claude_claw_agents_and_surface_json_438`, and `memory_health_surfaces_loaded_and_unloaded_files_438`.
|
||||
|
||||
|
||||
439. **Memory file discovery walks ALL ancestor directories up to `$HOME` boundary, silently loading any `CLAUDE.md` it finds — `/tmp/CLAUDE.md` left from a previous test silently bleeds into every project under `/tmp/*/`; no `--no-parent-memory` flag, no `.no-claude-md-boundary` marker file to limit discovery scope** — dogfooded 2026-05-11 by Jobdori on `f4a96740` in response to Clawhip pinpoint nudge at `1503335892461293675`. Reproduction: create three nested `CLAUDE.md` files with unique markers — `/tmp/claw-nested-probe/CLAUDE.md` (`PARENT_CLAUDE`), `subproj/CLAUDE.md` (`CHILD_CLAUDE`), `subproj/deep/CLAUDE.md` (`DEEP_CLAUDE`). Run `claw system-prompt --output-format json` from `subproj/deep/nest/` (note: `nest` has no `CLAUDE.md`). The `message` field contains **all three markers** (PARENT + CHILD + DEEP) and `status --output-format json` reports `memory_file_count: 3`. Boundary tests: (a) `$HOME/CLAUDE.md` is NOT picked up from `/tmp/no-claude-dir` (discovery stops at `$HOME` boundary, good); (b) From `/tmp/deep` (no nested CLAUDE.md), `/tmp/CLAUDE.md` IS picked up (count: 1); (c) git-root is NOT a discovery boundary — running from a git subdir still walks above the git root. **Ambient-context-bleed footgun:** any stale `/tmp/CLAUDE.md` (or `/home/<user>/projects/CLAUDE.md`, or any ancestor-path CLAUDE.md left over from a previous experiment, copy-paste, or AI-generated example) silently bleeds into every workspace nested below it. The user has no signal in `status --output-format json` indicating which ancestor file is contributing — only the aggregate `memory_file_count`. **Three required fixes:** (a) **expose discovery list**: `status --output-format json` and `system-prompt --output-format json` must include `memory_files:[{path, source:"workspace"|"ancestor"|"parent_dir"|"home", chars, contributes:bool}]` so users can see what's leaking in; (b) **add `--no-parent-memory` flag** to limit discovery to cwd only (no ancestor walk), or add a boundary marker (`.claude-no-walk`, `.claw-root`, or honor `.git` as the boundary by default — most users expect repo-root scope); (c) **`doctor` warns** when ancestor `CLAUDE.md` files are loaded from outside the current git repo (suggests they may be unintentional). **Sibling discovery scope question:** discovery walks up to `$HOME` — but for a user with a project at `/Users/foo/work/proj`, that's `/Users/foo/work/CLAUDE.md` + `/Users/foo/CLAUDE.md` (if it exists) both load. The home boundary is exclusive, but the entire `/Users/foo` tree under home is in scope. **Why this matters:** test workspaces, scratch dirs, AI-generated example projects, and shared `/tmp` workdirs are full of stale `CLAUDE.md` files. The current discovery rule means every claw invocation can silently inherit context from arbitrary ancestor paths. Cross-references #438 (memory discovery only finds CLAUDE.md, not AGENTS.md or CLAW.md), #421 (cwd canonicalization leak — the canonicalized form determines which ancestor walk path is used). Source: Jobdori live dogfood, `f4a96740`, 2026-05-11.
|
||||
|
||||
6
USAGE.md
6
USAGE.md
@@ -51,7 +51,7 @@ cd rust
|
||||
```
|
||||
|
||||
**Note:** Diagnostic verbs (`doctor`, `status`, `sandbox`, `version`) support `--output-format json` for machine-readable output. Invalid suffix arguments (e.g., `--json`) are now rejected at parse time rather than falling through to prompt dispatch.
|
||||
`version --output-format json` reports structured build provenance including full `git_sha`, derived `git_sha_short`, `is_dirty`, `branch`, `commit_date`, `commit_timestamp`, `rustc_version`, runtime `executable_path`, and `binary_provenance`; JSON keeps the prose report in `human_readable` instead of duplicating it under `message`.
|
||||
`version --output-format json` reports structured build provenance including full `git_sha`, derived `git_sha_short`, `is_dirty`, `branch`, `commit_date`, `commit_timestamp`, `rustc_version`, runtime `executable_path`, and `binary_provenance`; JSON keeps the prose report in `human_readable` instead of duplicating it under `message`. `status --output-format json` exposes `workspace.memory_files[]` with `path`, `source`, `chars`, and `contributes` for every loaded project memory file.
|
||||
|
||||
### Initialize a repository
|
||||
|
||||
@@ -594,11 +594,13 @@ Object-style matchers are optional. When present, they match tool names case-ins
|
||||
|
||||
## Project instruction rules
|
||||
|
||||
In addition to root instruction files such as `CLAUDE.md`, `AGENTS.md`, `.claw/CLAUDE.md`, `.claude/CLAUDE.md`, and `.claw/instructions.md`, `claw` loads sorted Markdown/text rule files from:
|
||||
In addition to root instruction files such as `CLAUDE.md`, `CLAW.md`, `AGENTS.md`, `.claw/CLAUDE.md`, `.claude/CLAUDE.md`, and `.claw/instructions.md`, `claw` loads sorted Markdown/text rule files from:
|
||||
|
||||
- `<repo>/.claw/rules/` (`.md`, `.txt`, `.mdc`) for shared project rules.
|
||||
- `<repo>/.claw/rules.local/` for personal local rules; this path is gitignored.
|
||||
|
||||
Root instruction-file priority is `CLAUDE.md`, then `CLAW.md`, then `AGENTS.md` for each discovered directory. All loaded files contribute to the system prompt and to `status --output-format json` as `workspace.memory_files:[{path, source, chars, contributes}]`; `claw doctor --output-format json` includes a `memory` check so automation can detect loaded and unexpected unloaded memory-file candidates without parsing prompt text.
|
||||
|
||||
By default, `claw` also imports detected rules from common AI coding tools such as Cursor (`.cursorrules`, `.cursor/rules/`), GitHub Copilot (`.github/copilot-instructions.md`), Windsurf, Plandex, and Crush. Control this with `rulesImport` in any settings file:
|
||||
|
||||
```json
|
||||
|
||||
@@ -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