V3 · iteration 1 · 2026-04-17
Every bit that went into iteration 1: SQL, bash, markdown, settings diffs, verification output. Nothing hidden.
Three tables in v2.*, read by claude_ro, written by acebuddy via hooks.
slug | emoji | tier | bound_skill --------------+-------+--------+------------- archivist | 💾 | system | undo auditor | 🧑⚖️ | system | done cop | 👮 | system | librarian | 📚 | system | scrap | 🗑️ | system | scrap-scan session-boss | 🕴️ | system | alex | 🌿 | user | alex steve | 🔧 | user | steve (8 rows)
id | host | project | started_at ----------+---------+----------------------------------------------------+------------------------------- b674d968 | spud2 | /home/nectar4362/test-iteration-1-spud2-hook | 2026-04-17 17:22:37.203351+00 d3efdd28 | dolphin | /c/Users/acebu/projects/mcp-and-skills-framework-a | 2026-04-17 14:43:09.629375+00 ac4e4fa3 | dolphin | /c/Users/acebu/projects | 2026-04-17 14:15:47.630702+00 4cd2e969 | dolphin | /c/Users/acebu/projects/data-workbench | 2026-04-17 14:13:17.084598+00 e61eceb8 | dolphin | /c/Users/acebu/projects | 2026-04-17 13:23:18.967888+00 5db14c49 | dolphin | /c/Users/acebu/projects/data-workbench | 2026-04-17 04:41:26.944464+00 (6 rows)
metadata.host correctly set. Cross-machine identity proven.One brain. Two writers. Dashed arrows fire automatically on every SessionStart and Stop.
Postgres 16 + pgvector · schema v2 · claude_ro reads · acebuddy writes
dolphinspud2Hooks fail loud to ~/.claude/logs/spine.log and exit 0 — if Hetzner is unreachable, the session continues and the row is simply lost.
Apply pattern: scp → docker cp → psql -f (Windows UTF-8 safe). Every migration is re-runnable.
-- ════════════════════════════════════════════════════════════════════
-- 001_spine.sql — v2 brain DB spine (Tier A)
-- ════════════════════════════════════════════════════════════════════
-- Sessions + turns. Two tables. Nothing else.
-- Per V3-HANDOFF §6.2: smallest spine that proves the contract works.
-- Personas defer to 002_personas.sql per §10 pre-execution checklist.
--
-- Idempotent: safe to re-run. Uses CREATE ... IF NOT EXISTS, CREATE OR
-- REPLACE FUNCTION, DROP TRIGGER IF EXISTS.
-- ════════════════════════════════════════════════════════════════════
CREATE SCHEMA IF NOT EXISTS v2;
COMMENT ON SCHEMA v2 IS
'Brain DB v2 — clean schema, parallel-run with public.* until cutover. '
'Cutover plan: ALTER SCHEMA public RENAME TO v1_archive; '
'ALTER SCHEMA v2 RENAME TO public.';
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE OR REPLACE FUNCTION v2.set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TABLE IF NOT EXISTS v2.sessions (
-- 6 universal columns
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'ended', 'abandoned', 'archived')),
source TEXT,
metadata JSONB NOT NULL DEFAULT '{}',
-- Session-specific
project_path TEXT,
purpose TEXT,
mode TEXT,
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
ended_at TIMESTAMPTZ,
turn_count INTEGER NOT NULL DEFAULT 0,
tokens_total INTEGER NOT NULL DEFAULT 0,
budget_cap INTEGER,
-- Temporal versioning
valid_from TIMESTAMPTZ NOT NULL DEFAULT now(),
valid_to TIMESTAMPTZ NOT NULL DEFAULT 'infinity'
);
CREATE INDEX IF NOT EXISTS idx_sessions_started_at ON v2.sessions (started_at DESC);
CREATE INDEX IF NOT EXISTS idx_sessions_status ON v2.sessions (status) WHERE status = 'active';
CREATE INDEX IF NOT EXISTS idx_sessions_project_path ON v2.sessions (project_path);
DROP TRIGGER IF EXISTS trg_sessions_updated_at ON v2.sessions;
CREATE TRIGGER trg_sessions_updated_at
BEFORE UPDATE ON v2.sessions
FOR EACH ROW EXECUTE FUNCTION v2.set_updated_at();
CREATE TABLE IF NOT EXISTS v2.turns (
-- 6 universal columns
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
status TEXT NOT NULL DEFAULT 'ok'
CHECK (status IN ('ok', 'errored', 'archived')),
source TEXT,
metadata JSONB NOT NULL DEFAULT '{}',
-- Turn-specific
session_id UUID NOT NULL REFERENCES v2.sessions(id) ON DELETE CASCADE,
turn_num INTEGER NOT NULL,
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
ended_at TIMESTAMPTZ,
user_message TEXT,
assistant_response TEXT,
tokens_in INTEGER,
tokens_out INTEGER,
tool_calls_count INTEGER NOT NULL DEFAULT 0,
error_note TEXT,
-- Auditor fills async at session close (Tier C, NULL until then)
classified_at TIMESTAMPTZ,
classification JSONB,
UNIQUE (session_id, turn_num)
);
CREATE INDEX IF NOT EXISTS idx_turns_session_id ON v2.turns (session_id);
CREATE INDEX IF NOT EXISTS idx_turns_started_at ON v2.turns (started_at DESC);
CREATE INDEX IF NOT EXISTS idx_turns_unclassified ON v2.turns (created_at) WHERE classified_at IS NULL;
DROP TRIGGER IF EXISTS trg_turns_updated_at ON v2.turns;
CREATE TRIGGER trg_turns_updated_at
BEFORE UPDATE ON v2.turns
FOR EACH ROW EXECUTE FUNCTION v2.set_updated_at();
-- Verify: \dt v2.*
-- Smoke: SELECT COUNT(*) FROM v2.sessions; SELECT COUNT(*) FROM v2.turns;
-- Rollback (DESTRUCTIVE): DROP SCHEMA v2 CASCADE;
-- ════════════════════════════════════════════════════════════════════
-- 002_personas.sql — v2.personas (Tier A seed: 8 rows)
-- ════════════════════════════════════════════════════════════════════
-- Schema + seed for the Village roster. Canonical prose (visual desc,
-- background, backstory, jobs, actions) lives in personas/<slug>/profile.md.
-- DB stores: lookup, footer, provenance, portrait pointer. Nothing more.
-- ════════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS v2.personas (
-- 6 universal columns
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'draft', 'retired', 'archived')),
source TEXT,
metadata JSONB NOT NULL DEFAULT '{}',
-- Persona-specific
slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
emoji TEXT,
tier TEXT NOT NULL CHECK (tier IN ('system', 'user')),
character_id TEXT UNIQUE,
seed INTEGER,
job_one_liner TEXT,
bound_skill TEXT,
profile_path TEXT,
portrait_path TEXT
);
CREATE INDEX IF NOT EXISTS idx_personas_tier ON v2.personas (tier);
CREATE INDEX IF NOT EXISTS idx_personas_status
ON v2.personas (status) WHERE status = 'active';
DROP TRIGGER IF EXISTS trg_personas_updated_at ON v2.personas;
CREATE TRIGGER trg_personas_updated_at
BEFORE UPDATE ON v2.personas
FOR EACH ROW EXECUTE FUNCTION v2.set_updated_at();
GRANT SELECT ON v2.personas TO claude_ro;
-- 8-row seed (system tier: 6 · user tier: 2)
INSERT INTO v2.personas
(slug, name, emoji, tier, character_id, seed, job_one_liner, bound_skill, profile_path, source)
VALUES
('session-boss', 'Session Boss', '🕴️', 'system',
'session-boss-v1-7e3a91c4', 142857001,
'session invariants, prime, budget, mode',
NULL, 'personas/session-boss/profile.md', 'seed-v1'),
('librarian', 'Librarian', '📚', 'system',
'librarian-v1-3c8f64e2', 271828182,
'per-turn raw logging + handoff append',
NULL, 'personas/librarian/profile.md', 'seed-v1'),
('auditor', 'Auditor', '🧑⚖️', 'system',
'auditor-v1-9b1e07d3', 314159265,
'session close — classify, rate, find repeated mistakes',
'done', 'personas/auditor/profile.md', 'seed-v1'),
('cop', 'Cop', '👮', 'system',
'cop-v1-5d2a7e88', 161803398,
'drift detection — AI + user, 3+ events triggers checkpoint',
NULL, 'personas/cop/profile.md', 'seed-v1'),
('archivist', 'Archivist', '💾', 'system',
'archivist-v1-c0d3f1a7', 577215664,
'per-turn local commit, push at session close, /undo rollback',
'undo', 'personas/archivist/profile.md', 'seed-v1'),
('scrap', 'Scrap', '🗑️', 'system',
'scrap-v1-7b3c9a01', 71830642,
'weighs what''s on disk and what it is, reports drag, never deletes',
'scrap-scan', 'personas/scrap/profile.md', 'seed-v1'),
('steve', 'Steve', '🔧', 'user',
'steve-v1-7f2c0a9b', 282574016,
'API specialist — Perplexity, OpenAI, Cloudflare, GitHub, ntfy, brain ingest',
'steve', 'personas/steve/profile.md', 'seed-v1'),
('alex', 'Alex', '🌿', 'user',
'alex-v1-a4e2b908', 102038174,
'learnings steward — classify, shorthand, surface, lean in',
'alex', 'personas/alex/profile.md', 'seed-v1')
ON CONFLICT (slug) DO NOTHING;
-- ════════════════════════════════════════════════════════════════════
-- 003_grants_claude_ro.sql — read access to v2.* for claude_ro
-- ════════════════════════════════════════════════════════════════════
-- 001 and 002 created tables but no schema-level USAGE for claude_ro,
-- so SELECT failed with "permission denied for schema v2".
--
-- This fixes that + sets ALTER DEFAULT PRIVILEGES so future v2 tables
-- auto-grant SELECT to claude_ro.
-- ════════════════════════════════════════════════════════════════════
-- 1. Schema access
GRANT USAGE ON SCHEMA v2 TO claude_ro;
-- 2. All current v2 tables
GRANT SELECT ON ALL TABLES IN SCHEMA v2 TO claude_ro;
-- 3. Future v2 tables get SELECT automatically
ALTER DEFAULT PRIVILEGES IN SCHEMA v2
GRANT SELECT ON TABLES TO claude_ro;
-- Verify:
-- SET ROLE claude_ro;
-- SELECT count(*) FROM v2.personas; -- should return 8, not error
-- RESET ROLE;
scp migrations/NNN_name.sql root@100.97.123.98:/tmp/NNN_name.sql
ssh root@100.97.123.98 \
'docker cp /tmp/NNN_name.sql postgres:/tmp/ && \
docker exec postgres psql -U acebuddy -d brain \
-v ON_ERROR_STOP=1 -f /tmp/NNN_name.sql'
docker cp + -f dodges the encoding conversion entirely.Two scripts. Both SSH to Hetzner via Tailscale, INSERT one row each, fail loud to ~/.claude/logs/spine.log. Never block Claude Code session start.
#!/bin/bash
# ════════════════════════════════════════════════════════════════════
# session-start.sh — v3 spine (Tier A) — SessionStart hook
# ════════════════════════════════════════════════════════════════════
# Inserts one row into v2.sessions on Claude Code SessionStart.
#
# Contract:
# - One INSERT, fail loud on DB error
# - Does NOT block session start if Hetzner is unreachable
# - Logs every action to ~/.claude/logs/spine.log
set -uo pipefail
LOG_DIR="$HOME/.claude/logs"
LOG_FILE="$LOG_DIR/spine.log"
mkdir -p "$LOG_DIR" 2>/dev/null || true
ts() { date -u +'%Y-%m-%dT%H:%M:%SZ'; }
log() { echo "$(ts) [session-start] $*" >> "$LOG_FILE"; }
# Portable Python: dolphin's `python3` is a broken Microsoft Store stub.
if python3 --version >/dev/null 2>&1; then
PY="python3"
elif py -3 --version >/dev/null 2>&1; then
PY="py -3"
else
PY="python"
fi
STDIN_JSON=""
if [ ! -t 0 ]; then
STDIN_JSON=$(cat 2>/dev/null || true)
fi
SESSION_ID="${CLAUDE_SESSION_ID:-}"
if [ -z "$SESSION_ID" ] && [ -n "$STDIN_JSON" ]; then
SESSION_ID=$(echo "$STDIN_JSON" \
| $PY -c "import json,sys;d=json.load(sys.stdin);print(d.get('session_id',''))" \
2>/dev/null || true)
fi
if [ -z "$SESSION_ID" ]; then
SESSION_ID=$($PY -c "import uuid;print(uuid.uuid4())" 2>/dev/null \
|| cat /proc/sys/kernel/random/uuid 2>/dev/null \
|| echo "fallback-$(date +%s)-$$")
log "WARN: no CLAUDE_SESSION_ID; generated $SESSION_ID"
fi
# Validate UUID shape — fail loud on weird input
if ! echo "$SESSION_ID" | grep -Eq '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'; then
log "ERROR: session_id '$SESSION_ID' is not a UUID; refusing INSERT"
exit 0
fi
PROJECT_PATH="${CLAUDE_PROJECT_PATH:-$PWD}"
PROJECT_PATH=$(echo "$PROJECT_PATH" | sed 's|\\|/|g')
SOURCE_TAG="claude-code"
HOSTNAME_SHORT=$(hostname 2>/dev/null | cut -d. -f1)
METADATA=$($PY -c "
import json
print(json.dumps({
'host': '$HOSTNAME_SHORT',
'spine_version': 'tier-a-v1',
}))
" 2>/dev/null || echo '{}')
SQL=$(cat <<EOF
INSERT INTO v2.sessions (id, source, project_path, started_at, metadata)
VALUES (
'${SESSION_ID}'::uuid,
'${SOURCE_TAG}',
\$\$${PROJECT_PATH}\$\$,
now(),
'${METADATA}'::jsonb
)
ON CONFLICT (id) DO NOTHING;
EOF
)
# Persist session_id for stop.sh siblings (per-pid file)
SID_FILE="/tmp/claude-spine-session-id.$$"
echo "$SESSION_ID" > "$SID_FILE" 2>/dev/null || true
echo "$SESSION_ID" > "/tmp/claude-spine-session-id.current" 2>/dev/null || true
SSH_TARGET="root@100.97.123.98"
SSH_OPTS="-o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new -o BatchMode=yes"
OUTPUT=$(ssh $SSH_OPTS "$SSH_TARGET" \
"docker exec -i postgres psql -U acebuddy -d brain -v ON_ERROR_STOP=1" \
<<< "$SQL" 2>&1)
RC=$?
if [ $RC -eq 0 ]; then
log "OK session=$SESSION_ID project=$PROJECT_PATH"
echo "[spine] session $SESSION_ID logged" >&2
else
log "FAIL rc=$RC session=$SESSION_ID output=$OUTPUT"
echo "[spine] session INSERT failed (rc=$RC) — degraded mode (see $LOG_FILE)" >&2
fi
exit 0
#!/bin/bash
# ════════════════════════════════════════════════════════════════════
# stop.sh — v3 spine (Tier A) — Stop hook
# ════════════════════════════════════════════════════════════════════
# Inserts one row into v2.turns on every Claude Code Stop event.
#
# turn_num strategy:
# Atomic at SQL level — the INSERT computes
# COALESCE(MAX(turn_num), 0) + 1 WHERE session_id = $1
# inside a single statement. UNIQUE(session_id, turn_num) catches
# races; we retry once on conflict, then fail loud.
set -uo pipefail
LOG_DIR="$HOME/.claude/logs"
LOG_FILE="$LOG_DIR/spine.log"
mkdir -p "$LOG_DIR" 2>/dev/null || true
ts() { date -u +'%Y-%m-%dT%H:%M:%SZ'; }
log() { echo "$(ts) [stop] $*" >> "$LOG_FILE"; }
if python3 --version >/dev/null 2>&1; then PY="python3"
elif py -3 --version >/dev/null 2>&1; then PY="py -3"
else PY="python"
fi
STDIN_JSON=""
[ ! -t 0 ] && STDIN_JSON=$(cat 2>/dev/null || true)
SESSION_ID="${CLAUDE_SESSION_ID:-}"
if [ -z "$SESSION_ID" ] && [ -n "$STDIN_JSON" ]; then
SESSION_ID=$(echo "$STDIN_JSON" \
| $PY -c "import json,sys;d=json.load(sys.stdin);print(d.get('session_id',''))" \
2>/dev/null || true)
fi
# Fallback: read from file session-start.sh wrote
[ -z "$SESSION_ID" ] && [ -f "/tmp/claude-spine-session-id.$PPID" ] && \
SESSION_ID=$(cat "/tmp/claude-spine-session-id.$PPID" 2>/dev/null || true)
[ -z "$SESSION_ID" ] && [ -f "/tmp/claude-spine-session-id.current" ] && \
SESSION_ID=$(cat "/tmp/claude-spine-session-id.current" 2>/dev/null || true)
if [ -z "$SESSION_ID" ]; then
log "ERROR: no session_id resolvable; skipping turn INSERT"
exit 0
fi
SOURCE_TAG="claude-code"
HOSTNAME_SHORT=$(hostname 2>/dev/null | cut -d. -f1)
METADATA=$($PY -c "import json; print(json.dumps({'host':'$HOSTNAME_SHORT','spine_version':'tier-a-v1'}))" 2>/dev/null || echo '{}')
SQL=$(cat <<EOF
BEGIN;
INSERT INTO v2.turns (session_id, turn_num, source, started_at, ended_at, metadata)
SELECT
'${SESSION_ID}'::uuid,
COALESCE(MAX(turn_num), 0) + 1,
'${SOURCE_TAG}',
now(), now(),
'${METADATA}'::jsonb
FROM v2.turns WHERE session_id = '${SESSION_ID}'::uuid;
UPDATE v2.sessions
SET turn_count = turn_count + 1
WHERE id = '${SESSION_ID}'::uuid;
COMMIT;
EOF
)
SSH_TARGET="root@100.97.123.98"
SSH_OPTS="-o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new -o BatchMode=yes"
OUTPUT=$(ssh $SSH_OPTS "$SSH_TARGET" \
"docker exec -i postgres psql -U acebuddy -d brain -v ON_ERROR_STOP=1" \
<<< "$SQL" 2>&1)
RC=$?
if [ $RC -eq 0 ]; then
log "OK session=$SESSION_ID (turn_num auto)"
else
# UNIQUE conflict retry
if echo "$OUTPUT" | grep -q "duplicate key value violates unique constraint"; then
log "RETRY session=$SESSION_ID — UNIQUE conflict, retrying once"
sleep 0.2
OUTPUT2=$(ssh $SSH_OPTS "$SSH_TARGET" \
"docker exec -i postgres psql -U acebuddy -d brain -v ON_ERROR_STOP=1" \
<<< "$SQL" 2>&1)
RC2=$?
[ $RC2 -eq 0 ] && log "OK session=$SESSION_ID (after retry)" \
|| log "FAIL session=$SESSION_ID retry rc=$RC2 output=$OUTPUT2"
else
log "FAIL session=$SESSION_ID rc=$RC output=$OUTPUT"
fi
fi
exit 0
Load order every session: global ~/.claude/CLAUDE.md first, then project-root CLAUDE.md. Server-side Hetzner file is reference-only for admin sessions.
| File | Machine | Change | Size |
|---|---|---|---|
CLAUDE.md (project root) | repo | Created — full project context | 96 lines |
~/.claude/CLAUDE.md | dolphin | 1-line v3 pointer added to Canonical Docs | +1 line |
~/.claude/CLAUDE.md | spud2 | 1-bullet v3 pointer added to Project Layout | +1 line |
/opt/acebuddy/CLAUDE.md | Hetzner | ### v2 schema subsection added before Invariants | +7 lines |
# V3 — Village brain, nervous system, authoring surface
**What this is.** A Claude-Code-native system where every session is logged to a Postgres spine, named personas act as cognitive handles for Pierre, and markdown (not chat) is the authoring surface for accumulated knowledge. Runs across dolphin · spud2 · Hetzner.
**Where we are now.** Rolling state lives in `NEXT-SESSION.md`. Open it first at session start. This file (project CLAUDE.md) is stable.
---
## Three pillars
| Pillar | Physical form | Machines |
|---|---|---|
| **Brain** | Postgres on Hetzner, schema `v2.*` | cax31 (admin only, no hooks) |
| **Nervous system** | `hooks/session-start.sh` + `stop.sh` → SSH → INSERT into `v2.*` | dolphin ✅ · spud2 ✅ |
| **Authoring surface** | `brain-vault/` markdown (separate repo, Obsidian-edited) | dolphin |
## Brain DB tables — v2 schema
| Table | Purpose | Written by |
|---|---|---|
| `v2.sessions` | One row per Claude Code session | `hooks/session-start.sh` |
| `v2.turns` | One row per assistant turn | `hooks/stop.sh` |
| `v2.personas` | Roster — 8 seeded | `migrations/002_personas.sql` |
Read-only queries (safe default):
```bash
ssh root@100.97.123.98 'docker exec postgres psql -U claude_ro -d brain -c "..."'
```
Write access is `acebuddy` role — announce first, reserved for hooks + migrations.
## Persona roster (what's in `v2.personas` right now)
| Emoji | Name | Tier | Skill | Role (one line) |
|---|---|---|---|---|
| 🕴️ | Session Boss | system | (hook) | session invariants, prime, budget, mode |
| 📚 | Librarian | system | (hook) | per-turn raw logging + handoff append |
| 🧑⚖️ | Auditor | system | `done` | session close — classify, rate, find repeated mistakes |
| 👮 | Cop | system | (hook) | drift detection |
| 💾 | Archivist | system | `undo` | commit + rollback |
| 🗑️ | Scrap | system | `scrap-scan`* | disk drag reporter, never deletes |
| 🔧 | Steve | user | `steve` | API specialist (Perplexity, OpenAI, CF, GitHub, ntfy, ingest) |
| 🌿 | Alex | user | `alex` | learnings steward — classify, shorthand, surface |
*`scrap-scan` skill not yet built.
## Invariants
- **Spine hooks never block session start.** SSH fails silently to `~/.claude/logs/spine.log`; the session continues. Don't add retries that block.
- **Personas are cognitive handles, not agents.** Footer line (`<emoji> <Name>: <one-liner>`) is the only surface Claude Code emits. No LLM roleplay.
- **IP separation.** `brain-vault/` = mental models (private, low-stakes). Project repos = IP (high-stakes, never rendered publicly).
- **Markdown is canonical.** `personas/<slug>/profile.md` is the source of truth for character prose. DB holds only lookup/footer/provenance pointers.
## Working rules (project-specific)
Global rules apply first. These add:
- **Subtract by default** — when adding X, name what retires to make room.
- **Show SQL / bash / diffs verbatim** during execution — no hiding behind sub-agents.
- **DB writes announce first.** `acebuddy` role is privileged; state the SQL before running.
- **Idempotent migrations.** `CREATE IF NOT EXISTS`, `ON CONFLICT DO NOTHING`, `DROP ... IF EXISTS`. Re-run must be safe.
- **Universal columns.** Every `v2.*` table: `id`, `created_at`, `updated_at`, `status`, `source`, `metadata`. Prose lives in markdown, not columns.
[... full health-check block and file-map table continue in repo]
- **Main architecture doc:** `/opt/acebuddy/CLAUDE.md` on Hetzner (~28 KB) — full cluster detail, bot list, invariants, schema notes. SSH and read when you need cluster internals. Don't duplicate it here.
- **Spine meta-repo:** `~/projects/repo-consolidation/CLAUDE.md` on spud2 — ecosystem map (thin reading layer).
+ - **V3 brain/nervous-system/personas:** `C:/Users/acebu/projects/mcp-and-skills-framework-and-brain-v3/CLAUDE.md` (dolphin), `~/projects/mcp-and-skills-framework-and-brain-v3/CLAUDE.md` (spud2). `v2.*` tables on Hetzner brain DB; hooks log every session to `v2.sessions`/`v2.turns`.
- **Global on each server:** `~/.claude/CLAUDE.md` on spud2/Hetzner — server-side Pierre-invariant context.
## Project layout on spud2
Current active projects under `~/projects/`:
- **`setup-home-assistant/`** — home-LAN runtime project. Home Assistant + media + voice playback + local automations.
- **`repo-consolidation/`** — meta-layer ("spine pattern") above the ~5 clusters running on hetzner-v2. Not a rewrite — a thin reading layer that lets Claude reason across acebuddy/family-os/data-team/navigator/reflector via hierarchical CLAUDE.md + filesystem MCP.
+ - **`mcp-and-skills-framework-and-brain-v3/`** — Village brain + nervous system + personas. Hooks log every session to `v2.*` on Hetzner. Project root `CLAUDE.md` auto-loads when working in that dir.
Each project has its own `.planning/` directory, `HANDOFF.md`, and per-project memory dir.
### Common queries
```sql
SELECT bot_name, status, action_count FROM v_bot_status;
SELECT topic, insight FROM v_learnings WHERE category = 'caddy';
...
```
+ ### v2 schema — Claude Code session spine (added 2026-04-17)
+ Parallel namespace to `public.*`. Written by Claude Code hooks on dolphin + spud2 via `acebuddy` role.
+
+ - `v2.sessions` — one row per session (`hooks/session-start.sh`)
+ - `v2.turns` — one row per turn (`hooks/stop.sh`)
+ - `v2.personas` — 8-row roster (system: session-boss, librarian, auditor, cop, archivist, scrap · user: steve, alex)
+
+ Source repo: `mygebruikernaam/mcp-and-skills-framework-and-brain-v3`. Migrations in `migrations/NNN_*.sql`, applied via `docker cp + psql -f`. `claude_ro` has SELECT on all v2 tables.
## Invariants (10 rules, 3 tiers)
~/.claude/settings.json — hook registrationAdded v3 spine entry to existing SessionStart array (alongside 2 GSD hooks). Created new Stop block. Nothing existing was overwritten.
SessionStart: [
gsd-check-update.js,
gsd-session-state.sh,
+ bash /home/nectar4362/projects/mcp-and-skills-framework-and-brain-v3/hooks/session-start.sh (timeout 10)
]
PostToolUse: [...unchanged...]
PreToolUse: [...unchanged...]
+ Stop: [
+ bash /home/nectar4362/projects/mcp-and-skills-framework-and-brain-v3/hooks/stop.sh (timeout 10)
+ ]
cp settings.json settings.json.backup-2026-04-17
jq '
.hooks.SessionStart += [{
"_comment": "v3 brain spine — insert v2.sessions row",
"hooks": [{
"type": "command",
"command": "bash /home/nectar4362/projects/mcp-and-skills-framework-and-brain-v3/hooks/session-start.sh",
"timeout": 10
}]
}]
| .hooks.Stop = ((.hooks.Stop // []) + [{
"_comment": "v3 brain spine — insert v2.turns row per Stop event",
"hooks": [{
"type": "command",
"command": "bash /home/nectar4362/projects/mcp-and-skills-framework-and-brain-v3/hooks/stop.sh",
"timeout": 10
}]
}])
' settings.json > settings.json.new
mv settings.json.new settings.json
=== applied === [ "PostToolUse", "PreToolUse", "SessionStart", "Stop" ] === SessionStart count === 3 === Stop count === 1 === v3 spine commands registered === SessionStart: bash /home/nectar4362/projects/mcp-and-skills-framework-and-brain-v3/hooks/session-start.sh Stop: bash /home/nectar4362/projects/mcp-and-skills-framework-and-brain-v3/hooks/stop.sh
What we checked before calling iteration 1 done.
| # | Gate | How | Result |
|---|---|---|---|
| G1 | Cross-machine identity — spud2 session row lands with host=spud2 |
Manual hook invocation on spud2 with fake session UUID | 🟢 b674d968 · host=spud2 · timestamp correct |
| G2 | 8 personas readable by claude_ro |
SELECT slug, emoji, tier, bound_skill FROM v2.personas |
🟢 8 rows returned (after 003 grants applied) |
| G3 | Project CLAUDE.md loads when Claude Code opens v3 dir | File exists at repo root · auto-load behaviour | 🟡 You'll verify on next fresh session — ask "what's the three-pillar map?" |
| G4 | spine.log shows OK lines on both hosts, zero new FAIL | Tail ~/.claude/logs/spine.log on dolphin + spud2 | 🟢 Dolphin: all OK · Spud2: 1 OK (the G1 test) |
2026-04-17T14:43:09Z [session-start] OK session=d3efdd28-c5f2-4638-bad1-1b3f878b07d2 project=/c/Users/acebu/projects/mcp-and-skills-framework-and-brain-v3 2026-04-17T14:46:17Z [stop] OK session=d3efdd28-c5f2-4638-bad1-1b3f878b07d2 (turn_num auto) 2026-04-17T14:51:21Z [stop] OK session=4cd2e969-29e4-4c81-b5d9-80ed10cd3267 (turn_num auto) 2026-04-17T14:51:58Z [stop] OK session=d3efdd28-c5f2-4638-bad1-1b3f878b07d2 (turn_num auto) 2026-04-17T14:56:36Z [stop] OK session=d3efdd28-c5f2-4638-bad1-1b3f878b07d2 (turn_num auto) 2026-04-17T15:15:04Z [stop] OK session=d3efdd28-c5f2-4638-bad1-1b3f878b07d2 (turn_num auto) 2026-04-17T15:15:54Z [stop] OK session=4cd2e969-29e4-4c81-b5d9-80ed10cd3267 (turn_num auto) 2026-04-17T15:17:24Z [stop] OK session=d3efdd28-c5f2-4638-bad1-1b3f878b07d2 (turn_num auto) 2026-04-17T15:21:29Z [stop] OK session=d3efdd28-c5f2-4638-bad1-1b3f878b07d2 (turn_num auto)
2026-04-17T17:22:37Z [session-start] OK session=b674d968-90db-4c2c-9247-15a37f56e92d project=/home/nectar4362/test-iteration-1-spud2-hook
Explicit subtraction. These are pickup points, not commitments.
5ea11ce, DeepSeek key in untracked files (now gitignored). git filter-repo ready to run when you say.personas/sketches/portrait-mapping.md. 3 gaps (Scrap, Alex, Emeka). Non-urgent.personas/sketches/v1-roster-20-draft.md. Bind when real work surfaces; don't preemptively fill v2.personas.ip:* tag filter. Needs ingest volume first.source_session populated). Use /ingest/markdown.classified_at + classification JSONB. Premature before spud2 produces volume.