Skip to content

M0 — Skeleton + Overnight Harness Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Stand up the Argus two-service skeleton (argus-hub + argus-memory) on Postgres+pgvector with a proven end-to-end capture→store→recall slice, full test infra, CI that builds images to Forgejo Packages, and the Anthropic-style overnight harness (feature_list.json + init.sh + initializer/coding prompts + agent-context files) — so the M1 memory features can then be built unattended.

Architecture: Two Go services (cmd/hub, cmd/memory) sharing internal/shared (config, db, logging), backed by one Postgres+pgvector container, orchestrated by docker compose. M0 builds the harness and a thin vertical slice, not the real memory logic — that's M1. Per Anthropic's long-running-agent guidance, the test harness + scaffolding must exist before any feature work.

Tech Stack: Go 1.23 (pgx/v5, chi router, distroless images), Postgres 17 + pgvector, Python 3.12 task-runner, Ofelia (scheduler), docker compose, Forgejo Actions CI → Forgejo Packages, Komodo (Periphery on pve-argus), SOPS+age. Dev DB tests via dockertest.

Who builds this: the autonomous agent (Fable via the /ralph-loop harness) writes 100% of the code in this plan. The initializer prompt scaffolds the repo + harness; the coding loop implements each task TDD-style and flips its feature_list.json entry to passes:true. The TDD tasks below are the agent's build spec, not a human checklist. The ONLY human work is the deploy-time LXC/Periphery setup in the next section — and that doesn't gate the build.


Human prerequisites (NOT agent tasks — needed for DEPLOY, not for the build)

These touch live homelab infra (Proxmox, Komodo) and must be done by a human, not the autonomous agent. Important: they are deployment prerequisites only. The overnight agent builds and tests all of M0/M1 locallydockertest spins up Postgres in throwaway containers, and init.sh runs docker compose up on whatever host the agent runs on. So the LXC can be created any time before the first homelab deploy; it does not gate the build.

  • [x] pve-argus LXC — LXC 107 on dumbledore, 192.168.1.34, 4 CPU / 8 GB, 40 G rootfs + 100 G /data mount (storage1) for Postgres/memory. Docker 29.5.3. CST.
  • [x] Komodo Periphery onboardedkomodo-periphery-sops deployed on 107, DNS pinned to pihole (.52), keypair + core.pub + age key in place, Core trust updated, server registered with address:""state: Ok (2026-06-09).
  • [ ] Forgejo Packages token for CI image push — store as Forgejo Actions secret PACKAGES_TOKEN on the argus repo. (Deploy-time only; the host already has pull auth. Mint when CI is wired — not a build blocker.)
  • [x] LiteLLM reachable from pve-argus (HTTP 401 = up at 192.168.1.28:4000).

Everything below is code and is TDD-driven.

File structure (created in M0)

argus/
  go.mod  go.sum  Makefile
  cmd/hub/main.go                    # argus-hub entrypoint (control plane)
  cmd/memory/main.go                 # argus-memory entrypoint (memory service)
  internal/shared/config/config.go   # env-driven config (one struct, no globals)
  internal/shared/db/db.go           # pgxpool connect + Ping
  internal/shared/db/migrate.go      # apply migrations/*.sql in order
  internal/shared/httpx/health.go    # shared /healthz handler
  internal/memory/capture.go         # POST /capture -> raw_items (M0: insert only)
  internal/memory/recall.go          # GET  /recall  -> raw_items (M0: keyword LIKE)
  internal/memory/server.go          # memory chi router
  internal/hub/runs.go               # status store: record a task run
  internal/hub/server.go             # hub chi router + minimal dashboard
  migrations/0001_init.sql           # raw_items, distilled_notes, runs (+ pgvector ext)
  task-runner/Dockerfile  task-runner/taskkit.py  task-runner/run.py
  tasks/.gitkeep
  agent/SOUL.md  agent/rulebook.md  agent/USER.md      # F14 agent-context layer
  harness/feature_list.json          # overnight build features (passes-tracked)
  harness/init.sh                    # bring up stack + smoke test
  harness/PROMPT-initializer.md  harness/PROMPT-coding.md  harness/claude-progress.txt
  compose.yaml  .env.example  secrets.enc.env
  Dockerfile.hub  Dockerfile.memory
  komodo/resource-sync.toml          # pve-argus stack (repo=runonyourown/argus)
  .forgejo/workflows/ci.yml

