fix: auto-bootstrapped memory was accidentally putting the MEMORY.md directly in the repo root rather than .coyote/memory/MEMORY.md
CI / All (ubuntu-latest) (push) Failing after 23s
CI / All (macos-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled

This commit is contained in:
2026-06-15 15:05:51 -06:00
parent 6ce69ee989
commit b927e2a200
2 changed files with 99 additions and 3 deletions
+9 -3
View File
@@ -11,19 +11,25 @@ pub(crate) const DEFAULT_SKILL_INSTRUCTIONS: &str = indoc! {"
pub(crate) const DEFAULT_MEMORY_INSTRUCTIONS: &str = indoc! {" pub(crate) const DEFAULT_MEMORY_INSTRUCTIONS: &str = indoc! {"
## Memory ## Memory
A persistent memory file system survives across sessions. The MEMORY.md content shown above is A persistent memory file system survives across sessions. The MEMORY.md content shown above is
your always-on context; put universal facts (user identity, hard rules, binding feedback) directly your always-on context (universal facts, hard rules, binding feedback). Drill files hold deeper,
in MEMORY.md so they appear on every turn. Drill files hold deeper, on-demand context. on-demand context that you fetch with `memory__read`.
Tools: Tools:
- `memory__read(name)`: Read a specific drill file's full content. - `memory__read(name)`: Read a specific drill file's full content.
- `memory__write(name, content, scope)`: Create or replace a drill file (scope: 'global' | 'workspace'). - `memory__write(name, content, scope)`: Create or replace a drill file (scope: 'global' | 'workspace').
The MEMORY.md index is appended automatically; do not also update the index by hand.
- `memory__edit_index(scope, content)`: Replace the entire MEMORY.md at the given scope.
Use this to add always-on facts, reorganize, prune stale entries, or fix descriptions.
- `memory__list()`: See all known drill files and their metadata. - `memory__list()`: See all known drill files and their metadata.
- `memory__lint()`: Health-check memory for orphans, broken links, oversized files. - `memory__lint()`: Health-check memory for orphans, broken links, oversized files.
RULES: RULES:
- Every interaction has two outputs: your answer AND any memory updates the conversation warrants. - Every interaction has two outputs: your answer AND any memory updates the conversation warrants.
Don't let learnings evaporate into chat history. Don't let learnings evaporate into chat history.
- When you create or modify a drill file, also update MEMORY.md so the index stays accurate. - All MEMORY.md edits MUST go through `memory__edit_index`. NEVER use `fs_write`, `fs_patch`,
or any other generic file tool on MEMORY.md — Coyote manages its location and a stray
MEMORY.md outside the managed path is invisible to memory.
- All drill files MUST go through `memory__write`. The index updates itself.
- Use [[wikilink]] notation in memory files to reference other memories by their `name:` slug. - Use [[wikilink]] notation in memory files to reference other memories by their `name:` slug.
- NEVER write secrets, credentials, or API keys to memory — memory is plaintext on disk. - NEVER write secrets, credentials, or API keys to memory — memory is plaintext on disk.
Use coyote's Vault for secrets. Use coyote's Vault for secrets.
+90
View File
@@ -129,6 +129,41 @@ pub fn memory_function_declarations() -> Vec<FunctionDeclaration> {
}, },
agent: false, agent: false,
}, },
FunctionDeclaration {
name: format!("{MEMORY_FUNCTION_PREFIX}edit_index"),
description:
"Replace the entire MEMORY.md index at the given scope. Use to add always-on facts, \
reorganize, prune stale entries, or fix descriptions. Coyote manages the path; \
NEVER use fs_write or any other generic file tool on MEMORY.md."
.to_string(),
parameters: JsonSchema {
type_value: Some("object".to_string()),
properties: Some(IndexMap::from([
(
"scope".to_string(),
JsonSchema {
type_value: Some("string".to_string()),
description: Some(
"Where to edit: 'global' (user-level) or 'workspace' (project-level)"
.into(),
),
..Default::default()
},
),
(
"content".to_string(),
JsonSchema {
type_value: Some("string".to_string()),
description: Some("Full new contents of MEMORY.md".into()),
..Default::default()
},
),
])),
required: Some(vec!["scope".to_string(), "content".to_string()]),
..Default::default()
},
agent: false,
},
] ]
} }
@@ -212,11 +247,33 @@ pub fn handle_memory_tool(ctx: &mut RequestContext, cmd_name: &str, args: &Value
"index_updated": index_updated, "index_updated": index_updated,
})) }))
} }
"edit_index" => {
let scope = arg_str(args, "scope")?;
let content = arg_str(args, "content")?;
let target_dir = match scope.as_str() {
"global" => paths::global_memory_dir(),
"workspace" => workspace_write_dir(&store, &cwd)?,
other => bail!("unknown scope '{}': use 'global' or 'workspace'", other),
};
let index_path = write_memory_index(&target_dir, &content)?;
Ok(json!({
"status": "ok",
"path": index_path.display().to_string(),
}))
}
"lint" => lint_memory(&store), "lint" => lint_memory(&store),
_ => bail!("unknown memory action: {action}"), _ => bail!("unknown memory action: {action}"),
} }
} }
fn write_memory_index(target_dir: &Path, content: &str) -> Result<PathBuf> {
fs::create_dir_all(target_dir)?;
let index_path = target_dir.join("MEMORY.md");
fs::write(&index_path, content)?;
Ok(index_path)
}
fn ensure_index_entry(index_path: &Path, name: &str, description: &str) -> Result<bool> { fn ensure_index_entry(index_path: &Path, name: &str, description: &str) -> Result<bool> {
let existing = fs::read_to_string(index_path).unwrap_or_default(); let existing = fs::read_to_string(index_path).unwrap_or_default();
let already_referenced = let already_referenced =
@@ -533,6 +590,39 @@ mod tests {
let _ = fs::remove_dir_all(&root); let _ = fs::remove_dir_all(&root);
} }
#[test]
fn write_memory_index_creates_dir_and_writes_content() {
let root = temp_root("write_index_create");
let target = root.join("nested").join(".coyote").join("memory");
let path =
write_memory_index(&target, "# Workspace Memory Index\n\n- [[foo]]: hello\n").unwrap();
assert_eq!(path, target.join("MEMORY.md"));
assert!(path.exists());
assert_eq!(
fs::read_to_string(&path).unwrap(),
"# Workspace Memory Index\n\n- [[foo]]: hello\n"
);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn write_memory_index_replaces_existing_content() {
let root = temp_root("write_index_replace");
fs::create_dir_all(&root).unwrap();
let index = root.join("MEMORY.md");
fs::write(&index, "# Old\n\n- [[stale]]: gone\n").unwrap();
let path = write_memory_index(&root, "# New\n").unwrap();
assert_eq!(path, index);
assert_eq!(fs::read_to_string(&path).unwrap(), "# New\n");
let _ = fs::remove_dir_all(&root);
}
#[test] #[test]
fn lint_flags_orphans_broken_links_and_oversized() { fn lint_flags_orphans_broken_links_and_oversized() {
let root = temp_root("lint"); let root = temp_root("lint");