Zion Boggan
repos/Zion Pm/SEED.md
zionboggan.com ↗
363 lines · markdown
History for this file →
1
# Zion PM v2 - Build Spec (SEED)
2
 
3
> **Status:** AUTHORITATIVE build charter. Execute top-to-bottom.
4
> **Origin:** Authored by GLM (CT247 opencode session) for Claude CT215 to implement.
5
> **Date:** 2026-06-20. **Target:** live today.
6
> **Replaces:** `claude-memory/zion-pm-conductor.md` (the earlier sketch - this is the real build).
7
 
8
---
9
 
10
## 1. Mission
11
 
12
A single self-hosted app that is Zion's **Project Manager**. The PM is GLM (Z.ai) - it takes any task,
13
decomposes it, **delegates work to the other models** (Claude Max, Codex Max, local Ollama),
14
supervises, and gates every outward action behind an approval outbox. The UX is one unified surface:
15
**Chat channels** (pick a model when creating a channel, or `@model` mid-chat), a **Board** (tasks), and a
16
**Fleet** pane (worker status + quota gauges). It absorbs the channel/mention concepts of Mattermost and
17
the board concept of Plane.so **into one bespoke Mastra app** - no dependency on either.
18
 
19
The PM system is built by the model fleet it will eventually manage - Claude CT215 is the lead executor
20
of this spec.
21
 
22
## 2. Locked decisions (do NOT re-litigate)
23
 
24
| Decision | Value | Rationale |
25
|---|---|---|
26
| Brain runtime | **Mastra (TypeScript)** | User choice; agent/workflow primitives + Next.js pairing |
27
| Host | **CT120** (destroy + recreate clean Debian 12) | Was parked `perseus` 1c/1GB; repurposed |
28
| Surface | **Unified bespoke app** (no Mattermost/Plane fork) | "Fork into one" = one Mastra app absorbing their concepts |
29
| PM brain model | **GLM** via Z.ai Coding Plan **subscription** (weekly cap, NOT PAYG) - Anthropic-compat endpoint | Frees Claude Max for dev work; model id via `PM_BRAIN_MODEL` env (default = what the plan serves, e.g. `glm-4.6`/`glm-5.2`; confirm at build) |
30
| Model roster | GLM (Z.ai Coding Plan sub), Claude Max, Codex Max, local Ollama | **No PAYG per-token API ever.** Subscription-only: CLI/container-auth (Claude, Codex) + Z.ai Coding Plan sub. Adding a future subscription model = 1 adapter file (see §6) |
31
| Delegation primitive | **Spawn agent processes** (CLI/SSH/HTTP), not API calls | Auth lives inside each logged-in worker |
32
| Build horizon | **Today** | Parallel dispatch across the fleet |
33
 
34
## 3. Verified infra facts (build on these)
35
 
36
### CT120 - target host
37
- **Current:** `perseus`, PARKED 2026-06-16, stopped, onboot=0, 1c/1024MB, rootfs intact.
38
- **Action:** `pct destroy 120` then recreate clean from debian-12 template (see §9 step 1).
39
- **Recommended resources:** **4 cores / 6GB RAM / 2GB swap / 24GB disk, onboot=1.**
40
- **Recommended IP:** static **REDACTED-IP** (mnemonic, free).
41
- **Host:** pve node `root@REDACTED-IP` (Dell R410, Xeon E5645, 24c/31GB, 3.5TB free).
42
 