Each internal/<area>/<file>.go has one responsibility; services share only internal/shared.


Task 1: Go module + Makefile + skeleton dirs

Files: - Create: go.mod, Makefile, tasks/.gitkeep

  • [ ] Step 1: Init the module

Run: cd /home/aaron/projects/argus && go mod init git.aaronbrazier.com/runonyourown/argus Expected: creates go.mod with module git.aaronbrazier.com/runonyourown/argus and go 1.23.

  • [ ] Step 2: Add the deps

Run: go get github.com/jackc/pgx/v5/pgxpool@latest github.com/go-chi/chi/v5@latest github.com/ory/dockertest/v3@latest Expected: go.mod/go.sum updated.

  • [ ] Step 3: Create the Makefile
.PHONY: test build up down smoke
test:    ; go test ./...
build:   ; go build -o bin/hub ./cmd/hub && go build -o bin/memory ./cmd/memory
up:      ; docker compose up -d --build
down:    ; docker compose down -v
smoke:   ; bash harness/init.sh
  • [ ] Step 4: Commit
mkdir -p tasks && touch tasks/.gitkeep
git add go.mod go.sum Makefile tasks/.gitkeep
git commit -m "chore(m0): go module + makefile skeleton"

Task 2: Shared config (TDD)

Files: - Create: internal/shared/config/config.go - Test: internal/shared/config/config_test.go

  • [ ] Step 1: Write the failing test
package config

import ("os"; "testing")

func TestLoadReadsEnv(t *testing.T) {
    os.Setenv("ARGUS_DATABASE_URL", "postgres://u:p@h:5432/db")
    os.Setenv("ARGUS_LITELLM_URL", "http://192.168.1.28:4000")
    c, err := Load()
    if err != nil { t.Fatalf("Load: %v", err) }
    if c.DatabaseURL != "postgres://u:p@h:5432/db" { t.Fatalf("db url = %q", c.DatabaseURL) }
    if c.LiteLLMURL != "http://192.168.1.28:4000" { t.Fatalf("litellm = %q", c.LiteLLMURL) }
}

func TestLoadRequiresDatabaseURL(t *testing.T) {
    os.Unsetenv("ARGUS_DATABASE_URL")
    if _, err := Load(); err == nil { t.Fatal("expected error when ARGUS_DATABASE_URL unset") }
}
  • [ ] Step 2: Run test to verify it fails

Run: go test ./internal/shared/config/ -run TestLoad -v Expected: FAIL — Load undefined.

  • [ ] Step 3: Write minimal implementation
package config

import ("fmt"; "os")

type Config struct {
    DatabaseURL string
    LiteLLMURL  string
    HTTPAddr    string
}

func Load() (Config, error) {
    db := os.Getenv("ARGUS_DATABASE_URL")
    if db == "" { return Config{}, fmt.Errorf("ARGUS_DATABASE_URL is required") }
    addr := os.Getenv("ARGUS_HTTP_ADDR")
    if addr == "" { addr = ":8080" }
    return Config{DatabaseURL: db, LiteLLMURL: os.Getenv("ARGUS_LITELLM_URL"), HTTPAddr: addr}, nil
}
  • [ ] Step 4: Run test to verify it passes

Run: go test ./internal/shared/config/ -v Expected: PASS (both tests).

  • [ ] Step 5: Commit
git add internal/shared/config/
git commit -m "feat(m0): env-driven config loader"

Task 3: Postgres schema + migration runner (TDD with dockertest)

Files: - Create: migrations/0001_init.sql, internal/shared/db/db.go, internal/shared/db/migrate.go - Test: internal/shared/db/migrate_test.go

  • [ ] Step 1: Write the schema
