# 2026-07-07 — DB as single source of truth for agent knowledge

**Status:** Done (2026-07-08) — wiki archived, FTS live, MCP + HTTP knowledge surface complete.

## Goal

Convert `knowledge/wiki/` into DB seeds, archive all original markdown files, and
archive every other file in the repo that the Oikos Go binary does not read. After
this, the DB is the **sole source of truth** for agents — one query surface, no grep
fallback. All old wiki files go to root `archive/`. New knowledge is registered via
the API, and `oikos export` writes it back to seed files for version control.

## Decisions from operator

| Question | Decision |
|----------|----------|
| Plans in DB? | No — agent construction docs only |
| Ingestion trigger | Seed-time (`oikos seed` reads `seeds/knowledge.yaml`) |
| Archive location | Root `archive/` |
| Content granularity | Structured sections: At-a-glance → entity attributes, procedures → runbooks, changelog → parsed entries, rest → `knowledge_entities.content` |
| Wiki files after conversion | **Archive them all** — DB is the only truth |
| Stale files not used by Oikos | **Archive them all** |

## What goes to `archive/`

Everything the Go binary (`cmd/oikos`, `internal/`) does **not** read at runtime.

### Full archive list

| Path | Destination | Why |
|------|-------------|-----|
| `knowledge/wiki/containers/*.md` (20 pages) | `archive/knowledge/containers/` | Converted to seeds |
| `knowledge/wiki/hosts/*.md` (3 pages) | `archive/knowledge/hosts/` | Converted to seeds |
| `knowledge/wiki/infrastructure/*.md` (12 pages) | `archive/knowledge/infrastructure/` | Converted to seeds |
| `knowledge/wiki/vms/*.md` (2 pages) | `archive/knowledge/vms/` | Converted to seeds |
| `knowledge/sources/investigations/*.md` (5 pages) | `archive/knowledge/investigations/` | Converted to seeds |
| `knowledge/sources/references/*.md` (1 page) | `archive/knowledge/references/` | Converted to seeds |
| `knowledge/sources/index.md` | `archive/knowledge/` | Index page, no entity mapping |
| `knowledge/index.md` | `archive/knowledge/` | Index page |
| `knowledge/log.md` | `archive/knowledge/` | Doc-maintenance log, superseded by audit_log |
| `knowledge/GLOSSARY.md` | `archive/knowledge/` | Glossary (can be re-ingested as `document:glossary` later) |
| `oikos/cards/` (45 files) | `archive/oikos-cards/` | Python MCP explain tool; superseded by DB entities |
| `.hermes/plans/` (7 files) | `archive/hermes-plans/` | Agent construction docs, never in DB |
| `ledger/2026-07.jsonl` | `archive/ledger/` | Python-era artifact; Go uses audit_log table |
| `mcp/build_host_files.py` | `archive/mcp/` | Redundant with `cmd/oikos/build_hosts.go` |

### Full delete list (no archival value)

| Path | Why |
|------|-----|
| `oikos/approve.py` | Superseded by `internal/domain/approval.go` |
| `oikos/decide.py` | Superseded by `internal/policy/classify.go` |
| `oikos/drift.py` | Superseded by `internal/scheduler/scheduler.go` |
| `oikos/ledger.py` | Superseded by `internal/db/` + `audit_log` table |
| `oikos/policy.py` | Superseded by `internal/policy/classify.go` |
| `oikos/relations.py` | Superseded by `blast_radius()` SQL function |
| `oikos/scheduler.py` | Superseded by `internal/scheduler/scheduler.go` |
| `oikos/signal.py` | Superseded by `signals` table + `internal/domain/signal.go` |
| `oikos/__init__.py` | Package init, never imported by Go binary |

### What stays (read by Oikos Go binary)

