[defaults]
verify = "cargo check"
agent = "codex"
[[task]]
name = "memory-types"
prompt = """Add Memory types to src/types.rs. Add these types AFTER the existing CompletionInfo struct (near the end of the file, before #[cfg(test)]):
```rust
/// Unique ID for a memory entry, prefixed with "m-"
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct MemoryId(pub String);
impl MemoryId {
pub fn generate() -> Self {
let val: u16 = rand::rng().random();
Self(format!("m-{val:04x}"))
}
pub fn as_str(&self) -> &str { &self.0 }
}
impl fmt::Display for MemoryId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum MemoryType {
Discovery, // Bug patterns, API behaviors, gotchas
Convention, // Code style, naming, architecture decisions
Lesson, // What worked/failed in past tasks
Fact, // Version, config, endpoint facts
}
impl MemoryType {
pub fn as_str(&self) -> &'static str {
match self {
Self::Discovery => "discovery",
Self::Convention => "convention",
Self::Lesson => "lesson",
Self::Fact => "fact",
}
}
pub fn parse_str(s: &str) -> Option<Self> {
match s {
"discovery" => Some(Self::Discovery),
"convention" => Some(Self::Convention),
"lesson" => Some(Self::Lesson),
"fact" => Some(Self::Fact),
_ => None,
}
}
pub fn label(&self) -> &'static str {
match self {
Self::Discovery => "DISC",
Self::Convention => "CONV",
Self::Lesson => "LSSN",
Self::Fact => "FACT",
}
}
}
impl fmt::Display for MemoryType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Memory {
pub id: MemoryId,
pub memory_type: MemoryType,
pub content: String,
pub source_task_id: Option<String>,
pub agent: Option<String>,
pub project_path: Option<String>,
pub content_hash: String,
pub created_at: DateTime<Local>,
pub expires_at: Option<DateTime<Local>>,
}
```
Do NOT modify any existing types. Just append the new types before the #[cfg(test)] block.
Also add a simple test for MemoryType::parse_str roundtrip inside the existing tests module.
"""
worktree = "feat/memory-types"
context = ["src/types.rs:TaskId,CompletionInfo"]
[[task]]
name = "memory-schema"
prompt = """Add memory table schema and CRUD operations to the store module.
1. In src/store/schema.rs:
- Add a CREATE TABLE for memories to CREATE_TABLES_SQL (append to the existing string):
```sql
CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY,
memory_type TEXT NOT NULL,
content TEXT NOT NULL,
source_task_id TEXT,
agent TEXT,
project_path TEXT,
content_hash TEXT NOT NULL,
created_at TEXT NOT NULL,
expires_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project_path);
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memory_type);
CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(content_hash);
```
- Also add the CREATE TABLE to the migrate() function as a fallback (like the existing CREATE_WORKGROUPS_SQL pattern).
- Add a `row_to_memory` function following the existing `row_to_task` / `row_to_event` pattern:
```rust
pub(super) fn row_to_memory(row: &Row) -> rusqlite::Result<Result<Memory>> {
Ok(Ok(Memory {
id: MemoryId(row.get::<_, String>(0)?),
memory_type: MemoryType::parse_str(&row.get::<_, String>(1)?).unwrap_or(MemoryType::Fact),
content: row.get(2)?,
source_task_id: row.get(3)?,
agent: row.get(4)?,
project_path: row.get(5)?,
content_hash: row.get(6)?,
created_at: parse_dt(&row.get::<_, String>(7)?),
expires_at: row.get::<_, Option<String>>(8)?.map(|s| parse_dt(&s)),
}))
}
```
2. In src/store/mutations.rs, add these methods to the Store impl:
- `insert_memory(&self, memory: &Memory) -> Result<()>` — INSERT OR IGNORE (dedupe by content_hash)
- `delete_memory(&self, id: &str) -> Result<()>` — DELETE by id
3. In src/store/queries.rs, add these methods to the Store impl:
- `list_memories(&self, project_path: Option<&str>, memory_type: Option<MemoryType>) -> Result<Vec<Memory>>` — list with optional filters, ordered by created_at DESC, excludes expired entries (where expires_at IS NULL or expires_at > now)
- `search_memories(&self, query: &str, project_path: Option<&str>, limit: usize) -> Result<Vec<Memory>>` — LIKE-based search on content field, excludes expired, ordered by created_at DESC
- `get_memory(&self, id: &str) -> Result<Option<Memory>>` — get by ID
Use the existing patterns in schema.rs/mutations.rs/queries.rs exactly. Import Memory, MemoryId, MemoryType from crate::types.
"""
worktree = "feat/memory-schema"
context = ["src/store/schema.rs", "src/store/mutations.rs", "src/store/queries.rs", "src/types.rs:Memory,MemoryId,MemoryType"]
[[task]]
name = "memory-cli"
prompt = """Create src/cmd/memory.rs — CLI handler for `aid memory` subcommand.
Follow the patterns in src/cmd/group.rs for structure. The module should export these functions:
```rust
pub fn add(store: &Store, memory_type: &str, content: &str, project_path: Option<&str>) -> Result<()>
pub fn list(store: &Store, memory_type: Option<&str>, project_path: Option<&str>) -> Result<()>
pub fn search(store: &Store, query: &str, project_path: Option<&str>) -> Result<()>
pub fn forget(store: &Store, id: &str) -> Result<()>
```
Implementation details:
- `add`: Parse memory_type string via MemoryType::parse_str (bail if invalid). Generate MemoryId. Compute content_hash as first 16 hex chars of a simple hash (use std::hash::Hasher + std::hash::Hash, DefaultHasher). Set project_path from arg or detect from git root. Insert via store.insert_memory(). Print "Memory {id} saved ({type})".
- `list`: Call store.list_memories() with filters. Print table: ID | TYPE | CONTENT (truncated to 60 chars) | AGE | SOURCE. Use the label() method for type display.
- `search`: Call store.search_memories() with limit 20. Print same table format.
- `forget`: Call store.delete_memory(). Print "Memory {id} forgotten".
File header:
```rust
// CLI handler for `aid memory` — add, list, search, forget memories.
// Exports: add, list, search, forget.
// Deps: store::Store, types::{Memory, MemoryId, MemoryType}.
```
Keep it under 120 lines. Use chrono::Local for timestamps.
Also register the module in src/cmd/mod.rs by adding `pub mod memory;` after the existing modules.
"""
worktree = "feat/memory-cli"
context = ["src/cmd/group.rs", "src/cmd/mod.rs"]
[[task]]
name = "memory-inject"
prompt = """Add memory auto-injection to the prompt building pipeline in src/cmd/run_prompt.rs.
Create a new helper function `inject_memories` and call it from `build_prompt_bundle`.
1. Add a new function in src/cmd/run_prompt.rs:
```rust
/// Query relevant memories and inject them into the prompt.
fn inject_memories(store: &Store, prompt: &str, max_memories: usize) -> Result<Option<String>> {
// Detect project path from current git root
let project_path = detect_project_path();
// Search memories relevant to this prompt (keyword match)
let memories = store.search_memories(prompt, project_path.as_deref(), max_memories)?;
if memories.is_empty() {
return Ok(None);
}
// Format memories as a context block
let mut lines = vec!["[Agent Memory — knowledge from past tasks]".to_string()];
for mem in &memories {
lines.push(format!("- [{}] {}", mem.memory_type.label(), mem.content));
}
let token_count = crate::templates::estimate_tokens(&lines.join("\n"));
eprintln!("[aid] Injected {} memories (~{} tokens)", memories.len(), token_count);
Ok(Some(lines.join("\n")))
}
fn detect_project_path() -> Option<String> {
std::process::Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.output()
.ok()
.and_then(|o| if o.status.success() {
String::from_utf8(o.stdout).ok().map(|s| s.trim().to_string())
} else {
None
})
}
```
2. In `build_prompt_bundle`, after the existing `inject_skill` call (near the end), add:
```rust
// Inject relevant memories from past tasks
if let Some(memory_block) = inject_memories(store, &args.prompt, 10)? {
effective_prompt = format!("{memory_block}\n\n{effective_prompt}");
}
```
3. Also add a memory-save instruction to the effective prompt. After the milestone_instr line, append:
```rust
let memory_instr = "\n\nTo save knowledge for future tasks, write on its own line: [MEMORY: type] content\nValid types: discovery, convention, lesson, fact\nExample: [MEMORY: discovery] The auth module uses bcrypt, not argon2\n";
effective_prompt.push_str(memory_instr);
```
Keep changes minimal — only add the function and the two injection points.
"""
worktree = "feat/memory-inject"
context = ["src/cmd/run_prompt.rs", "src/store/queries.rs:search_memories"]
[[task]]
name = "memory-extract"
prompt = """Add post-task memory extraction to the watcher/completion pipeline.
Create a new module src/memory.rs that extracts [MEMORY: type] tags from agent output and saves them to the store.
1. Create src/memory.rs:
```rust
// Post-task memory extraction from agent output.
// Parses [MEMORY: type] tags and saves to store.
// Exports: extract_and_save_memories.
// Deps: store::Store, types::{Memory, MemoryId, MemoryType}.
use anyhow::Result;
use chrono::Local;
use std::hash::{Hash, Hasher};
use std::collections::hash_map::DefaultHasher;
use crate::store::Store;
use crate::types::*;
/// Parse [MEMORY: type] content lines from agent output text.
pub fn parse_memory_tags(text: &str) -> Vec<(MemoryType, String)> {
let mut results = Vec::new();
for line in text.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("[MEMORY:") {
if let Some(close) = rest.find(']') {
let type_str = rest[..close].trim();
let content = rest[close + 1..].trim();
if !content.is_empty() {
if let Some(mem_type) = MemoryType::parse_str(type_str) {
results.push((mem_type, content.to_string()));
}
}
}
}
}
results
}
/// Compute a content hash for deduplication.
fn content_hash(content: &str) -> String {
let mut hasher = DefaultHasher::new();
content.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
/// Extract memories from agent output and save to store.
pub fn extract_and_save_memories(
store: &Store,
output: &str,
task_id: &str,
agent: Option<&str>,
project_path: Option<&str>,
) -> Result<usize> {
let tags = parse_memory_tags(output);
let mut saved = 0;
for (mem_type, content) in &tags {
let hash = content_hash(content);
let expires_at = if *mem_type == MemoryType::Lesson {
Some(Local::now() + chrono::Duration::days(30))
} else {
None
};
let memory = Memory {
id: MemoryId::generate(),
memory_type: *mem_type,
content: content.clone(),
source_task_id: Some(task_id.to_string()),
agent: agent.map(|s| s.to_string()),
project_path: project_path.map(|s| s.to_string()),
content_hash: hash,
created_at: Local::now(),
expires_at,
};
store.insert_memory(&memory)?;
saved += 1;
}
if saved > 0 {
eprintln!("[aid] Extracted {saved} memories from task {task_id}");
}
Ok(saved)
}
```
2. Register the module in src/main.rs by adding `mod memory;` after the existing `mod hooks;` line.
3. In src/watcher.rs, find the completion handling section where CompletionInfo is processed. After the task status is updated, add a call to extract memories. You'll need to:
- Read the task's output (from output_path if it exists, or from the log file)
- Call `crate::memory::extract_and_save_memories(store, &output_text, task_id, agent_name, repo_path)`
- The exact integration point depends on the watcher code — look for where `update_task_completion` is called and add the extraction right after it.
If the watcher code is complex, just add the extraction in the simplest place where you have access to both the store and the task output. The key is that memory extraction happens automatically when a task completes.
Add unit tests for parse_memory_tags in the same file:
- Test basic parsing: `[MEMORY: discovery] Auth uses bcrypt` → (Discovery, "Auth uses bcrypt")
- Test multiple tags in one block
- Test invalid type is skipped
- Test empty content is skipped
"""
worktree = "feat/memory-extract"
context = ["src/watcher.rs:0-40", "src/main.rs:1-36"]
[[task]]
name = "memory-main"
prompt = """Wire the `aid memory` subcommand into main.rs CLI.
In src/main.rs:
1. Add a new Memory variant to the Commands enum (after the Store variant):
```rust
#[command(after_help = r#"Examples:
aid memory add discovery "The auth module uses bcrypt not argon2"
aid memory list --type convention
aid memory search "auth"
aid memory forget m-a3f1"#)]
/// Manage agent shared memory (discoveries, conventions, lessons)
Memory {
#[command(subcommand)]
action: MemoryCommands,
},
```
2. Add the MemoryCommands enum (after StoreCommands):
```rust
#[derive(Subcommand)]
enum MemoryCommands {
/// Add a memory entry
Add {
/// Memory type: discovery, convention, lesson, fact
#[arg(name = "TYPE")]
memory_type: String,
/// Content to remember
content: String,
},
/// List memories
List {
/// Filter by type
#[arg(long = "type")]
memory_type: Option<String>,
},
/// Search memories by keyword
Search {
/// Search query
query: String,
},
/// Delete a memory entry
Forget {
/// Memory ID (e.g. m-a3f1)
id: String,
},
}
```
3. Add the match arm in the main function (after the Store match arm):
```rust
Commands::Memory { action } => match action {
MemoryCommands::Add { memory_type, content } => {
cmd::memory::add(&store, &memory_type, &content, None)?;
}
MemoryCommands::List { memory_type } => {
cmd::memory::list(&store, memory_type.as_deref(), None)?;
}
MemoryCommands::Search { query } => {
cmd::memory::search(&store, &query, None)?;
}
MemoryCommands::Forget { id } => {
cmd::memory::forget(&store, &id)?;
}
},
```
Keep changes minimal — only add the enum variant, the subcommand enum, and the match arm.
"""
worktree = "feat/memory-main"
context = ["src/main.rs:54-60,367-398,400-410,663-672"]