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 locally — dockertest 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-argusLXC — LXC 107 on dumbledore, 192.168.1.34, 4 CPU / 8 GB, 40 G rootfs + 100 G/datamount (storage1) for Postgres/memory. Docker 29.5.3. CST. - [x] Komodo Periphery onboarded —
komodo-periphery-sopsdeployed on 107, DNS pinned to pihole (.52), keypair + core.pub + age key in place, Core trust updated, server registered withaddress:""→ state: Ok (2026-06-09). - [ ] Forgejo Packages token for CI image push — store as Forgejo Actions secret
PACKAGES_TOKENon theargusrepo. (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 —
BIGGENERATEDmust beBIGINT 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.goand 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
}
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 (
Healthzundefined).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: Commit —
git 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: Commit —
git 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
}
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 stubNoShows(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 bothcmd/*/main.goentrypoints (load config, connect db, migrate, ListenAndServe). Thengo 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: Commit —
git 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.23→gcr.io/distroless/static; copy the built binary +migrations/). One per service, building the matching./cmd/.... -
[ ] Step 2: compose.yaml — services:
postgres(pgvector/pgvector:pg17, healthcheckpg_isready, volume),memory(build Dockerfile.memory,ARGUS_DATABASE_URLfrom 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+healthcheckhitting/healthz. -
[ ] Step 3:
.env.exampledocuments 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.envwith real values, thensops -e .env > secrets.enc.env(age recipient = the standard homelab key), thenrm .env. Verifygrep -q sops secrets.enc.env. -
[ ] Step 5: Verify compose parses —
docker compose config -q(aftercp .env.example .env). Thenrm .env. -
[ ] Step 6: Commit —
git 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.shExpected:SMOKE PASS. (Ports: memory:8081, hub:8080— set in compose.) -
[ ] Step 3: Commit —
git 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: Commit —
git 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 flippasses. 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, confirminit.shis green, ensurefeature_list.json+AGENT.mdexist, commit baseline.PROMPT-coding.md— the per-loop prompt: the Anthropic ritual (pwd→ readclaude-progress.txt→git log→feature_list.json→ runinit.shsmoke → pick ONEpasses:falsefeature → TDD it → E2E verify → flippasses→ 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 everypasses:true. -
[ ] Step 3:
claude-progress.txt— seed with "M0 harness complete; M1 not started." -
[ ] Step 4: Commit —
git 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+ pushargus-hub/argus-memory/task-runnertogit.aaronbrazier.com/runonyourown/*Forgejo Packages usingPACKAGES_TOKEN, (3) the homelab-apps validate gate (yamllint compose,docker compose config,grep -q sops secrets.enc.env, secret scan). Mirrorhomelab-apps/.forgejo/workflows/ci.ymlfor 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 + push —
git 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, itsci.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.Migrateare used consistently across tasks 2–8. - Known seeded bug: the
BIGGENERATEDtypo 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.