| Path | Read by | Why keep |
|------|---------|----------|
| `seeds/ontology.yaml` | `oikos seed` | DB bootstrap |
| `seeds/inventory.yaml` | `oikos seed` | DB bootstrap |
| `seeds/policy.yaml` | `oikos seed` | DB bootstrap |
| `seeds/knowledge.yaml` | `oikos seed` | **NEW** — knowledge bootstrap |
| `inventory.yaml` | `oikos build-hosts`, `oikos homelab list` | Topology source of truth |
| `secrets/*.yaml` | `oikos secret list`, `oikos homelab secret` | SOPS-encrypted secrets |
| `migrations/*.sql` | `oikos migrate` | Embedded SQL migrations |
| `api/openapi.yaml` | `make generate` (oapi-codegen) | API contract |
| `compose/`, `Dockerfile`, `docker-compose.yml` | Docker build/deploy | Runtime packaging |
| `cmd/`, `internal/` | `go build` | Source code |
| `bin/homelab` | `oikos homelab` subcommand | CLI binary |
| `oikos/gen-topology.py` | `bin/homelab` (Python shim) | Topology generation (until ported) |
| `oikos/gen_topology_lib.py` | imported by gen-topology.py | Same |
| `scripts/deploy.sh` | Gitea webhook → deploy | Production deploy |
| `scripts/rollback.sh` | Operator manual | Rollback |
| `scripts/watchdog.sh` | cron on mac-mini | Health monitoring |
| `scripts/verify-phase6.sh` | Operator manual | Acceptance verification |
| `scripts/sync/` | systemd/launchd timers | Repo sync to clients |
| `scripts/check-caddy-backends.sh` | Operator diagnostic | Health check utility |
| `scripts/cutover-checklist.md` | Operator reference | Cutover tracking |
| `scripts/dns-sync.py` | Operator manual (not yet ported) | DNS sync to Technitium |
| `scripts/validate-seeds.py` | CI / operator manual | Seed validation |
| `tools/` | `post-pull.sh` auto-setup | Client tooling install |
| `ssh/` | `homelab ssh` | SSH key deployment |
| `vps/` | `homelab render-vps-configs` | VPS templates |
| `secrets-issuance/` | Running on apps/105 | Identity issuance (until decommissioned) |
| `AGENTS.md` | Agent onboarding | Entry point |
| `README.md` | Human onboarding | Entry point |
| `.agents/` | Agent conventions, skills, operating model | Agent runtime guidance |
| `plans/` | Agent construction docs | Intent documents |
| `hermes/` | Hermes config + persona | Agent gateway config |
| `Makefile`, `go.mod`, `go.sum`, `sqlc.yaml`, `.gitignore`, `.sops.yaml`, `.gitea/` | Build/config | Build tooling |

### Ambiguous — needs confirmation

| Path | Question |
|------|----------|
| `.agents/` | Contains OIKOS.md (operating model), SKILL.md runbooks, conventions. Go binary doesn't read these at runtime — they're agent orientation docs. Should they also be converted to seeds and archived? Or do they stay as the "agent manual" alongside AGENTS.md? |
| `scripts/dns-sync.py`, `scripts/validate-seeds.py` | Actively used Python scripts with no Go equivalent. Keep until ported? |
| `secrets-issuance/` | Running on apps/105. Archive after decommission? |

## Architecture change

```
BEFORE (split):
  knowledge/wiki/  →  agents grep clone for narrative
  Postgres DB      →  agents query MCP for structured data
  Two surfaces, no bridge

AFTER (DB-only):
  seeds/knowledge.yaml  →  oikos seed  →  knowledge_entities table
                                            ↓
                                     MCP search_knowledge("jellyfin transcode")
                                     MCP get_entity_knowledge("lxc:jellyfin")
                                     HTTP GET /api/v1/knowledge/search
                                            ↓
                                     One surface, everything indexed
```

```
┌──────────────────────────────────┐
│  seeds/knowledge.yaml             │  Version-controlled source material
│  (generated by oikos export)      │  Round-trip: seed → DB → export → seed
│  documents:                       │
│    - slug: containers/101-jellyfin│
│      entity: lxc:jellyfin         │
│      title: "101 — jellyfin"      │
│      content: "..."               │
│      at_glance: {host: strong, ..}│
│      changelog: [{date, title, ..}]│
│      tags: [container, media, ..] │
│  investigations:                  │
│    - slug: 2026-06-06-caddy-...   │
│      about: [service:caddy, ...]  │
│      ...                          │
│  runbooks:                        │
│    - slug: service-health-check   │
│      procedure_for: [service]     │
│      ...                          │
└──────────────┬───────────────────┘
               │ oikos seed
               ▼
┌──────────────────────────────────┐
│  knowledge_entities table         │
│  title | content | content_hash   │
│  source | tags                    │
│  ─ FTS: ts_vector(title+content)  │
│                                   │
│  + entities table:                │
│    document:containers/101-jellyfin│
│    investigation:2026-06-06-...   │
│    runbook:service-health-check   │
│                                   │
│  + relationships:                 │
│    document ──documents──► lxc    │
│    investigation ──about──► service│
│    runbook ──procedure-for──► type│
└──────────────────────────────────┘
               │
               ▼
┌──────────────────────────────────┐
│  MCP + HTTP API                   │
│  search_knowledge("jellyfin")     │
│  get_entity_knowledge("lxc:...")  │
│  POST /api/v1/knowledge/{id}      │
└──────────────────────────────────┘
```