43
### Fleet endpoints (all LAN-reachable from CT120)
44
| Role | Host | Transport | Notes |
45
|---|---|---|---|
46
| GLM brain (this PM) | `https://api.z.ai/api/anthropic` | HTTPS, `ZAI_API_KEY` (from Z.ai Coding Plan **subscription**, weekly cap - NOT PAYG) | Anthropic-compat; model id via `PM_BRAIN_MODEL` env, default = plan's current (this CT247 session routes `zai/glm-5.2`; confirm `/api/anthropic` id at build) |
47
| Claude Max worker | `zion@REDACTED-IP` (CT215) | SSH + `claude -p` | Claude Code, `--dangerously-skip-permissions` ok; tmux pool optional |
48
| Codex Max worker | `zion@REDACTED-IP` (CT241) | SSH + `codex exec` | `codex-full`/`cdx` wrappers; `approval_policy=never`, full sandbox |
49
| Ollama worker (free tier) | `http://REDACTED-IP:11434` (CT205) | HTTP `/api/chat` | RTX 3060 12GB on pve-gpu node; models: `qwen2.5-coder:7b`, `deepseek-coder-v2`, `mistral-nemo:12b` |
50
| Ollama overflow | `http://REDACTED-IP:11434` (WIN3060) | HTTP | Second 3060; desktop may sleep - optional |
51
| homelab-rag MCP | `http://REDACTED-IP:9876/sse` | SSE | Conductor's `queryMemory` tool |
52
| cbm-homelab MCP | `http://REDACTED-IP:9877/sse` | SSE | Conductor's `queryGraph` tool |
53
| serena MCP | `http://REDACTED-IP:9878/sse` | SSE | Optional for MVP |
54
| memory-graph MCP | `http://REDACTED-IP:9879/sse` | SSE | Optional for MVP; durable cross-agent memory |
55
| Traefik edge | CT244 `REDACTED-IP` | dynamic YAML | `/opt/edge/traefik/dynamic/*.yml` |
56
| Authentik SSO | CT246 `REDACTED-HOST` | forwardAuth middleware | Reuse `authentik-forwardauth@docker` (same as vault.lab/notes.lab) |
57
 
58
### Operational doctrine (from homelab conventions - obey)
59
- **noexec CIFS rule:** the canonical repo lives at `/shared/projects/zion-pm/` (git, committed from CT215),
60
  but the **app runs from CT120 local fs** `/opt/zion-pm`. Deploy = `git clone` / `git pull` on CT120. Never
61
  execute from the share.
62
- **RAG-first:** the conductor must call `queryMemory` (homelab-rag MCP) before reading/grepping files.
63
- **Secret hygiene:** `ZAI_API_KEY` lives in `/shared/projects/.env.secrets` (has CRLF - strip when sourcing).
64
  Workers SSH via **keys**, never passwords in code. Treat `[REDACTED:...]` as proof a secret exists.
65
- **Approval outbox:** every outward action (email / deploy / publish / infra) **drafts** - never auto-fires.
66
  Internal/reversible work (PM↔worker delegation, file edits) is full-auto.
67
- **Git:** commits to `/shared/projects` happen from CT215 (per homelab rule).
68
 
69
## 4. Architecture
70
 
71
```
72
                 ┌─────────────── REDACTED-HOST (Traefik+Authentik) ──────────────┐
73
   Zion (PWA) ──▶│            Next.js UI - Chat / Board / Fleet panes (:4100)            │
74
                 └─────────────────────────────┬──────────────────────────────────────────┘
75
                                               │ websocket / REST
76
77
                                  ┌─────────── Mastra runtime ────────────┐
78
                                  │  pmConductor agent (GLM via Z.ai)      │
79
                                  │  Tools:                                │
80
                                  │   delegate(model, task, opts)          │
81
                                  │   createTask / updateTask              │
82
                                  │   draftApproval(action)                │
83
                                  │   queryMemory (→ homelab-rag MCP)      │
84
                                  │   queryGraph  (→ cbm-homelab MCP)      │
85
                                  └───┬──────────┬──────────┬──────────┬───┘
86
                                      ▼          ▼          ▼          ▼
87
                   ┌─────────────────────────────────────────────────────────┐
88
                   │  Worker adapters (TS)        cap table (sqlite)         │
89
                   │   claude   → SSH CT215  `claude -p`        ▓▓▓░ Max     │
90
                   │   codex    → SSH CT241  `codex exec`       ▓▓░░ Max     │
91
                    │   ollama   → HTTP CT205 /api/chat          ▓▓▓▓ free     │
92
                    └─────────────────────────────────────────────────────────┘
93
              (GLM conductor is the brain itself - NOT a worker adapter. Workers = claude/codex/ollama only.)
94
95
                                      ▼  (every worker may also mount the 4 MCPs)
96
                              shared memory substrate (rag + cbm + serena + mem-graph)
97
```
98
 