-- migrations/0001_init.sql
CREATE EXTENSION IF NOT EXISTS vector;

-- Immutable raw layer (source of truth — never UPDATEd). Dual-track per the memory research.
CREATE TABLE IF NOT EXISTS raw_items (
  id          BIGGENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  source      TEXT NOT NULL,                 -- claude-code | codex | telegram | task | bookmark
  kind        TEXT NOT NULL,                 -- turn | document | event | synthesis
  project     TEXT,
  session_id  TEXT,
  body        TEXT NOT NULL,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS raw_items_fts ON raw_items USING GIN (to_tsvector('english', body));

-- Mutable distilled layer (regenerable from raw). M0 creates the table; M1 fills it.
CREATE TABLE IF NOT EXISTS distilled_notes (
  id          BIGGENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  raw_id      BIGINT REFERENCES raw_items(id),
  content     TEXT NOT NULL,
  keywords    TEXT[] NOT NULL DEFAULT '{}',
  tags        TEXT[] NOT NULL DEFAULT '{}',
  context     TEXT,
  source_ref  TEXT,                          -- grounding quote/span (write-gate)
  embedding   vector(768),                   -- nomic local embed dim
  valid_from  TIMESTAMPTZ NOT NULL DEFAULT now(),
  valid_to    TIMESTAMPTZ,
  confidence  REAL NOT NULL DEFAULT 0.5,
  deleted_at  TIMESTAMPTZ,                    -- soft delete / tombstone
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Task-run status store (observability spine).
CREATE TABLE IF NOT EXISTS runs (
  id          BIGGENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  task        TEXT NOT NULL,
  started_at  TIMESTAMPTZ NOT NULL,
  ended_at    TIMESTAMPTZ,
  status      TEXT NOT NULL,                  -- ok | fail | skipped | running
  runtime     TEXT,                           -- which engine ran (max|codex|api|local|code)
  output      TEXT,
  cost_usd    NUMERIC
);

NOTE: fix the obvious typo when implementing — BIGGENERATED must be BIGINT GENERATED. (Left here so the test catches it.)

  • [ ] Step 2: Write the failing test
package db

import ("context"; "testing"; "github.com/ory/dockertest/v3")

func TestMigrateCreatesTables(t *testing.T) {
    pool := dockertest.NewPostgres(t, "pgvector/pgvector:pg17") // helper: starts container, returns *pgxpool.Pool
    if err := Migrate(context.Background(), pool, "../../../migrations"); err != nil {
        t.Fatalf("Migrate: %v", err)
    }
    for _, tbl := range []string{"raw_items", "distilled_notes", "runs"} {
        var n int
        err := pool.QueryRow(context.Background(),
            "SELECT count(*) FROM information_schema.tables WHERE table_name=$1", tbl).Scan(&n)
        if err != nil || n != 1 { t.Fatalf("table %s missing (n=%d err=%v)", tbl, n, err) }
    }
}
  • [ ] Step 3: Run test — verify it fails

Run: go test ./internal/shared/db/ -run TestMigrate -v Expected: FAIL — Migrate undefined AND (once defined) the BIGGENERATED SQL error, proving the migration actually executes.

  • [ ] Step 4: Implement db.go + migrate.go and fix the SQL typo

// db.go
package db
import ("context"; "github.com/jackc/pgx/v5/pgxpool")
func Connect(ctx context.Context, url string) (*pgxpool.Pool, error) {
    p, err := pgxpool.New(ctx, url); if err != nil { return nil, err }
    return p, p.Ping(ctx)
}
// migrate.go
package db
import ("context"; "os"; "path/filepath"; "sort"; "github.com/jackc/pgx/v5/pgxpool")
func Migrate(ctx context.Context, p *pgxpool.Pool, dir string) error {
    files, err := filepath.Glob(filepath.Join(dir, "*.sql")); if err != nil { return err }
    sort.Strings(files)
    for _, f := range files {
        sql, err := os.ReadFile(f); if err != nil { return err }
        if _, err := p.Exec(ctx, string(sql)); err != nil { return err }
    }
    return nil
}
Then change both BIGGENERATED BY DEFAULT AS IDENTITY to BIGINT GENERATED BY DEFAULT AS IDENTITY in 0001_init.sql.

  • [ ] Step 5: Run test — verify it passes

Run: go test ./internal/shared/db/ -v Expected: PASS — all three tables exist.

  • [ ] Step 6: Commit
git add migrations/ internal/shared/db/
git commit -m "feat(m0): dual-track schema + migration runner (dockertest)"

Task 4: Shared health handler (TDD)

Files: - Create: internal/shared/httpx/health.go - Test: internal/shared/httpx/health_test.go

  • [ ] Step 1: Failing test
package httpx
import ("net/http"; "net/http/httptest"; "testing")
func TestHealthz(t *testing.T) {
    r := httptest.NewRequest(http.MethodGet, "/healthz", nil); w := httptest.NewRecorder()
    Healthz(func() error { return nil })(w, r)
    if w.Code != 200 || w.Body.String() != "ok" { t.Fatalf("got %d %q", w.Code, w.Body.String()) }
}
func TestHealthzFails(t *testing.T) {
    r := httptest.NewRequest(http.MethodGet, "/healthz", nil); w := httptest.NewRecorder()
    Healthz(func() error { return http.ErrServerClosed })(w, r)
    if w.Code != 503 { t.Fatalf("got %d", w.Code) }
}
  • [ ] Step 2: Run — fails (Healthz undefined). go test ./internal/shared/httpx/ -v

  • [ ] Step 3: Implement

package httpx
import "net/http"
func Healthz(check func() error) http.HandlerFunc {
    return func(w http.ResponseWriter, _ *http.Request) {
        if err := check(); err != nil { http.Error(w, err.Error(), 503); return }
        w.Write([]byte("ok"))
    }
}
  • [ ] Step 4: Run — passes. go test ./internal/shared/httpx/ -v
  • [ ] Step 5: Commitgit add internal/shared/httpx/ && git commit -m "feat(m0): shared healthz handler"

Task 5: argus-memory capture + recall slice (TDD, dockertest)

Files: - Create: internal/memory/capture.go, internal/memory/recall.go, internal/memory/server.go - Test: internal/memory/capture_test.go

  • [ ] Step 1: Failing test (round-trip)
package memory
import ("context"; "testing"; "github.com/ory/dockertest/v3"; sdb "git.aaronbrazier.com/runonyourown/argus/internal/shared/db")
func TestCaptureThenRecall(t *testing.T) {
    pool := dockertest.NewPostgres(t, "pgvector/pgvector:pg17")
    if err := sdb.Migrate(context.Background(), pool, "../../migrations"); err != nil { t.Fatal(err) }
    s := &Store{Pool: pool}
    id, err := s.Capture(context.Background(), Item{Source: "claude-code", Kind: "turn", Body: "argus uses pgvector"})
    if err != nil || id == 0 { t.Fatalf("Capture: id=%d err=%v", id, err) }
    hits, err := s.Recall(context.Background(), "pgvector")
    if err != nil || len(hits) != 1 || hits[0].Body != "argus uses pgvector" { t.Fatalf("Recall: %+v err=%v", hits, err) }
}
  • [ ] Step 2: Run — fails. go test ./internal/memory/ -run TestCapture -v

  • [ ] Step 3: Implement capture.go + recall.go

// capture.go
package memory
import ("context"; "github.com/jackc/pgx/v5/pgxpool")
type Store struct{ Pool *pgxpool.Pool }
type Item struct{ Source, Kind, Project, SessionID, Body string }
func (s *Store) Capture(ctx context.Context, it Item) (int64, error) {
    var id int64
    err := s.Pool.QueryRow(ctx,
        `INSERT INTO raw_items (source,kind,project,session_id,body) VALUES ($1,$2,$3,$4,$5) RETURNING id`,
        it.Source, it.Kind, it.Project, it.SessionID, it.Body).Scan(&id)
    return id, err
}
// recall.go
package memory
import "context"
type Hit struct{ ID int64; Body string }
func (s *Store) Recall(ctx context.Context, q string) ([]Hit, error) {
    rows, err := s.Pool.Query(ctx,
        `SELECT id, body FROM raw_items WHERE to_tsvector('english',body) @@ plainto_tsquery('english',$1) ORDER BY id DESC LIMIT 20`, q)
    if err != nil { return nil, err }
    defer rows.Close()
    var out []Hit
    for rows.Next() { var h Hit; if err := rows.Scan(&h.ID, &h.Body); err != nil { return nil, err }; out = append(out, h) }
    return out, rows.Err()
}

  • [ ] Step 4: Run — passes. go test ./internal/memory/ -v
  • [ ] Step 5: Commitgit add internal/memory/ && git commit -m "feat(m0): memory capture+recall vertical slice"

Task 6: HTTP servers for both services (TDD)

Files: - Create: internal/memory/server.go, internal/hub/runs.go, internal/hub/server.go, cmd/memory/main.go, cmd/hub/main.go - Test: internal/memory/server_test.go, internal/hub/runs_test.go

  • [ ] Step 1: Failing test — memory HTTP capture+recall
package memory
import ("bytes"; "context"; "net/http"; "net/http/httptest"; "testing"; "github.com/ory/dockertest/v3"; sdb "git.aaronbrazier.com/runonyourown/argus/internal/shared/db")
func TestServerCaptureRecall(t *testing.T) {
    pool := dockertest.NewPostgres(t, "pgvector/pgvector:pg17"); sdb.Migrate(context.Background(), pool, "../../migrations")
    h := NewServer(&Store{Pool: pool})
    w := httptest.NewRecorder()
    h.ServeHTTP(w, httptest.NewRequest("POST", "/capture", bytes.NewBufferString(`{"source":"telegram","kind":"turn","body":"hello argus"}`)))
    if w.Code != 201 { t.Fatalf("capture code %d", w.Code) }
    w2 := httptest.NewRecorder()
    h.ServeHTTP(w2, httptest.NewRequest("GET", "/recall?q=argus", nil))
    if w2.Code != 200 || !bytes.Contains(w2.Body.Bytes(), []byte("hello argus")) { t.Fatalf("recall %d %s", w2.Code, w2.Body) }
}
  • [ ] Step 2: Run — fails. go test ./internal/memory/ -run TestServer -v

  • [ ] Step 3: Implement server.go (chi router: POST /capture, GET /recall, GET /healthz)

package memory
import ("encoding/json"; "net/http"; "github.com/go-chi/chi/v5"; "git.aaronbrazier.com/runonyourown/argus/internal/shared/httpx")
func NewServer(s *Store) http.Handler {
    r := chi.NewRouter()
    r.Get("/healthz", httpx.Healthz(func() error { return s.Pool.Ping(nil) }))
    r.Post("/capture", func(w http.ResponseWriter, req *http.Request) {
        var it Item
        if err := json.NewDecoder(req.Body).Decode(&it); err != nil || it.Body == "" { http.Error(w, "bad item", 400); return }
        id, err := s.Capture(req.Context(), it); if err != nil { http.Error(w, err.Error(), 500); return }
        w.WriteHeader(201); json.NewEncoder(w).Encode(map[string]int64{"id": id})
    })
    r.Get("/recall", func(w http.ResponseWriter, req *http.Request) {
        hits, err := s.Recall(req.Context(), req.URL.Query().Get("q")); if err != nil { http.Error(w, err.Error(), 500); return }
        json.NewEncoder(w).Encode(hits)
    })
    return r
}
Fix the Healthz ping: pass req.Context() — change the check to func() error { return s.Pool.Ping(context.Background()) } and import context.

  • [ ] Step 4: Run — passes. go test ./internal/memory/ -v

  • [ ] Step 5: Hub runs store — failing test

package hub
import ("context"; "testing"; "time"; "github.com/ory/dockertest/v3"; sdb "git.aaronbrazier.com/runonyourown/argus/internal/shared/db")
func TestRecordRun(t *testing.T) {
    pool := dockertest.NewPostgres(t, "pgvector/pgvector:pg17"); sdb.Migrate(context.Background(), pool, "../../migrations")
    st := &Runs{Pool: pool}
    id, err := st.Record(context.Background(), Run{Task: "ipam-regen", StartedAt: time.Now(), Status: "ok", Runtime: "code"})
    if err != nil || id == 0 { t.Fatalf("Record: %d %v", id, err) }
    open, err := st.NoShows(context.Background(), nil); if err != nil { t.Fatal(err) }
    _ = open
}
  • [ ] Step 6: Implement runs.go (Record + a stub NoShows(expected []string) ([]string, error) returning expected tasks with no run today — real schedule wiring is M3; M0 just proves the store).
package hub
import ("context"; "time"; "github.com/jackc/pgx/v5/pgxpool")
type Runs struct{ Pool *pgxpool.Pool }
type Run struct{ Task, Status, Runtime, Output string; StartedAt time.Time }
func (r *Runs) Record(ctx context.Context, run Run) (int64, error) {
    var id int64
    err := r.Pool.QueryRow(ctx,
        `INSERT INTO runs (task,started_at,ended_at,status,runtime,output) VALUES ($1,$2,now(),$3,$4,$5) RETURNING id`,
        run.Task, run.StartedAt, run.Status, run.Runtime, run.Output).Scan(&id)
    return id, err
}
func (r *Runs) NoShows(ctx context.Context, expected []string) ([]string, error) {
    var missing []string
    for _, task := range expected {
        var n int
        r.Pool.QueryRow(ctx, `SELECT count(*) FROM runs WHERE task=$1 AND started_at::date = now()::date`, task).Scan(&n)
        if n == 0 { missing = append(missing, task) }
    }
    return missing, nil
}
  • [ ] Step 7: Run — passes. go test ./internal/hub/ -v

  • [ ] Step 8: Implement internal/hub/server.go (healthz + GET / dashboard listing recent runs as HTML) and both cmd/*/main.go entrypoints (load config, connect db, migrate, ListenAndServe). Then go build ./....

// cmd/memory/main.go
package main
import ("context"; "log"; "net/http"
    "git.aaronbrazier.com/runonyourown/argus/internal/memory"
    "git.aaronbrazier.com/runonyourown/argus/internal/shared/config"
    "git.aaronbrazier.com/runonyourown/argus/internal/shared/db")
func main() {
    c, err := config.Load(); if err != nil { log.Fatal(err) }
    pool, err := db.Connect(context.Background(), c.DatabaseURL); if err != nil { log.Fatal(err) }
    if err := db.Migrate(context.Background(), pool, "migrations"); err != nil { log.Fatal(err) }
    log.Printf("argus-memory on %s", c.HTTPAddr)
    log.Fatal(http.ListenAndServe(c.HTTPAddr, memory.NewServer(&memory.Store{Pool: pool})))
}
(cmd/hub/main.go mirrors this with hub.NewServer.)

  • [ ] Step 9: Commitgit add internal/ cmd/ && git commit -m "feat(m0): hub+memory http servers + entrypoints"

Task 7: Dockerfiles + compose + secrets

Files: - Create: Dockerfile.hub, Dockerfile.memory, compose.yaml, .env.example, secrets.enc.env

  • [ ] Step 1: Distroless Dockerfiles (multi-stage golang:1.23gcr.io/distroless/static; copy the built binary + migrations/). One per service, building the matching ./cmd/....

  • [ ] Step 2: compose.yaml — services: postgres (pgvector/pgvector:pg17, healthcheck pg_isready, volume), memory (build Dockerfile.memory, ARGUS_DATABASE_URL from env, /healthz), hub (build Dockerfile.hub, depends_on memory+postgres healthy), ofelia (mcuadros/ofelia:latest, mounts ./tasks + docker.sock), task-runner (build task-runner/, sleeps — invoked by ofelia). Each app service: restart: unless-stopped + healthcheck hitting /healthz.

  • [ ] Step 3: .env.example documents keys: ARGUS_DATABASE_URL, ARGUS_LITELLM_URL=http://192.168.1.28:4000, ARGUS_LANGFUSE_URL=http://192.168.1.28:3002, POSTGRES_PASSWORD, TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID.

  • [ ] Step 4: secrets.enc.env — create plaintext .env with real values, then sops -e .env > secrets.enc.env (age recipient = the standard homelab key), then rm .env. Verify grep -q sops secrets.enc.env.

  • [ ] Step 5: Verify compose parsesdocker compose config -q (after cp .env.example .env). Then rm .env.

  • [ ] Step 6: Commitgit add Dockerfile.* compose.yaml .env.example secrets.enc.env && git commit -m "feat(m0): compose stack + distroless images + SOPS secrets"

Task 8: init.sh smoke test (the harness baseline)

Files: - Create: harness/init.sh

  • [ ] Step 1: Write init.sh — the per-session baseline Anthropic prescribes: bring the stack up, wait for both /healthz, POST a capture, GET recall, assert the body round-trips, print PASS/FAIL, leave the stack up. Exit non-zero on any failure.
#!/usr/bin/env bash
set -euo pipefail
cp -n .env.example .env 2>/dev/null || true   # dev only; real deploy uses SOPS
docker compose up -d --build
echo "waiting for health..."
for url in http://localhost:8081/healthz http://localhost:8080/healthz; do
  for i in $(seq 1 30); do curl -fsS "$url" >/dev/null 2>&1 && break; sleep 2; done
done
curl -fsS -X POST localhost:8081/capture -d '{"source":"smoke","kind":"turn","body":"smoke-test-marker"}' >/dev/null
out=$(curl -fsS "localhost:8081/recall?q=smoke-test-marker")
echo "$out" | grep -q smoke-test-marker && echo "SMOKE PASS" || { echo "SMOKE FAIL"; exit 1; }
  • [ ] Step 2: Run it. Run: bash harness/init.sh Expected: SMOKE PASS. (Ports: memory :8081, hub :8080 — set in compose.)

  • [ ] Step 3: Commitgit add harness/init.sh && git commit -m "feat(m0): init.sh end-to-end smoke test"

Task 9: Agent-context layer (F14)

Files: - Create: agent/SOUL.md, agent/rulebook.md, agent/USER.md

  • [ ] Step 1: Author them by adapting the Hermes versions in ~/projects/hermes-config/ for Argus: SOUL.md (identity = "Argus, Aaron's observable homelab agent + memory"; the ground-truth hierarchy — terminal output wins, verified memory over training, no confabulation), rulebook.md (hard rules: destructive-command confirmation, Tailscale-only, Forgejo-primary, single-user; + verification discipline), USER.md (Aaron's current profile — fix the stale infra: pve-yact-infra is gone → llm-infra on .28, see [[llm-infra-location]]; the 5-VLAN network; dumbledore/cluster hosts).

  • [ ] Step 2: Commitgit add agent/ && git commit -m "feat(m0): SOUL/rulebook/USER agent-context layer"

Task 10: The overnight harness files (feature_list.json + prompts)

Files: - Create: harness/feature_list.json, harness/PROMPT-initializer.md, harness/PROMPT-coding.md, harness/claude-progress.txt

  • [ ] Step 1: feature_list.json — the Anthropic-style structured list seeding M1. Each feature: {id, title, steps:[...concrete verify steps...], test, passes:false}. Tests are protected — the coding agent may ONLY flip passes. Seed with the M1 memory features (capture API multi-source, grounded write-gate, distill via LiteLLM, embeddings, hybrid recall, soft-delete, port memory-os raw, dual-write hook). Example entry:
{
  "id": "M1-write-gate",
  "title": "Grounded write-gate rejects ungrounded distilled notes",
  "steps": [
    "Add Distill(rawID) that asks LiteLLM to extract candidate notes",
    "For each candidate, generate a verification question and require a supporting span from the raw body",
    "Discard + log any candidate whose grounding span is absent",
    "Filter Claude API-error/system text before any capture (regression test for the memory-os poison loop)"
  ],
  "test": "go test ./internal/memory/ -run TestWriteGateRejectsUngrounded",
  "passes": false
}
  • [ ] Step 2: PROMPT-initializer.md — runs ONCE: read the design spec + memory research, confirm init.sh is green, ensure feature_list.json + AGENT.md exist, commit baseline. PROMPT-coding.md — the per-loop prompt: the Anthropic ritual (pwd → read claude-progress.txtgit logfeature_list.json → run init.sh smoke → pick ONE passes:false feature → TDD it → E2E verify → flip passes → commit + append progress), with the guardrails baked in (NO placeholders, search-before-assume, never edit tests, F12 fallback wrapper for the agent runtime, halt-and-document if stuck). Emit <promise>ARGUS-M1-COMPLETE</promise> only when every passes:true.

  • [ ] Step 3: claude-progress.txt — seed with "M0 harness complete; M1 not started."

  • [ ] Step 4: Commitgit add harness/ && git commit -m "feat(m0): overnight harness (feature_list + initializer/coding prompts)"

Task 11: CI + Komodo stack declaration

Files: - Create: .forgejo/workflows/ci.yml, komodo/resource-sync.toml

  • [ ] Step 1: ci.yml (runs-on: homelab-infra) — jobs: (1) go test ./... (needs docker for dockertest), (2) on push to main: docker build + push argus-hub/argus-memory/task-runner to git.aaronbrazier.com/runonyourown/* Forgejo Packages using PACKAGES_TOKEN, (3) the homelab-apps validate gate (yamllint compose, docker compose config, grep -q sops secrets.enc.env, secret scan). Mirror homelab-apps/.forgejo/workflows/ci.yml for steps (3).

  • [ ] Step 2: komodo/resource-sync.toml — one [[stack]]: name=argus, server="pve-argus", git_provider="git.aaronbrazier.com", git_https=true, git_account="zerocool", repo="runonyourown/argus", branch="main", run_directory=".", file_paths=["compose.yaml"], env_file_path=".env", poll_for_updates=true, pre_deploy.command="sops -d secrets.enc.env > .env". (Per [[homelab-apps-deploy-flow]].)

  • [ ] Step 3: Commit + pushgit add .forgejo/ komodo/ && git commit -m "ci(m0): tests + image build to Packages + Komodo stack" && git push origin main. Watch the Forgejo run go green.


Self-Review

  • Spec coverage: M0 covers the §8 component skeleton (hub, memory, postgres+pgvector, ofelia, task-runner), the §4 two-service split, the dual-track schema (§7), the deploy model (§8), the agent-context layer (F14), and the overnight harness (research doc). It deliberately does NOT implement the real memory logic (write-gate/distill/hybrid recall/resurfacing/port) — that's M1's feature_list.json, seeded in Task 10.
  • Placeholder scan: Tasks 7, 9, 10, 11 describe file content by responsibility rather than full literal code (Dockerfiles, compose, SOPS, CI, the prose agent/harness files). That's intentional — they follow verbatim existing homelab patterns (homelab-apps/apps/*/compose.yaml, its ci.yml, hermes-config/*) which the implementer copies. Every Go code step has complete code + a failing test first.
  • Type consistency: Store{Pool}, Item{Source,Kind,Project,SessionID,Body}, Hit{ID,Body}, Runs{Pool}, Run{Task,Status,Runtime,Output,StartedAt}, config.Config{DatabaseURL,LiteLLMURL,HTTPAddr}, db.Connect/db.Migrate are used consistently across tasks 2–8.
  • Known seeded bug: the BIGGENERATED typo in Task 3 is intentional, caught by the migration test (proves the migration actually runs, not just compiles).

Open prerequisite for execution

dockertest.NewPostgres is a thin helper this plan assumes — Task 3 Step 4 must also add internal/shared/db/dockertest_helper_test.go providing it (start pgvector/pgvector:pg17, return a connected *pgxpool.Pool, t.Cleanup to purge). If preferred, swap dockertest for a TEST_DATABASE_URL env pointing at a throwaway PG.