## Execution — Fresh install on mac-mini

The current Docker stack on mac-mini was deployed during cutover testing with dev data.
Replace it with a fresh production instance using the real seeds (ontology, inventory,
policy, knowledge).

### Steps

```bash
# On mac-mini (192.168.8.175):

# 1. Stop current stack
docker compose --profile full down

# 2. Wipe old Postgres volume (fresh start)
docker volume rm oikos_pg-data

# 3. Pull latest repo (already committed to local main)
git pull origin main

# 4. Rebuild image with current code
docker compose build

# 5. Fresh migrate + seed (now includes knowledge)
docker compose run --rm migrate
docker compose run --rm seed

# 6. Verify seed ingested
docker compose run --rm -e OIKOS_DATABASE_URL=... oikos \
  psql -c "SELECT count(*) FROM knowledge_entities;"
# Expect: 54+ rows

# 7. Start production stack
docker compose --profile full up -d

# 8. Health check
curl http://localhost:8090/healthz
./scripts/verify-phase6.sh

# 9. Verify knowledge endpoints
curl "http://localhost:8090/api/v1/knowledge/search?q=jellyfin"
```

After fresh install succeeds, the old `backups/pre-cutover-20260707.sql` dump becomes
stale (it's from the dev instance). The new instance is the canonical production DB.

---

## Phase 1 — Seed format + ingestion

### 1.1 Add `content_hash` to `knowledge_entities`

**Migration 010:**

```sql
ALTER TABLE knowledge_entities ADD COLUMN content_hash TEXT;
```

### 1.2 Seed format: `seeds/knowledge.yaml`

One YAML file with three sections. Ingested by `oikos seed` after ontology, inventory,
and policy.

```yaml
# Oikos knowledge seed — documents, investigations, and runbooks.
# Generated by oikos export. Ingested on deploy.
# After ingest the DB is authoritative.

version: 1

documents:
  - slug: "containers/101-jellyfin"
    title: "101 — jellyfin"
    content: |
      # 101 — jellyfin

      Jellyfin media server with hardware-accelerated transcoding...

      ## Service / port
      | Service | Port | Notes |
      |---------|------|-------|
      | jellyfin | 8096 | Web UI + API |
      ...

      ## SSO
      Authentik OIDC via SSO-Auth plugin v4.0.0.4. No Caddy forward-auth gate.
      ...
    entity_slug: "lxc:jellyfin"         # links via `documents` edge
    tags: ["container", "media", "jellyfin"]
    at_glance:                          # structured: pushed to entity attributes if empty
      host: strong
      ip: "192.168.8.246"
      cores: 4
      ram: "8 GiB"
      mounts: ["/mnt/media_local"]
      public_host: media.hubris.network
    changelog:
      - date: "2026-07-05"
        title: "migrated from hubris to strong"
        body: "Phase 2 of strong migration..."
      - date: "2026-06-10"
        title: "VAAPI configured on Radeon 680M"
        body: "GPU passthrough via dev0 + dev1..."

  - slug: "hosts/strong"
    title: "strong — Proxmox host"
    content: |
      # strong — Proxmox host
      Reformatted from Linux workstation to Proxmox VE 9.2.3...
    entity_slug: "proxmox-host:strong"
    tags: ["host", "proxmox", "hypervisor"]
    at_glance:
      role: hypervisor
      hardware: "Minisforum UM773 Lite (Ryzen 7 PRO 6850U, 28 GiB RAM)"
      os: linux
    changelog:
      - date: "2026-07-05"
        title: "strong migration Phase 2 complete"
        body: "arriman + jellyfin + grimmory migrated. ludo-lvm..."

  # ... 35+ more document entries

investigations:
  - slug: "2026-06-06-caddyfile-truncation"
    title: "Caddyfile truncation — all LAN services down"
    date: "2026-06-06"
    status: resolved
    duration: "~45 min"
    content: |
      ## Symptom
      All *.hubris.network services returned 502...
      ## Root cause
      ...
    about_slugs: ["service:caddy", "service:dns"]  # links via `about` edges
    tags: ["incident", "caddy", "dns", "downtime"]

  - slug: "2026-06-06-authentik-session-lifetime"
    title: "Frequent Authentik login prompts"
    date: "2026-06-06"
    status: resolved
    duration: "~30 min"
    content: |
      ## Summary
      ...
    about_slugs: ["service:authentik"]
    tags: ["incident", "authentik", "oidc"]

  # ... 3 more investigation entries

runbooks:
  - slug: "service-health-check"
    name: "Service health check"
    risk_class: read_only
    entity_type: "service"              # applies to all service entities
    procedure:
      params_schema:
        type: object
        properties:
          unit: {type: string}
        required: ["unit"]
      steps:
        - name: "check systemd unit"
          runner: ssh
          command: "systemctl is-active {{ .unit }}"
          timeout_s: 10
      verify:
        - runner: ssh
          command: "systemctl is-active {{ .unit }}"
          expect: {exit_code: 0, stdout_contains: "active"}

  - slug: "lifecycle-provision-node"
    name: "Provision new node"
    risk_class: config_mutation
    entity_type: "lxc"
    procedure:
      params_schema:
        type: object
        properties:
          name: {type: string}
          template: {type: string}
          cores: {type: integer}
          memory_mb: {type: integer}
        required: ["name", "template"]
      steps:
        - name: "create container"
          runner: ssh
          target: "{{ .host }}"
          command: "pct create $(pvesh get /nextid) {{ .template }} ..."
          timeout_s: 120
      verify: [...]

  # ... 10 more runbook entries
```

### 1.3 Ingest logic

`oikos seed` already runs: migrate → ontology → inventory → policy.

Add knowledge as step 5:

```
oikos seed
  1. migrate (DDL)
  2. ontology seed (entity_types, relationship_types, lifecycles)
  3. inventory seed (entities, relationships)
  4. policy seed (risk_classes, approval_rules, autonomy)
  5. knowledge seed (documents, investigations, runbooks)  ← NEW
```

Knowledge ingest for each section:

**Documents:**
- For each entry, create `document:<slug>` entity in `entities` table (type: `document`)
- Insert into `knowledge_entities` (title, content, source=slug, tags, content_hash)
- Create `documents` edge from `document:<slug>` to `entity_slug`
- Push `at_glance` fields into the linked entity's `attributes` if those keys are empty (backfill)
- Store `changelog` as JSONB in document entity `attributes.changelog`
- Skip if `content_hash` matches existing row (idempotent)

**Investigations:**
- Create `investigation:<slug>` entity (type: `investigation`, lifecycle: `infrastructure`)
- Insert into `knowledge_entities`
- Create `about` edges to each entity in `about_slugs`
- Store `date`, `status`, `duration` in entity `attributes`

**Runbooks:**
- Create `runbook:<slug>` entity (type: `runbook`, lifecycle: `skill`)
- Insert into `knowledge_entities`
- Create `procedure-for` edges to `entity_type` (abstract type — applies to all instances)
- If a `skills` row with matching slug already exists, only update `knowledge_entities.content` and `content_hash` (don't overwrite the structured `procedure` JSONB in the skills table — that's managed separately)

### 1.4 Export: DB → seed

`oikos export` already writes `seeds/ontology.yaml`, `seeds/inventory.yaml`,
`seeds/policy.yaml`. Add `seeds/knowledge.yaml`:

```go
// internal/db/export.go
func ExportKnowledge(ctx context.Context, pool *pgxpool.Pool) ([]byte, error) {
    // SELECT all document + investigation + runbook entities
    // JOIN knowledge_entities for content
    // JOIN relationships for entity links
    // Serialize to seeds/knowledge.yaml format
}
```

Round-trip guarantee: `seed → DB → export → seed` is byte-stable. Tested in CI.

## Phase 2 — Convert wiki to seeds, archive originals

### 2.1 Conversion script (one-time)

```
oikos knowledge convert [--dry-run]
```

- Reads every page in `knowledge/wiki/`, `knowledge/sources/`, `knowledge/GLOSSARY.md`
- Parses structured sections (at-glance, changelog, procedures)
- Maps each page to entity slugs using the path convention + `inventory.yaml` doc_page
- Generates `seeds/knowledge.yaml`
- Reports: "would write N documents, M investigations, K runbooks"
- Without `--dry-run`: writes `seeds/knowledge.yaml`

Implementation: initially a Go version in `cmd/oikos/`, can fall back to a one-shot
Python script if that's faster to build.

### 2.2 Archive

After conversion verified (seeds ingest cleanly):

```bash
# Create archive directory
mkdir -p archive/knowledge/{containers,hosts,infrastructure,vms,investigations,references}

# Move all knowledge files
mv knowledge/wiki/containers/*.md archive/knowledge/containers/
mv knowledge/wiki/hosts/*.md archive/knowledge/hosts/
mv knowledge/wiki/infrastructure/*.md archive/knowledge/infrastructure/
mv knowledge/wiki/vms/*.md archive/knowledge/vms/
mv knowledge/sources/investigations/*.md archive/knowledge/investigations/
mv knowledge/sources/references/*.md archive/knowledge/references/
mv knowledge/index.md knowledge/log.md knowledge/GLOSSARY.md archive/knowledge/

# Move other stale artifacts
mv oikos/cards/ archive/oikos-cards/
mv .hermes/plans/*.md archive/hermes-plans/
mv ledger/2026-07.jsonl archive/ledger/
mv mcp/build_host_files.py archive/mcp/

# Delete superseded Python
rm oikos/__init__.py oikos/approve.py oikos/decide.py oikos/drift.py \
   oikos/ledger.py oikos/policy.py oikos/relations.py oikos/scheduler.py \
   oikos/signal.py

# Clean empty directories
rmdir knowledge/sources/investigations/archive/  # already moved
rmdir knowledge/sources/investigations/
rmdir knowledge/sources/references/
rmdir knowledge/sources/
rmdir knowledge/wiki/containers/archive/          # already moved
rmdir knowledge/wiki/containers/
rmdir knowledge/wiki/hosts/
rmdir knowledge/wiki/infrastructure/
rmdir knowledge/wiki/vms/
rmdir knowledge/wiki/
```

### 2.3 What the repo looks like after

```
/
├── seeds/
│   ├── ontology.yaml
│   ├── inventory.yaml
│   ├── policy.yaml
│   └── knowledge.yaml        ← NEW: documents + investigations + runbooks
├── archive/
│   ├── knowledge/            ← all old wiki markdown
│   ├── oikos-cards/          ← 45 Python explain cards
│   ├── hermes-plans/         ← 7 agent construction docs
│   ├── ledger/               ← Python-era JSONL
│   └── mcp/                  ← old Python build_host_files.py
├── .agents/                  ← agent conventions + skills (stays)
├── plans/                    ← construction plans (stays)
├── cmd/, internal/           ← Go source
├── migrations/, api/, compose/
├── inventory.yaml, hosts/*.yaml
├── AGENTS.md, README.md
└── ...
```

The repo shrinks by ~65 narrative files and ~45 card files. All knowledge is in
`seeds/knowledge.yaml` — one file, git-tracked, round-trippable.

## Phase 3 — Un-stub MCP + HTTP

Same as previous revision:

- `search_knowledge(query)` → PostgreSQL FTS with `ts_rank`, `ts_headline`
- `get_entity_knowledge(slug)` → aggregates documents, investigations, runbooks linked to entity
- `GET /api/v1/knowledge/search` → FTS endpoint
- `GET /api/v1/knowledge/{slug}` → entity-linked knowledge
- `POST /api/v1/knowledge/{slug}` → agent registers new knowledge
- `PATCH /api/v1/knowledge/{slug}` → agent updates knowledge

## Phase 4 — Agent conventions

### Knowledge registration

Agents call `POST /api/v1/knowledge/{entity_slug}` when they:
- Discover an undocumented detail (e.g., config quirk, gotcha)
- Complete an investigation
- Create a new entity and need to attach docs
- Update entity attributes discovered during operation

### Knowledge query (single call pattern)

```python
# One call replaces grep + get_entity + get_relations
ctx = mcp.get_entity_knowledge("lxc:jellyfin")
# Returns:
#   - entity attributes (structured)
#   - documents (full-text indexed narrative)
#   - investigations (any incidents involving this entity)
#   - runbooks (procedures that apply to this entity type)
#   - relationships (blast radius, dependencies)
```

### Export cycle

```
agent discovers → POST /api/v1/knowledge → DB updated
operator reviews → oikos export → seeds/knowledge.yaml updated
git commit + push → version-controlled, diffable
next deploy → oikos seed → DB back in sync
```

## Files to create/modify

| File | Action |
|------|--------|
| `migrations/010_knowledge_hash.up.sql` | New — `content_hash TEXT` |
| `internal/knowledge/seed.go` | New — knowledge seed ingest (read seeds/knowledge.yaml → DB) |
| `internal/knowledge/seed_test.go` | New — round-trip tests |
| `internal/knowledge/convert.go` | New — one-shot wiki→seed converter |
| `internal/knowledge/parser.go` | New — section parser (at-glance, changelog) |
| `internal/db/seed.go` | Modify — add knowledge step after policy |
| `internal/db/export.go` | Modify — add `ExportKnowledge` |
| `internal/httpapi/impl.go` | Modify — un-stub knowledge endpoints |
| `internal/httpapi/knowledge.go` | New — knowledge handlers |
| `internal/mcp/server.go` | Modify — un-stub `search_knowledge`, `get_entity_knowledge` |
| `cmd/oikos/main.go` | Modify — add `knowledge convert` subcommand |
| `api/openapi.yaml` | Modify — knowledge endpoint schemas |
| `seeds/knowledge.yaml` | New — generated by converter, ingested on seed |
| `archive/` | New — all old wiki + stale files |
| `plans/2026-07-07-comprehensive-audit-and-next-steps.md` | Modify — mark knowledge items as addressed |

## Verification

```bash
# 1. Convert wiki to seed
oikos knowledge convert --dry-run
# → "would write 37 documents, 5 investigations, 12 runbooks"
oikos knowledge convert

# 2. Fresh DB, full seed
oikos migrate && oikos seed

# 3. Verify knowledge populated
psql -c "SELECT title, source, cardinality(tags) FROM knowledge_entities;"
# → 54 rows (37 docs + 5 investigations + 12 runbooks)

# 4. Verify FTS
curl "http://localhost:8090/api/v1/knowledge/search?q=jellyfin+transcode"
# → returns document:containers/101-jellyfin with ts_headline snippet

# 5. Verify entity-linked knowledge
curl "http://localhost:8090/api/v1/knowledge/lxc:jellyfin"
# → {entity, documents: [...], investigations: [], runbooks: [...], relationships: {...}}

# 6. Verify MCP
curl -X POST localhost:8092/query \
  -d '{"tool":"search_knowledge","args":{"query":"caddy truncation"}}'
# → returns investigation:2026-06-06-caddyfile-truncation

# 7. Verify agent registration
curl -X POST localhost:8090/api/v1/knowledge/lxc:jellyfin \
  -H "Content-Type: application/json" \
  -d '{"title":"VAAPI note","content":"Requires /dev/dri/renderD128 passthrough","tags":["gpu"]}'
# → 201 Created

# 8. Verify export round-trip
oikos export   # writes seeds/*.yaml
oikos seed     # re-ingests (skips all — hashes match)
# → "knowledge: ingested 0, skipped 54, errors 0"

# 9. Archive originals (manual, after verification)
# → move all wiki files to archive/ as listed in Phase 2.2

# 10. Verify repo still builds and deploys
make generate && make build && make test
docker compose --profile dev up -d
./scripts/verify-phase6.sh
```

---

## Changelog

### 2026-07-07 — v2: full archive, wiki→seeds only
Convert wiki to seeds, archive ALL originals, archive all stale files. No human-editing
frontend retained — DB is the only source of truth. Knowledge cycle: agent API →
oikos export → seeds/knowledge.yaml → git → oikos seed → DB.