Access

Enter name and PIN to view.

That combination does not match. Try again.

V3 · iteration 1 · 2026-04-17

Spine + brain + personas + CLAUDE.md stack — live on three machines

Every bit that went into iteration 1: SQL, bash, markdown, settings diffs, verification output. Nothing hidden.

dolphin ✓ logging spud2 ✓ logging Hetzner ✓ brain Windows → Ubuntu → Docker → Postgres private repo · PIN gate · just me
Section 1 · State

Brain DB — live row counts

Three tables in v2.*, read by claude_ro, written by acebuddy via hooks.

6
v2.sessions
30
v2.turns
8
v2.personas
2
hosts logging

v2.personas — 8 seeded rows SELECT via claude_ro

     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)

v2.sessions — recent across both machines cross-machine identity

   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)
Both dolphin and spud2 are now writing session rows with metadata.host correctly set. Cross-machine identity proven.
Section 2 · Shape

What talks to what

One brain. Two writers. Dashed arrows fire automatically on every SessionStart and Stop.

🧠 Hetzner · CAX31 · the brain

v2.sessions · 6 v2.turns · 30 v2.personas · 8

Postgres 16 + pgvector · schema v2 · claude_ro reads · acebuddy writes

↑ ssh + docker exec · one INSERT per SessionStart · one per Stop ↑

💻 dolphin · Windows 11 · Git Bash

  • Claude Code (daily driver)
  • hooks/session-start.sh + stop.sh
  • CLAUDE.md global + project
  • host = dolphin

🖥️ spud2 · Ubuntu 24.04 · bash

  • Claude Code (secondary)
  • hooks/session-start.sh + stop.sh
  • CLAUDE.md global + project
  • host = spud2

Hooks fail loud to ~/.claude/logs/spine.log and exit 0 — if Hetzner is unreachable, the session continues and the row is simply lost.

Section 3 · SQL

Migrations — three files, all idempotent

Apply pattern: scp → docker cp → psql -f (Windows UTF-8 safe). Every migration is re-runnable.

001_spine.sql · 191 lines · v2.sessions + v2.turns · applied earlier
-- ════════════════════════════════════════════════════════════════════
-- 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 · 156 lines · roster schema + 8-row seed · applied today
-- ════════════════════════════════════════════════════════════════════
-- 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 · 37 lines · opens schema access · applied today
-- ════════════════════════════════════════════════════════════════════
-- 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;

Apply pattern reusable for every future migration

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'
Why not stdin pipe? Emojis in the persona seed (🕴️📚🧑‍⚖️…) get mangled by Windows Git Bash → Linux SSH stdin. docker cp + -f dodges the encoding conversion entirely.
Section 4 · Bash

Hooks — session-start.sh + stop.sh

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.

hooks/session-start.sh · 134 lines · runs on every SessionStart
#!/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
hooks/stop.sh · 157 lines · runs on every Stop · atomic turn_num
#!/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
Section 5 · Markdown

CLAUDE.md stack — 4 files touched

Load order every session: global ~/.claude/CLAUDE.md first, then project-root CLAUDE.md. Server-side Hetzner file is reference-only for admin sessions.

FileMachineChangeSize
CLAUDE.md (project root)repoCreated — full project context96 lines
~/.claude/CLAUDE.mddolphin1-line v3 pointer added to Canonical Docs+1 line
~/.claude/CLAUDE.mdspud21-bullet v3 pointer added to Project Layout+1 line
/opt/acebuddy/CLAUDE.mdHetzner### v2 schema subsection added before Invariants+7 lines
1 · Project-root CLAUDE.md · new file · 96 lines · auto-loads in v3 dir
# 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]
2 · Dolphin global CLAUDE.md patch · 1 line added · Canonical Docs section
  - **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.
3 · Spud2 global CLAUDE.md patch · 1 bullet added · Project Layout section
  ## 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.
4 · Hetzner /opt/acebuddy/CLAUDE.md patch · 7-line subsection added before Invariants
  ### 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)
Section 6 · Settings

Spud2 ~/.claude/settings.json — hook registration

Added v3 spine entry to existing SessionStart array (alongside 2 GSD hooks). Created new Stop block. Nothing existing was overwritten.

Before → after (jq diff)

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)
+ ]

jq transform applied backup written first

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

Verify SessionStart count: 3 · Stop count: 1

=== 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
Section 7 · Gates

Verification — 4 gates, all green

What we checked before calling iteration 1 done.

#GateHowResult
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)

Dolphin spine.log tail this live session

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)

Spud2 spine.log tail

2026-04-17T17:22:37Z [session-start] OK session=b674d968-90db-4c2c-9247-15a37f56e92d project=/home/nectar4362/test-iteration-1-spud2-hook
Section 8 · Next

What comes after iteration 1

Explicit subtraction. These are pickup points, not commitments.

Held for next iteration

Explicitly dropped for this iteration