99
### Layers
100
1. **Mastra runtime** - `pmConductor` agent (GLM). Holds the system prompt, the tool registry, and the
101
   workflow for decompose→delegate→verify.
102
2. **Worker adapters** - one TS module per model behind a unified `delegate()` entry point. Each knows its
103
   transport. A **cap table** gates availability and provides tier fallback.
104
3. **Router** - message ingestion → resolve channel-default-model + parse `@mentions` → route to conductor
105
   (if glm) or direct-dispatch (otherwise) → stream reply back to UI.
106
4. **Web UI (Next.js)** - three panes: Chat, Board, Fleet. Mobile-first PWA.
107
5. **Store** - libSQL/sqlite at `/opt/zion-pm/data/zionpm.db`.
108
6. **Edge** - Traefik `pm.lab` route + Authentik forwardAuth.
109
 
110
## 5. Data model (sqlite)
111
 
112
```sql
113
CREATE TABLE channels(
114
  id            TEXT PRIMARY KEY,           -- ulid
115
  name          TEXT NOT NULL UNIQUE,
116
  default_model TEXT NOT NULL,              -- glm|claude|codex|ollama
117
  created_at    INTEGER NOT NULL
118
);
119
CREATE TABLE messages(
120
  id            TEXT PRIMARY KEY,
121
  channel_id    TEXT NOT NULL REFERENCES channels(id),
122
  role          TEXT NOT NULL,              -- user|assistant|system
123
  author_model  TEXT,                       -- which model produced an assistant msg
124
  body          TEXT NOT NULL,
125
  parent_id     TEXT REFERENCES messages(id), -- threading
126
  created_at    INTEGER NOT NULL
127
);
128
CREATE TABLE tasks(
129
  id            TEXT PRIMARY KEY,
130
  title         TEXT NOT NULL,
131
  status        TEXT NOT NULL DEFAULT 'todo', -- todo|doing|done|blocked
132
  assigned_model TEXT,
133
  channel_id    TEXT REFERENCES channels(id),
134
  created_at    INTEGER NOT NULL,
135
  updated_at    INTEGER NOT NULL
136
);
137
CREATE TABLE outbox(                          -- approval-gated outward actions
138
  id           TEXT PRIMARY KEY,
139
  action_type  TEXT NOT NULL,                 -- email|deploy|publish|infra
140
  payload_json TEXT NOT NULL,
141
  status       TEXT NOT NULL DEFAULT 'draft', -- draft|approved|rejected|sent
142
  created_at   INTEGER NOT NULL,
143
  approved_at  INTEGER
144
);
145
CREATE TABLE cap_state(
146
  model        TEXT PRIMARY KEY,
147
  available    INTEGER NOT NULL,              -- 0/1
148
  reason       TEXT,
149
  last_checked INTEGER NOT NULL
150
);
151
```
152
 
153
## 6. Worker adapter contracts (the delegation core)
154
 
155
### Types
156
```ts
157
export type ModelId = 'glm' | 'claude' | 'codex' | 'ollama';
158
export type OllamaModel = 'qwen2.5-coder:7b' | 'deepseek-coder-v2:16b' | 'mistral-nemo:12b';
159
 
160
export interface DelegateParams {
161
  model: ModelId;
162
  ollamaModel?: OllamaModel;   // required iff model === 'ollama'
163
  task: string;                // the work envelope (self-contained)
164
  context?: string;            // RAG/graph snippets to inject
165
  cwd?: string;                // working dir on the worker host (claude/codex)
166
  timeoutMs?: number;          // default 120_000
167
}
168
 
169
export interface DelegateResult {
170
  ok: boolean;
171
  output: string;              // stdout / model reply
172
  artifacts: string[];         // file paths produced (claude/codex)
173
  error?: string;
174
  durationMs: number;
175
}
176
```
177
 
