new file mode 100644
index 0000000..7e5a898
@@ -0,0 +1,222 @@
+---
+visibility: public
+---
+
+# picortex — Initial Roadmap
+
+**Date:** 2026-04-23
+**Status:** Draft — Codex review in hand, revisions pending (see [codex-2026-04-23.md](../reviews/codex-2026-04-23.md))
+
+> **Open follow-ups from Codex review (2026-04-23):**
+> - S3 may be an anti-pattern (shared tmux before per-chat isolation). Candidate: merge S3 into S4 and prefix with a `claude --print` spike. — `picortex-b92`
+> - Add S1.5: minimal SQLite schema, event normalization, replay/idempotency storage, outbound delivery records. — `picortex-ul4`
+> - Pick Linux-only or reduce v0.1 feature set for Mac Mini. — `picortex-mn3`
+> - Replace broad sudoers with narrow wrapper binary. — `picortex-5sc`
+> - Reply-capture proof stage — run real Claude CLI in tmux, verify sentinels in practice before committing the design. — `picortex-3vj`
+> - Move queueing/concurrency/retry rules into core plan. — `picortex-nk2`
+> - Egress-deny story must be a gate, not polish. — `picortex-xqx`
+> - Drop warm pool unless measurement proves need. — `picortex-j2p`
+
+A phased plan from "no code" to "Jacob uses it daily from his phone." Each stage ends in a demo that proves a property. No stage is allowed to merge without: (a) beads ticket closed, (b) tests green, (c) a short screen recording against linq-sim committed to `docs/demos/`.
+
+---
+
+## Phase A — Testing harness & fundamentals
+
+### S0 · Planning & docs (done, 2026-04-23)
+
+- [x] PRD, ADRs 0001-0005, Specs 001-008, Wiki, LLM wiki, llms.txt, AGENTS.md
+- [x] Beads initialized (`bd init picortex`)
+- [x] Codex second opinion requested and filed at `docs/reviews/codex-2026-04-23.md`
+
+### S1 · Minimal backend skeleton (1-2 days)
+
+**Goal:** `npm run dev` starts a Fastify server on :7823 that accepts signed linq-sim webhooks and echoes them back as outbound sendMessage calls. No Claude Code yet.
+
+- Fastify app, strict TS, pino logger, `X-Request-ID`
+- `POST /api/linq/inbound` with HMAC verification
+- Outbound Linq client (pointed at linq-sim by default)
+- `/health`, `/api/frontend-log` stubs
+- Vitest config, first smoke test
+- `picortex-S1-*` beads tickets
+
+**Demo:** Send a message in linq-sim's `/` admin UI → picortex logs it with request ID and replies with "echo: <text>".
+
+### S2 · linq-sim: thread/reply support (2-3 hours, lives in `~/code/cortex`)
+
+**Goal:** linq-sim supports `data.reply_to_message_id` on `message.received` and outbound `sendMessage`.
+
+- Add `replyToMessageId` field to `buildInboundPayload()` in `src/server.js`
+- Mirror on outbound-capture `sendMessage` handler
+- Add `replyTo` composer input + "Reply" button in `ui.html` and `as-user.html`
+- Optional: validate parent exists in ring buffer
+- Contribute back to Cortex via PR
+
+**Demo:** linq-sim UI supports composing a reply; picortex echoes back preserving the reply pointer.
+
+### S3 · Single-user tmux + Claude Code spawn (2-3 days)
+
+**Goal:** Backend on receive creates / reuses a single global tmux session `picortex:default`, sends the user's text to it, captures Claude Code's reply, sends back via Linq. Not isolated yet — one session for all chats.
+
+- `node-pty` + tmux wrapping
+- Input routing (`tmux send-keys`, then tail `pipe-pane` output)
+- Reply parsing (terminal → clean text)
+- Attention gating mode `always` only
+- `picortex-S3-*` tickets
+
+**Demo:** Text picortex from linq-sim, get a real Claude Code response grounded in files under `~/picortex-default/`.
+
+---
+
+## Phase B — Per-chat isolation & attention
+
+### S4 · Linux-user provisioning per chat (3-4 days)
+
+**Goal:** Per-chat isolation working end-to-end.
+
+- `useradd --create-home --home-dir $CHAT_WORKSPACE_ROOT/<chat_id> --shell /bin/bash chat-<hex>`
+- sudoers drop-in granting the picortex service user `runuser -u chat-<hex>`
+- `chmod 0700` on each home dir
+- SQLite schema: `chats`, `chat_users`, `messages`, `events`, `bridge_events`, `chat_config`
+- Tmux-per-chat naming; on-demand creation
+- Warm-pool and idle-reap background job (stubbed — no actual reap until S6)
+
+**Demo:** Two linq-sim "users" send parallel conversations; `ls -la /srv/picortex/chats/` shows two distinct, 0700-locked dirs; cross-chat file access denied.
+
+### S5 · Attention gating (2-3 days)
+
+**Goal:** Inbound messages to groups are filtered. LLM discriminator runs on `discriminate` mode.
+
+- Per-chat config in `chat_config` table
+- `@mention` detection for iMessage group markers
+- Mode `mentions-only`, `discriminate`, `discriminate-quiet`, `silent`
+- `.picortex/prompts/discriminator.md` default template, committed to the chat's home dir as a tiny git repo
+- Admin command: `/picortex attention <mode>` (message body parser)
+
+**Demo:** Same linq-sim group chat; off-topic messages don't trigger a response in `discriminate` mode; on-topic ones do.
+
+### S6 · Lifecycle & warm pool (2 days)
+
+**Goal:** Sessions hibernate cleanly.
+
+- Idle-detector cron: kill tmux after 7 days inactive
+- Home-dir archive after 30 days
+- On inbound to archived chat: restore, cold-start path
+- Warm pool: keep N-1 idle tmux sessions pre-spawned for latency
+
+**Demo:** Force an idle kill; resend from linq-sim; see cold-start path fire within NFR-1 budget.
+
+---
+
+## Phase C — Mobile-first web UI
+
+### S7 · Web UI: messages + file browser (3-4 days)
+
+**Goal:** Mobile Safari experience. A user authenticated with Noos OAuth can see all their chats, pick one, and view messages + files in a split / swipe layout.
+
+- Vite + React + Tailwind app on :7824
+- Noos OAuth flow (follows voice-assistant pattern)
+- `/chats`, `/chats/:id`, `/chats/:id/files/*` routes
+- Mobile-first layout: `[Messages | Files | Terminal-stub]` swipe panels
+- Viewport meta, 44pt tap targets, keyboard focus rings
+- Version display in footer, update-available dot badge
+- Reply-to rendering (FR-20)
+
+**Demo:** Load picortex on phone; see most recent linq-sim conversation; swipe to see file tree; tap a file, view contents.
+
+### S8 · Web terminal (2-3 days)
+
+**Goal:** Attach to any chat's tmux session from the browser.
+
+- xterm.js client
+- WebSocket PTY bridge: `/ws/terminal/:chat_id`
+- Backend spawns `tmux attach -t picortex:<chat_id>` inside `runuser -u chat-<hex>`
+- Read-only vs read-write modes (v1: read-write for Jacob; restrict later)
+- Resize propagation
+
+**Demo:** Open chat on phone; swipe to terminal; see Claude Code's live output; type `ls` and see it run as the chat user.
+
+### S9 · Sharing bridge with audit (2-3 days)
+
+**Goal:** Cross-chat file import with out-of-band challenge.
+
+- `BridgeEvent` table
+- "Import from personal workspace" command parser
+- DM challenge loop (reuses Linq 1:1 path)
+- UI: bridge log viewer in chat settings
+
+**Demo:** From a linq-sim group chat, ask bot to import a file from DM workspace; challenge DM appears; reply "yes"; file copied; BridgeEvent row visible in UI.
+
+---
+
+## Phase D — Production hardening & ecosystem
+
+### D1 · Deployment target & runbook (1 day)
+
+- Decide: Hetzner sibling of jcortex, Fly.io, or HMA (Q3)
+- Write `docs/runbooks/deploy.md`
+- Systemd unit, Caddy reverse proxy
+- `deploy.sh` one-liner
+- Closes Q3
+
+### D2 · Security-isolation report (research ticket `picortex-sec-1`)
+
+- Compare Docker vs Linux-user vs firejail/bubblewrap vs nsjail vs Landlock for Claude-Code-per-chat
+- Reference existing Jacob research: `~/memory/research/openclaw-group-chat-security.md`, `openclaw-security-audit-2026-02-20.md`, `openclaw-voice-call-tool-execution.md`
+- Decide if ADR-0002 holds or if we graduate to bubblewrap/Landlock
+- Deliverable: `docs/wiki/isolation-models.md` + revised ADR if needed
+
+### D3 · OpenChat linq-adapter (optional; may live in `~/code/openchat` instead)
+
+- Add `reactions` table + REST + WS events (closes `OpenChat-yg8`-adjacent work)
+- Add outbound `/api/partner/v3/*` adapter emitting HMAC-signed webhooks matching Linq's 14 event types
+- picortex can now use OpenChat as an alternate channel without Linq
+- Estimated 1-2 weeks
+- Decision gate: is the effort worth it before picortex v0.1? Probably no — defer to v0.2.
+
+### D4 · Cut v0.1.0
+
+- Tag, push, fill in `CHANGELOG.md`
+- Update root `PROJECTS.md` entry to **ACTIVE**
+- Announce to self in daily note
+
+---
+
+## Explicit deferrals (v0.2+)
+
+- Cross-chat MCP (Cortex R6)
+- Noos knowledge-graph federation
+- Group-chat mobile-first UI
+- Native iOS / Android
+- OpenChat adapter productionization (D3)
+- Multi-user / shared access
+
+---
+
+## Dependency graph
+
+```
+S0 ─▶ S1 ─▶ S2 ─▶ S3 ─▶ S4 ─▶ S5 ─▶ S6 ─▶ S7 ─▶ S8 ─▶ S9 ─▶ D1 ─▶ D4 (v0.1.0)
+ │
+ D2 (runs parallel to S4+) ─┘
+ D3 (deferred)
+```
+
+## Estimated total effort
+
+- Planning (S0): done in this session
+- Phase A (S1-S3): ~1 week
+- Phase B (S4-S6): ~1.5 weeks
+- Phase C (S7-S9): ~1.5 weeks
+- Phase D (D1-D2, D4): ~3 days
+- **Total to v0.1.0: ~4 weeks of focused work**, 6-8 weeks elapsed at 50% focus.
+
+## Risks
+
+| Risk | Mitigation |
+|---|---|
+| Linux-user isolation insufficient for adversarial workloads | D2 report; if fails, add bubblewrap/Landlock inside `runuser` |
+| Claude Code CLI doesn't behave well under long-lived tmux | Fall back to short-lived `claude --print` invocations per turn |
+| Linq API changes / not yet live for Jacob | linq-sim covers dev; real Linq onboarding can wait until S7+ |
+| tmux attach streaming performance via WebSocket | Precedent exists (Cortex cloudcli, Claude Code UI) |
+| Scope creep toward rebuilding Cortex | This plan is capped at v0.1; additional features must re-scope |
\ No newline at end of file