The Knowledge Base (KB) is a PARA-structured persistent memory system for AI agents.
It survives agent compactions, session boundaries, and context window limits by storing
facts, decisions, architecture records, and project state in files on disk.
PARA stands for:
Projects — active deliverables with deadlines
Areas — ongoing responsibilities without end dates
Archives — stale content, git-preserved, never deleted
The KB works alongside the memory MCP tools (search_nodes, open_nodes,
read_graph, create_entities, add_observations, create_relations) and the
file-based scratchpad pattern. Use all three layers together:
Layer
Scope
Persistence
Example
KB files
Long-term org memory
Indefinite (git)
~/kb/projects/foo/facts.yaml
MCP knowledge graph
Relationship graph
Per-session + recall
Entities, relations, observations
Scratchpad file
Active session bridge
< 30 min
~/.kb-session-context.md
Workflow
The KB lifecycle follows a predictable loop across every agent session:
Session Start → Load Context → Check Decisions → Work & Capture → Log → Session End
Per-Session Loop
Cold start — MCP server connects, scratchpad is checked for < 30 min state
Load yesterday — last session's End-of-Session Summary is retrieved
Open decisions — all facts with status: open across every category
Work — agent performs tasks, captures facts as they arise
Log decisions — every resolved decision gets a fact with status: confirmed
Verify all code blocks in KB skills parse correctly
Querying the Knowledge Base
Using MCP Memory Tools
The memory MCP server exposes a knowledge graph. Query it with structured tools:
search_nodes — find entities by name or pattern:
# Python — via MCP
result = tool.memory_search_nodes(query="client-alpha")
for node in result.nodes:
print(f"{node.name}: {node.entity_type}")
for obs in node.observations:
print(f" - {obs}")
open_nodes — load full entity details including all observations and relations:
nodes = tool.memory_open_nodes(names=["client-alpha", "website-redesign"])
for node in nodes:
print(f"=== {node.name} ({node.entity_type}) ===")
for obs in node.observations:
print(f" {obs}")
read_graph — dump the entire knowledge graph for cold-start initialization:
When the MCP server is unavailable, fall back to filesystem tools:
# Find all facts about a specific entity
grep -r "entity: client-alpha" ~/kb/
# List all open decisions across every project
grep -r "status: open" ~/kb/projects/ --include="facts.yaml"# Quick count of entities per categoryfor d in projects areas resources; do
count=$(find ~/kb/$d -maxdepth 2 -name "facts.yaml" | wc -l)
echo"$d: $count entities"done# Search daily notes for a keyword
rg "blocker" ~/kb/areas/daily/
Canonical writer for per-entity YAML fact files alongside the knowledge graph:
import yaml
from datetime import datetime
from pathlib import Path
KB = Path.home() / "kb"defsave_fact(entity: str, category: str, value: str, **kw):
"""Append one fact to a per-entity YAML file.
Args:
entity: the thing being described (project, tool, client)
category: 'projects', 'areas', or 'resources'
value: the fact text
**kw: optional metadata (status, priority, related, source)
"""
dir_ = KB / category / entity
dir_.mkdir(parents=True, exist_ok=True)
facts_file = dir_ / "facts.yaml"
facts = []
if facts_file.exists():
withopen(facts_file) as f:
facts = yaml.safe_load(f) or []
facts.append({
"value": value,
"recorded_at": datetime.now().isoformat(),
**kw
})
withopen(facts_file, "w") as f:
yaml.dump(facts, f, default_flow_style=False, sort_keys=False)
return facts_file
Maintenance
Pruning Stale Observations
Knowledge graphs accumulate noise. Run periodic pruning:
defprune_stale_observations(days_threshold=90):
"""Flag observations older than threshold for review."""from datetime import timedelta
cutoff = datetime.now() - timedelta(days=days_threshold)
stale = []
for category in ["projects", "areas", "resources"]:
for facts_file in (KB / category).rglob("facts.yaml"):
withopen(facts_file) as f:
records = yaml.safe_load(f) or []
fresh = []
for rec in records:
recorded = datetime.fromisoformat(rec["recorded_at"])
if recorded < cutoff and rec.get("status") != "archived":
stale.append({
"file": str(facts_file),
"value": rec["value"],
"recorded_at": rec["recorded_at"]
})
else:
fresh.append(rec)
iflen(fresh) != len(records):
withopen(facts_file, "w") as f:
yaml.dump(fresh, f, default_flow_style=False, sort_keys=False)
return stale
Weekly Review Checklist
Move completed projects from projects/ to archives/
Delete observations marked "obsolete" or "superseded"
Verify all open decisions still need resolution
Run entity-index-gen.py to rebuild the cross-reference
Check that today's daily note has an End-of-Session Summary
Archiving Stale Content
#!/usr/bin/env bash# archive-stale.sh — move projects untouched for 90+ days to archives
KB=~/kb
CUTOFF=$(date -d "90 days ago" +%s)
fordirin"$KB"/projects/*/; do
last=$(git -C "$KB"log -1 --format="%at" -- "$dir" 2>/dev/null || echo 0)
if [ "$last" -gt 0 ] && [ "$last" -lt "$CUTOFF" ]; then
name=$(basename"$dir")
mv"$dir""$KB/archives/projects/$name"echo"Archived: $name"fidone
Session Start Routine
This is the critical cold-start flow every agent session should execute.
Step 1 — Check the Scratchpad Bridge
from pathlib import Path
from datetime import datetime, timedelta
SCRATCHPAD = Path.home() / ".kb-session-context.md"defload_session_context():
"""Read the bridge file if it exists and is fresh (< 30 min)."""ifnot SCRATCHPAD.exists():
return {"state": "cold_start", "context": {}}
mtime = datetime.fromtimestamp(SCRATCHPAD.stat().st_mtime)
if datetime.now() - mtime > timedelta(minutes=30):
return {"state": "stale", "context": {}}
text = SCRATCHPAD.read_text()
context = {}
for line in text.split("\n"):
if line.startswith("- "):
key, _, val = line[2:].partition(": ")
context[key.lower()] = val
return {"state": "warm", "context": context}
Step 2 — Load the Knowledge Graph
# Re-establish entity context
graph = tool.memory_read_graph()
stale_entities = [e.name for e in graph.entities
if"stale"in (e.observations or [])]
# Open entities related to today's focus
focus = "client-alpha"
related = tool.memory_open_nodes(names=[focus])
for node in related:
print(f"Loaded: {node.name} — {len(node.observations)} observations")
Step 3 — Create Today's Daily Note
defensure_daily_note():
"""Create today's note if it doesn't exist."""
KB = Path.home() / "kb"
today = datetime.now().strftime("%Y-%m-%d")
note_file = KB / "areas" / "daily" / f"{today}.md"if note_file.exists():
return note_file
# Load yesterday's summary for continuity
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
yesterday_file = KB / "areas" / "daily" / f"{yesterday}.md"
yesterday_summary = ""if yesterday_file.exists():
text = yesterday_file.read_text()
if"## End-of-Session Summary"in text:
yesterday_summary = text.split("## End-of-Session Summary", 1)[1]
content = f"""# Daily Note: {today}
## Session Focus
_(What is the single most important outcome this session?)_
## Continuity Context
{yesterday_summary}
## Open Decisions Needing Input
## Plan
- [ ]
## Decisions Made Today
## End-of-Session Summary
_Write before closing — this bridges the next session._
"""
note_file.parent.mkdir(parents=True, exist_ok=True)
note_file.write_text(content)
return note_file
Step 4 — Load Open Decisions
defload_open_decisions():
"""Return every entity fact with status=open."""
KB = Path.home() / "kb"
decisions = []
for category in ["projects", "areas", "resources"]:
for facts_file in (KB / category).rglob("facts.yaml"):
withopen(facts_file) as f:
records = yaml.safe_load(f) or []
for rec in records:
if rec.get("status") in ("open", "pending"):
decisions.append({
"entity": facts_file.parent.name,
"category": category,
"value": rec["value"]
})
return decisions
Full Bash Session Starter
#!/usr/bin/env bash# kb-session.sh — run at every agent session start
KB=~/kb
TODAY=$(date +%Y-%m-%d)
YESTERDAY=$(date -d yesterday +%Y-%m-%d)
NOTE=$KB/areas/daily/$TODAY.md
# Step 1: Load yesterday's summaryif [ -f "$KB/areas/daily/$YESTERDAY.md" ]; thenecho"=== Yesterday's Summary ==="
sed -n '/## End-of-Session Summary/,$ p' \
"$KB/areas/daily/$YESTERDAY.md" | head -20
fi# Step 2: Check open decisionsecho""echo"=== Open Decisions ==="
grep -rl "status: open""$KB"/{projects,areas,resources}/*/facts.yaml 2>/dev/null \
| whileread -r f; do
entity=$(basename"$(dirname "$f")")
grep -A1 "value:""$f" | head -2
echo" (entity: $entity)"done || echo" None"# Step 3: Create today's noteif [ ! -f "$NOTE" ]; thencat > "$NOTE" <<EOF
# Daily Note: $TODAY
## Session Focus
## Decisions Made Today
## End-of-Session Summary
---
EOFecho""echo"Created $NOTE — edit the focus line and begin."fi
Anti-Rationalization Table
Rationalization
Reality
"I remember what happened last session"
Agents lose everything on compaction. Without KB files, you start blank.
"The code comments are enough documentation"
Comments describe what; KB describes why you chose that approach.
"YAML facts are too structured for quick notes"
Unstructured daily notes cost nothing. Structure grows as the entity matures.
"I'll backfill facts when the project settles"
The gap between "I know this" and "I wrote this" is where information rot begins.
"grep is fast enough for recall"
grep finds text. Semantic relationships need the knowledge graph.
"My projects are too small for PARA"
PARA scales down to one file per entity. Starting flat means refactoring 200 files later.
"The scratchpad file is enough"
The scratchpad handles active bridging; the KB handles permanent organizational memory.
"Daily notes are for humans, not agents"
Agents read more daily notes than humans do — they have zero tacit context.
"I don't need relations — tags are fine"
Relations model causality and dependency. Tags model membership. Both are needed.
"Archiving is busywork"
Without archiving, every grep finds 3-year-old context about dead projects.
Verification Checklist
~/kb/{projects,areas,resources,archives} directory tree exists
Today's daily note is present at ~/kb/areas/daily/YYYY-MM-DD.md
Yesterday's daily note contains an End-of-Session Summary section
save_fact() writes valid YAML re-readable with yaml.safe_load()
Entity facts contain recorded_at, value, and status fields
load_open_decisions() returns at least one result before cleanup
Knowledge graph has at least 3 entity types registered
Scratchpad bridge file is written on session end, checked on session start
Scratchpad older than 30 minutes is treated as stale (not loaded)
prune_stale_observations(90) returns no false positives
All Python code blocks parse without syntax errors
All bash code blocks pass bash -n syntax check
Archive script has been run at least once
YAML fact files are under git — git log --oneline shows fact history
Weekly review checklist completed within the last 7 days
When to Use
Use this skill when working with kb. Specifically:
Every session start — load context, check open decisions, create daily note
After every decision — capture as a YAML fact with status: open or
status: confirmed
At session end — write End-of-Session Summary, update scratchpad, commit
During investigations — store entity facts as you discover them
Weekly — review stale content, archive untouched projects
When spawning subagents — share relevant KB entities via local:// URIs
so subagents start with context instead of cold