178
### `delegate()` entry point (src/workers/delegate.ts)
179
1. Check `cap_state[model]`; if unavailable, **escalate tier**: claude→codex→ollama (strong→weak).
180
   The conductor may override.
181
2. Call the matching adapter.
182
3. Persist a `messages` row (author_model) + update `cap_state.last_checked`.
183
4. Return `DelegateResult`.
184
 
185
### Adapters
186
- **`claudeAdapter`** (src/workers/claude.ts): SSH `zion@REDACTED-IP`, run
187
  `claude -p '<task>' --dangerously-skip-permissions` in `cwd` (default `/shared/projects`). Capture stdout.
188
  Use `ssh2` (node) with key auth. MVP = one-shot; phase 2 = tmux pool `pm-claude-N`.
189
- **`codexAdapter`** (src/workers/codex.ts): SSH `zion@REDACTED-IP`, run `codex exec '<task>'`
190
  (or `cdx` wrapper) in `cwd`. Capture stdout.
191
- **`ollamaAdapter`** (src/workers/ollama.ts): `POST http://REDACTED-IP:11434/api/chat`
192
  `{model: ollamaModel, messages:[{role:'system',...},{role:'user',content:task}], stream:false}`.
193
  Parse `message.content`.
194
 
195
> **No `glm-sub` adapter** - the conductor (GLM) does all GLM reasoning itself. Worker tier = claude/codex/ollama only.
196
>
197
> **Adding a future subscription model** (e.g. a Gemini CLI sub, Cursor, etc.): create one new
198
> `src/workers/<model>.ts` implementing the `DelegateParams→DelegateResult` contract, add one row to
199
> `cap_state`, and one option in the channel model-picker + mention grammar. Same pattern as claude/codex.
200
 
201
### SSH key setup (WS-A, once)
202
On CT120 as the app user: `ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519`, then append the pubkey to
203
`/home/zion/.ssh/authorized_keys` on **CT215** and **CT241** (the user `zion`). Verify:
204
`ssh -o BatchMode=yes zion@REDACTED-IP 'echo ok'` returns `ok`.
205
 
206
## 7. Router (channel-default + @mention)
207
 
208
```ts
209
// src/router.ts - pseudocode
210
async function onMessage(channelId, userId, text) {
211
  const binding  = (await db.channels.get(channelId)).default_model;
212
  const mentions = parseMentions(text);                 // ['@claude', ...] → ['claude']
213
  const targets  = mentions.length ? mentions : [binding];
214
 
215
  const ragCtx = await conductor.tools.queryMemory(text); // always inject memory
216
 
217
  for (const target of targets) {
218
    let reply: string;
219
    if (target === 'glm') {
220
      // conductor owns it: may decompose + sub-delegate + verify
221
      reply = (await conductor.run(text, { channelId, context: ragCtx })).text;
222
    } else {
223
      reply = (await delegate({ model: target, task: text, context: ragCtx })).output;
224
    }
225
    await db.messages.insert({ channelId, role:'assistant', author_model: target,
226
                               body: reply, parentId, createdAt: Date.now() });
227
    io.to(channelId).emit('message', { ... });           // push to UI
228
  }
229
}
230
```
231
 
232
**Mention grammar:** `@(glm|claude|codex|ollama)(?::(\S+))?` - the optional `:model` suffix selects an
233
Ollama model, e.g. `@ollama:mistral-nemo:12b`. Mention wins over channel-default **for that turn only**.
234
 
235
## 8. Web UI (Next.js, three panes)
236
 
237
- **Chat** - left: channel list (each shows its default-model badge). Main: threaded message view with
238
  author-model color coding + @mention chips. Composer shows the active target model. **New Channel modal:
239
  name + model `<select>`** (the "select model in a new channel" UX).
240
- **Board** - kanban (todo / doing / done / blocked). Cards = tasks. Drag = status change. Click card →
241
  assign model, link to channel.
242
- **Fleet** - table: model | status | last task | cap gauge | `Ping` button (runs a trivial delegate and
243
  reports latency). Live-updates via websocket.
244
 
