| 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. |