diff --git a/AGENTS.md b/AGENTS.md index 905bf0c..9370280 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,7 @@ pi/ skills/ source of truth for agent skill templates control-agent/ orchestration agent HEARTBEAT.md health check checklist (deployed to ~/.pi/agent/) + memory/ seed files for persistent memory dev-agent/ coding agent sentry-agent/ monitoring/triage agent settings.json pi agent settings @@ -70,6 +71,7 @@ Agent runtime layout: │ ├── extensions/ deployed extensions │ ├── skills/ agent-owned (can modify freely) │ ├── HEARTBEAT.md periodic health check checklist (admin-managed) +│ ├── memory/ persistent agent memory (agent-owned, survives deploys) │ ├── baudbot-version.json deploy version (git SHA, timestamp) │ └── baudbot-manifest.json SHA256 hashes of all deployed files ├── workspace/ project repos + git worktrees diff --git a/README.md b/README.md index d23a1eb..ea3ba34 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,18 @@ The control agent runs a periodic heartbeat loop (default: every 10 minutes) tha - Are there stale worktrees or stuck todos? The checklist lives in `HEARTBEAT.md` — edit it to add custom checks. The heartbeat extension (`heartbeat.ts`) handles scheduling, error backoff, and the `heartbeat` tool for runtime control. If the checklist is empty, no heartbeat fires (saves tokens). +### Persistent Memory + +Agents maintain persistent memory across session restarts via Markdown files in `~/.pi/agent/memory/`: + +| File | What it stores | +|------|---------------| +| `operational.md` | Infrastructure learnings, common errors and fixes | +| `repos.md` | Per-repo build quirks, CI gotchas, architecture notes | +| `users.md` | User preferences, timezone, communication style | +| `incidents.md` | Past incidents: what broke, root cause, how it was fixed | + +Memory files are agent-owned — agents read them on startup and update them as they learn. Deploy seeds the files on first install but never overwrites existing content. ## Architecture @@ -127,6 +139,7 @@ baudbot_agent (unprivileged uid) │ ├── extensions/ deployed extensions (read-only) │ ├── skills/ agent-owned (can modify) │ ├── HEARTBEAT.md periodic health check checklist +│ ├── memory/ persistent agent memory (agent-owned) │ └── baudbot-manifest.json SHA256 integrity hashes ├── ~/workspace/ project repos + worktrees └── ~/.config/.env secrets (600 perms) diff --git a/bin/deploy.sh b/bin/deploy.sh index 8c63ac3..8db219e 100755 --- a/bin/deploy.sh +++ b/bin/deploy.sh @@ -158,6 +158,28 @@ else log "would copy: HEARTBEAT.md" fi +# ── Memory Seeds ───────────────────────────────────────────────────────────── + +echo "Deploying memory seeds..." + +MEMORY_SEED_DIR="$STAGE_DIR/skills/control-agent/memory" +MEMORY_DEST="$BAUDBOT_HOME/.pi/agent/memory" + +if [ "$DRY_RUN" -eq 0 ]; then + # Memory seeds — only copy if files don't already exist (agent-owned, don't clobber) + as_agent mkdir -p "$MEMORY_DEST" + if [ -d "$MEMORY_SEED_DIR" ]; then + for seed in "$MEMORY_SEED_DIR"/*.md; do + [ -f "$seed" ] || continue + base=$(basename "$seed") + as_agent bash -c "[ -f '$MEMORY_DEST/$base' ] || cp '$seed' '$MEMORY_DEST/$base'" + log "✓ memory/$base (seed, won't overwrite)" + done + fi +else + log "would seed: memory/*.md (only if missing)" +fi + # ── Slack Bridge ───────────────────────────────────────────────────────────── echo "Deploying slack-bridge..." diff --git a/bin/test.sh b/bin/test.sh index bbcac94..d6cc8cf 100755 --- a/bin/test.sh +++ b/bin/test.sh @@ -50,6 +50,7 @@ if [ "$FILTER" = "all" ] || [ "$FILTER" = "js" ]; then echo "JS/TS:" run "tool-guard" node --test pi/extensions/tool-guard.test.mjs run "heartbeat" node --test pi/extensions/heartbeat.test.mjs + run "memory" node --test pi/extensions/memory.test.mjs run "bridge security" node --test slack-bridge/security.test.mjs run "extension scanner" node --test bin/scan-extensions.test.mjs echo "" diff --git a/pi/extensions/memory.test.mjs b/pi/extensions/memory.test.mjs new file mode 100644 index 0000000..b39c844 --- /dev/null +++ b/pi/extensions/memory.test.mjs @@ -0,0 +1,395 @@ +/** + * Tests for persistent agent memory. + * + * Tests the memory seed files, deploy logic, and skill file integration. + * Memory is a convention (Markdown files + deploy script), not a runtime + * module, so we test file structure, content, and deploy behavior. + * + * Run: node --test pi/extensions/memory.test.mjs + */ + +import { describe, it, beforeEach, afterEach } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import os from "node:os"; +import { execSync } from "node:child_process"; + +// ── Paths ─────────────────────────────────────────────────────────────────── + +const REPO_ROOT = path.resolve( + path.dirname(new URL(import.meta.url).pathname), + "../.." +); +const MEMORY_SEED_DIR = path.join( + REPO_ROOT, + "pi/skills/control-agent/memory" +); +const EXPECTED_SEED_FILES = [ + "operational.md", + "repos.md", + "users.md", + "incidents.md", +]; + +// ── Test helpers ──────────────────────────────────────────────────────────── + +let tmpDir; + +function setup() { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "memory-test-")); +} + +function teardown() { + fs.rmSync(tmpDir, { recursive: true, force: true }); +} + +// ── Tests: seed files exist and have correct structure ────────────────────── + +describe("memory: seed files exist", () => { + it("memory seed directory exists", () => { + assert.ok( + fs.existsSync(MEMORY_SEED_DIR), + `Memory seed directory should exist at ${MEMORY_SEED_DIR}` + ); + }); + + for (const file of EXPECTED_SEED_FILES) { + it(`seed file exists: ${file}`, () => { + const filePath = path.join(MEMORY_SEED_DIR, file); + assert.ok(fs.existsSync(filePath), `${file} should exist`); + }); + } + + it("no unexpected files in memory seed directory", () => { + const actual = fs + .readdirSync(MEMORY_SEED_DIR) + .filter((f) => f.endsWith(".md")) + .sort(); + const expected = [...EXPECTED_SEED_FILES].sort(); + assert.deepEqual(actual, expected); + }); +}); + +describe("memory: seed file content", () => { + for (const file of EXPECTED_SEED_FILES) { + it(`${file} is valid Markdown with a heading`, () => { + const content = fs.readFileSync( + path.join(MEMORY_SEED_DIR, file), + "utf-8" + ); + assert.ok(content.length > 0, "file should not be empty"); + assert.ok(content.startsWith("#"), "file should start with a Markdown heading"); + }); + + it(`${file} contains no-secrets warning`, () => { + const content = fs.readFileSync( + path.join(MEMORY_SEED_DIR, file), + "utf-8" + ); + assert.ok( + content.includes("DO NOT store secrets") || + content.includes("do not store secrets"), + `${file} should contain a no-secrets warning` + ); + }); + + it(`${file} does not contain actual secrets`, () => { + const content = fs.readFileSync( + path.join(MEMORY_SEED_DIR, file), + "utf-8" + ); + // Check for common secret patterns + assert.ok(!content.match(/sk-ant-[a-zA-Z0-9]/), "should not contain Anthropic keys"); + assert.ok(!content.match(/sk-[a-zA-Z0-9]{20,}/), "should not contain OpenAI keys"); + assert.ok(!content.match(/ghp_[a-zA-Z0-9]{20,}/), "should not contain GitHub tokens"); + assert.ok(!content.match(/xoxb-[a-zA-Z0-9]/), "should not contain Slack tokens"); + assert.ok(!content.match(/xapp-[a-zA-Z0-9]/), "should not contain Slack app tokens"); + }); + + it(`${file} contains only example/template content (comments)`, () => { + const content = fs.readFileSync( + path.join(MEMORY_SEED_DIR, file), + "utf-8" + ); + // Meaningful lines (not headings, not empty, not comments, not HTML comments) + const meaningful = content + .split("\n") + .filter((line) => { + const trimmed = line.trim(); + if (trimmed.length === 0) return false; + if (trimmed.startsWith("#")) return false; + if (trimmed.startsWith("")) return false; + if (trimmed.startsWith("**DO NOT")) return false; + return true; + }); + // Should only have template/description text, not real entries + for (const line of meaningful) { + assert.ok( + !line.match(/^## \d{4}-\d{2}-\d{2}/), + `${file} should not contain real dated entries (found: ${line})` + ); + } + }); + } +}); + +describe("memory: repos.md has known repo sections", () => { + it("contains modem section", () => { + const content = fs.readFileSync( + path.join(MEMORY_SEED_DIR, "repos.md"), + "utf-8" + ); + assert.ok(content.includes("## modem"), "should have modem section"); + }); + + it("contains website section", () => { + const content = fs.readFileSync( + path.join(MEMORY_SEED_DIR, "repos.md"), + "utf-8" + ); + assert.ok(content.includes("## website"), "should have website section"); + }); + + it("contains baudbot section", () => { + const content = fs.readFileSync( + path.join(MEMORY_SEED_DIR, "repos.md"), + "utf-8" + ); + assert.ok(content.includes("## baudbot"), "should have baudbot section"); + }); +}); + +// ── Tests: deploy script seeds correctly ──────────────────────────────────── + +describe("memory: deploy seeding logic", () => { + beforeEach(setup); + afterEach(teardown); + + it("copies seed files to empty destination", () => { + const destDir = path.join(tmpDir, "memory"); + fs.mkdirSync(destDir, { recursive: true }); + + for (const file of EXPECTED_SEED_FILES) { + const src = path.join(MEMORY_SEED_DIR, file); + const dest = path.join(destDir, file); + // Simulate: [ -f dest ] || cp src dest + if (!fs.existsSync(dest)) { + fs.copyFileSync(src, dest); + } + } + + for (const file of EXPECTED_SEED_FILES) { + assert.ok( + fs.existsSync(path.join(destDir, file)), + `${file} should be seeded` + ); + } + }); + + it("does NOT overwrite existing files", () => { + const destDir = path.join(tmpDir, "memory"); + fs.mkdirSync(destDir, { recursive: true }); + + // Pre-populate with agent-modified content + const customContent = "# Operational Learnings\n\n## 2026-02-17\n- Custom entry\n"; + fs.writeFileSync(path.join(destDir, "operational.md"), customContent); + + // Run the seed logic + for (const file of EXPECTED_SEED_FILES) { + const src = path.join(MEMORY_SEED_DIR, file); + const dest = path.join(destDir, file); + // Simulate: [ -f dest ] || cp src dest + if (!fs.existsSync(dest)) { + fs.copyFileSync(src, dest); + } + } + + // operational.md should keep agent's custom content + const result = fs.readFileSync( + path.join(destDir, "operational.md"), + "utf-8" + ); + assert.equal(result, customContent, "should NOT overwrite existing file"); + + // Other files should be seeded (they didn't exist) + for (const file of EXPECTED_SEED_FILES) { + assert.ok( + fs.existsSync(path.join(destDir, file)), + `${file} should exist after seeding` + ); + } + }); + + it("handles partial seeding (some files exist, some don't)", () => { + const destDir = path.join(tmpDir, "memory"); + fs.mkdirSync(destDir, { recursive: true }); + + // Only repos.md exists + const customRepos = "# Repos\n\n## modem\n- Uses Next.js 15\n"; + fs.writeFileSync(path.join(destDir, "repos.md"), customRepos); + + for (const file of EXPECTED_SEED_FILES) { + const src = path.join(MEMORY_SEED_DIR, file); + const dest = path.join(destDir, file); + if (!fs.existsSync(dest)) { + fs.copyFileSync(src, dest); + } + } + + // repos.md should keep custom content + assert.equal( + fs.readFileSync(path.join(destDir, "repos.md"), "utf-8"), + customRepos + ); + + // Others should be seeded from templates + for (const file of ["operational.md", "users.md", "incidents.md"]) { + const content = fs.readFileSync(path.join(destDir, file), "utf-8"); + const seed = fs.readFileSync(path.join(MEMORY_SEED_DIR, file), "utf-8"); + assert.equal(content, seed, `${file} should match seed template`); + } + }); +}); + +// ── Tests: skill files reference memory correctly ─────────────────────────── + +describe("memory: skill file integration", () => { + const controlSkill = fs.readFileSync( + path.join(REPO_ROOT, "pi/skills/control-agent/SKILL.md"), + "utf-8" + ); + const devSkill = fs.readFileSync( + path.join(REPO_ROOT, "pi/skills/dev-agent/SKILL.md"), + "utf-8" + ); + const sentrySkill = fs.readFileSync( + path.join(REPO_ROOT, "pi/skills/sentry-agent/SKILL.md"), + "utf-8" + ); + + it("control-agent SKILL.md has Memory section", () => { + assert.ok(controlSkill.includes("## Memory"), "should have Memory section"); + }); + + it("control-agent SKILL.md references memory directory", () => { + assert.ok( + controlSkill.includes("~/.pi/agent/memory/"), + "should reference memory directory" + ); + }); + + it("control-agent SKILL.md lists all memory files", () => { + assert.ok(controlSkill.includes("operational.md"), "should list operational.md"); + assert.ok(controlSkill.includes("repos.md"), "should list repos.md"); + assert.ok(controlSkill.includes("users.md"), "should list users.md"); + assert.ok(controlSkill.includes("incidents.md"), "should list incidents.md"); + }); + + it("control-agent startup checklist includes memory read", () => { + assert.ok( + controlSkill.includes("Read memory files"), + "startup checklist should include memory read" + ); + }); + + it("control-agent SKILL.md warns against storing secrets", () => { + assert.ok( + controlSkill.includes("Never store secrets"), + "should warn against storing secrets in memory" + ); + }); + + it("dev-agent SKILL.md has Memory section", () => { + assert.ok(devSkill.includes("## Memory"), "should have Memory section"); + }); + + it("dev-agent SKILL.md references repos.md", () => { + assert.ok( + devSkill.includes("repos.md"), + "should reference repos.md" + ); + }); + + it("dev-agent SKILL.md warns against storing secrets", () => { + assert.ok( + devSkill.includes("Never store secrets"), + "should warn against storing secrets" + ); + }); + + it("sentry-agent SKILL.md has Memory section", () => { + assert.ok(sentrySkill.includes("## Memory"), "should have Memory section"); + }); + + it("sentry-agent SKILL.md references incidents.md", () => { + assert.ok( + sentrySkill.includes("incidents.md"), + "should reference incidents.md" + ); + }); + + it("sentry-agent startup reads incident history", () => { + assert.ok( + sentrySkill.includes("Read incident history"), + "startup should read incident history" + ); + }); + + it("sentry-agent SKILL.md warns against storing secrets", () => { + assert.ok( + sentrySkill.includes("Never store secrets"), + "should warn against storing secrets" + ); + }); +}); + +// ── Tests: deploy.sh has memory seeding ───────────────────────────────────── + +describe("memory: deploy.sh integration", () => { + const deployScript = fs.readFileSync( + path.join(REPO_ROOT, "bin/deploy.sh"), + "utf-8" + ); + + it("deploy.sh has memory seeds section", () => { + assert.ok( + deployScript.includes("Memory Seeds") || deployScript.includes("memory seeds"), + "deploy.sh should have a memory seeds section" + ); + }); + + it("deploy.sh creates memory destination directory", () => { + assert.ok( + deployScript.includes("mkdir -p") && + deployScript.includes("memory"), + "deploy.sh should create memory directory" + ); + }); + + it("deploy.sh uses conditional copy (won't overwrite)", () => { + // The key pattern: [ -f dest ] || cp src dest + assert.ok( + deployScript.includes("-f") && deployScript.includes("|| cp"), + "deploy.sh should use conditional copy to avoid overwriting" + ); + }); +}); + +// ── Tests: setup.sh creates memory directory ──────────────────────────────── + +describe("memory: setup.sh integration", () => { + const setupScript = fs.readFileSync( + path.join(REPO_ROOT, "setup.sh"), + "utf-8" + ); + + it("setup.sh creates memory directory", () => { + assert.ok( + setupScript.includes("mkdir -p") && + setupScript.includes("memory"), + "setup.sh should create memory directory" + ); + }); +}); diff --git a/pi/skills/control-agent/SKILL.md b/pi/skills/control-agent/SKILL.md index 7b4ac86..bcf7742 100644 --- a/pi/skills/control-agent/SKILL.md +++ b/pi/skills/control-agent/SKILL.md @@ -59,6 +59,42 @@ You can control the heartbeat with the `heartbeat` tool: - `heartbeat trigger` — fire one immediately The checklist is admin-managed (`HEARTBEAT.md` is deployed by `deploy.sh`). If you need to add checks, note the request for the admin. +## Memory + +You have persistent memory that survives across session restarts. Memory files live in `~/.pi/agent/memory/` — read them on startup and update them as you learn. + +### Reading Memory + +On startup (after the checklist items), read all memory files to restore context: +```bash +ls ~/.pi/agent/memory/ +# Then read each .md file +``` + +### Memory Files + +| File | Purpose | +|------|---------| +| `operational.md` | Infrastructure learnings, common errors and fixes | +| `repos.md` | Per-repo knowledge: build quirks, CI gotchas, architecture notes | +| `users.md` | User preferences: communication style, timezone, priorities | +| `incidents.md` | Past incidents: what broke, root cause, how it was fixed | + +### Updating Memory + +When you learn something new, append it to the appropriate file under a dated heading: +```markdown +## 2026-02-17 +- Learned that XYZ causes ABC — fix is to do DEF +``` + +**Update memory when you:** +- Discover a new operational quirk or fix +- Learn a user preference from their feedback +- Resolve an incident (add root cause + fix) +- Discover a repo-specific build/CI/deploy detail + +**Never store secrets, API keys, or tokens in memory files.** ## Core Principles @@ -356,6 +392,7 @@ The script: - [ ] Run `list_sessions` — note live UUIDs, confirm `control-agent` is listed - [ ] Run `startup-cleanup.sh` with live UUIDs (cleans sockets + restarts Slack bridge) +- [ ] **Read memory files** — `ls ~/.pi/agent/memory/` then read each `.md` file to restore context from previous sessions - [ ] Verify `BAUDBOT_SECRET` env var is set - [ ] Create/verify inbox for `BAUDBOT_EMAIL` env var exists - [ ] Start email monitor (inline mode, **300s / 5 min**) diff --git a/pi/skills/control-agent/memory/incidents.md b/pi/skills/control-agent/memory/incidents.md new file mode 100644 index 0000000..53fe021 --- /dev/null +++ b/pi/skills/control-agent/memory/incidents.md @@ -0,0 +1,13 @@ +# Incident History + +Past incidents: what broke, root cause, how it was fixed, and what to watch for. +Use this to recognize recurring patterns and avoid re-investigating known issues. + +**DO NOT store secrets, API keys, or tokens in this file.** + + diff --git a/pi/skills/control-agent/memory/operational.md b/pi/skills/control-agent/memory/operational.md new file mode 100644 index 0000000..55df260 --- /dev/null +++ b/pi/skills/control-agent/memory/operational.md @@ -0,0 +1,12 @@ +# Operational Learnings + +Record things you learn about running agents, common errors and fixes, and infrastructure quirks. +Add entries under dated headings. Keep entries concise — one line per learning. + +**DO NOT store secrets, API keys, or tokens in this file.** + + diff --git a/pi/skills/control-agent/memory/repos.md b/pi/skills/control-agent/memory/repos.md new file mode 100644 index 0000000..0d812af --- /dev/null +++ b/pi/skills/control-agent/memory/repos.md @@ -0,0 +1,28 @@ +# Repository Knowledge + +Per-repo knowledge: build quirks, CI gotchas, architecture notes, common failure modes. + +**DO NOT store secrets, API keys, or tokens in this file.** + +## modem + + + +## website + + + +## baudbot + + diff --git a/pi/skills/control-agent/memory/users.md b/pi/skills/control-agent/memory/users.md new file mode 100644 index 0000000..bcd98ae --- /dev/null +++ b/pi/skills/control-agent/memory/users.md @@ -0,0 +1,13 @@ +# User Preferences + +Communication preferences, timezone, priorities, and working patterns for each user. + +**DO NOT store secrets, API keys, or tokens in this file.** + + diff --git a/pi/skills/dev-agent/SKILL.md b/pi/skills/dev-agent/SKILL.md index d7acb5f..b535f07 100644 --- a/pi/skills/dev-agent/SKILL.md +++ b/pi/skills/dev-agent/SKILL.md @@ -56,6 +56,19 @@ These are enforced by three layers: 2. **Tool-guard** — blocks write/edit tool calls to protected paths 3. **Pre-commit hook** — blocks git commits of protected files +## Memory + +Before starting work, check for repo-specific knowledge in the shared memory store: +```bash +cat ~/.pi/agent/memory/repos.md 2>/dev/null || true +``` + +This file contains per-repo build quirks, CI gotchas, and architecture notes learned by previous agents. Use this context to avoid known pitfalls. + +When you discover something new about a repo (build quirk, CI gotcha, dependency issue), append it to `~/.pi/agent/memory/repos.md` under the appropriate repo heading before reporting completion to Baudbot. + +**Never store secrets, API keys, or tokens in memory files.** + ## Startup On startup, immediately: diff --git a/pi/skills/sentry-agent/SKILL.md b/pi/skills/sentry-agent/SKILL.md index 96be815..685702d 100644 --- a/pi/skills/sentry-agent/SKILL.md +++ b/pi/skills/sentry-agent/SKILL.md @@ -19,14 +19,31 @@ Triage and investigate Sentry alerts on demand. You receive alerts forwarded by You do **NOT** poll — you are idle until the control-agent sends you an alert. This saves tokens. +## Memory + +On startup, check for past incident history: +```bash +cat ~/.pi/agent/memory/incidents.md 2>/dev/null || true +``` + +This file contains records of past incidents — what broke, root cause, and how it was fixed. Use this to: +- Recognize recurring patterns (e.g. "this same null access error happened before in PR #142") +- Avoid re-investigating known issues +- Provide richer triage context to the control-agent + +When you investigate a new incident and find a root cause, append it to `~/.pi/agent/memory/incidents.md` with the date, issue title, root cause, fix, and what to watch for. + +**Never store secrets, API keys, or tokens in memory files.** + ## Startup When this skill is loaded: -1. Verify `SENTRY_AUTH_TOKEN` is set (needed for `sentry_monitor get`) -2. The `#bots-sentry` channel ID is configured via `SENTRY_CHANNEL_ID` env var -3. Acknowledge readiness to the control-agent -4. Stand by for incoming alerts +1. **Read incident history** — `cat ~/.pi/agent/memory/incidents.md 2>/dev/null || true` +2. Verify `SENTRY_AUTH_TOKEN` is set (needed for `sentry_monitor get`) +3. The `#bots-sentry` channel ID is configured via `SENTRY_CHANNEL_ID` env var +4. Acknowledge readiness to the control-agent +5. Stand by for incoming alerts ## Triage Guidelines diff --git a/setup.sh b/setup.sh index eb35531..cd0fc1f 100755 --- a/setup.sh +++ b/setup.sh @@ -174,6 +174,7 @@ echo "=== Setting up runtime directories ===" sudo -u baudbot_agent bash -c ' mkdir -p ~/.pi/agent/extensions mkdir -p ~/.pi/agent/skills + mkdir -p ~/.pi/agent/memory mkdir -p ~/runtime/slack-bridge '