245
Stack: Next.js App Router, Tailwind, `@mastra/core`, socket.io (or server actions + polling for MVP).
246
Auth = Authentik header (`X-Forwarded-User`) trusted behind Traefik; map to a local user row.
247
 
248
## 9. Build steps (dependency order)
249
 
250
### WS-A - CT120 provision (operator, gates everything)
251
```bash
252
# on pve node root@REDACTED-IP
253
pct stop 120 2>/dev/null; pct destroy 120 --purge
254
# create clean debian-12 CT (adjust template/storage to what pve has)
255
pct create debian-12 120 \
256
  --hostname zion-pm --cores 4 --memory 6144 --swap 2048 \
257
  --rootfs local-lvm:24 --onboot 1 \
258
  --net0 name=eth0,bridge=vmbr0,ip=REDACTED-IP/24,gw=REDACTED-IP
259
pct start 120
260
pct enter 120
261
# inside CT120:
262
apt update && apt -y install nodejs npm git build-essential curl python3
263
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt -y install nodejs   # Node 20 LTS
264
npm i -g pnpm
265
# SSH keys for the app user (run as root or a dedicated 'zionpm' user):
266
ssh-keygen -t ed25519 -N "" -f /root/.ssh/id_ed25519
267
# distribute pubkey to CT215 + CT241 ~/.ssh/authorized_keys (zion user)
268
ssh -o BatchMode=yes zion@REDACTED-IP 'echo ok'   # must print ok
269
```
270
 
271
### WS-B - Mastra scaffold + conductor (Codex CT241 or Claude CT215)
272
```bash
273
# on CT120, in /opt
274
cd /opt && pnpm create mastra@latest zion-pm --template nextjs
275
cd zion-pm && pnpm install
276
pnpm add @mastra/mcp ssh2 better-sqlite3 ulid socket.io
277
```
278
- Wire GLM via Z.ai Anthropic-compat (set `ZAI_API_KEY` env, base URL `https://api.z.ai/api/anthropic`).
279
- Define `pmConductor` agent (`src/mastra/agents/pm.ts`) with the §4 tool registry.
280
- Register MCP clients for homelab-rag + cbm-homelab via `@mastra/mcp` (SSE URLs in §3).
281
 
282
### WS-C - Worker adapters + cap table (Claude CT215)
283
Implement §6 exactly: `src/workers/{delegate,claude,codex,ollama}.ts` (3 adapters + delegate entry) + `src/db.ts`
284
(better-sqlite3, schema in §5). Seed `cap_state` with `claude`,`codex`,`ollama` all `available=1` (glm = conductor, not a worker row).
285
 
286
### WS-D - Next.js UI (Claude CT215)
287
Implement §8: Chat / Board / Fleet panes, new-channel model picker, @mention rendering. Seed channels:
288
`#pm`(glm), `#claude`, `#codex`, `#ollama`.
289
 
290
### WS-E - Router (GLM CT247 / this session can also do it)
291
Implement §7: message ingestion, channel-default + mention parse, dispatch loop, websocket push.
292
 
293
### WS-F - Edge + SSO (Codex CT241)
294
On CT244 add `/opt/edge/traefik/dynamic/pm.yml`:
295
```yaml
296
http:
297
  routers:
298
    pm:
299
      rule: Host(`REDACTED-HOST`)
300
      entrypoints: [websecure]
301
      tls: { certResolver: letsencrypt }
302
      service: pm
303
      middlewares: [authentik-forwardauth@docker]
304
  services:
305
    pm:
306
      loadBalancer:
307
        servers: [{ url: http://REDACTED-IP:4100 }]
308
```
309
Add `REDACTED-HOST` to AdGuard/local DNS → CT244. Reload Traefik. Confirm Authentik forwardAuth
310
applies (mirror `vault.lab`).
311
 
312
### WS-G - Integration + e2e test (GLM, this session)
313
Run the §10 acceptance test. Document at `/shared/projects/zion-pm/` (README, ops runbook, this SEED).
314
Git-commit from CT215.
315
 
316
## 10. Acceptance test - "done today" bar
317
 
318
All of these must pass end-to-end on `https://REDACTED-HOST`:
319
 
320
1. **Boot:** CT120 clean, app serves :4100, `pm.lab` resolves behind Authentik login.
321
2. **Channels seeded:** `#pm`(glm), `#claude`, `#codex`, `#ollama` visible with correct model badges.
322
3. **GLM conductor (channel-default):** Post in `#pm`: *"Summarize the cbm-homelab project's architecture
323
   in 5 bullets."* → conductor calls `queryMemory`/`queryGraph` → posts a 5-bullet reply authored by `glm`.
324
4. **@mention override:** In `#pm`: *"@ollama:mistral-nemo:12b suggest 3 names for this PM app"* → routes
325
   to Ollama CT205 → inline reply authored by `ollama`, despite channel default being glm.
326
5. **Heavy delegation:** In `#pm`: *"@claude create a README.md at /shared/projects/zion-pm/ describing this
327
   app"* → claude adapter SSHes CT215, runs `claude -p`, returns → reply posts with the file path; file exists.
328
6. **Board:** a task created from a conversation appears on the Board, assignable, drag todo→done persists.
329
7. **Fleet:** all three workers (claude/codex/ollama) + the GLM conductor show green; `Ping` returns latency for each.
330
8. **Approval outbox:** conductor drafts a (stub) outward action → appears as `draft` in outbox → UI shows
331
   Approve/Reject → approving marks `approved`; nothing auto-sends.
332
 
333
## 11. Operating rules (non-negotiable)
334
 
335
1. **noexec CIFS** - app runs from `/opt/zion-pm` on CT120, never from `/shared/projects`.
336
2. **RAG-first** - conductor queries homelab-rag MCP before any file read.
337
3. **Secrets** - `ZAI_API_KEY` from `.env.secrets` (strip CRLF); SSH keys only; never print secrets.
338
4. **Approval outbox** - outward actions draft only; internal delegation is full-auto.
339
5. **Git** - canonical repo at `/shared/projects/zion-pm`, remote = **private `github.com/zionboggan/zion-pm`**; commit + push from CT215; CT120 deploys via `git pull`.
340
6. **Cap-aware** - never silently drop a task; fall through tiers or queue with a visible Board card.
341
 
342
## 12. Out of scope (phase 2+)
343
 
344
- Voice/email intake (`intake-voice.sh`, `intake-email.py` from the old sketch).
345
- Multi-account email sender identities (`policy.yaml > sender_identities`).
346
- Cockpit PWA skin over this app's API.
347
- Tmux worker pools (MVP is ephemeral one-shot `claude -p`/`codex exec`; promote to persistent pooled sessions later).
348
- Cap auto-detection from Claude/Codex/Z.ai usage endpoints.
349
- serena + memory-graph MCP wired into workers (MVP uses rag + cbm only).
350
 
351
---
352
 
353
**Handoff line for Claude CT215:**
354
> Read `/root/zion-pm/SEED.md` on CT247 (`ssh root@REDACTED-IP 'cat /root/zion-pm/SEED.md'`),
355
> copy it to `/shared/projects/zion-pm/SEED.md`, then execute WS-A through WS-G per §9. Lead = you
356
> (Claude CT215). Delegate WS-B/F to Codex CT241, WS-E/G stays with GLM. Target: §10 acceptance today.
357
> Obey §11. (Decisions baked in: 3 adapters - glm-sub dropped; GLM brain = Z.ai Coding Plan sub not PAYG;
358
> repo = private github.com/zionboggan/zion-pm; workers = ephemeral one-shot.)
359
 
360
> **Note on file location:** CT247's local `/shared/projects` mount is read-only (`nobody:nogroup`) and
361
> CT247→CT215 SSH is not peered, so the spec could not be written directly to the canonical share from CT247.
362
> Claude CT215 fetches it via the SSH command above (CT215→CT247 needs to be peered, or Zion copy-pastes
363
> the content). After fetch, commit it to `/shared/projects/zion-pm/SEED.md` as the first WS-G step.