picortex — Initial Roadmap
Date: 2026-04-23 Status: Draft — Codex review in hand, revisions pending (see 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 --printspike. —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/inboundwith HMAC verification- Outbound Linq client (pointed at linq-sim by default)
/health,/api/frontend-logstubs- 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
replyToMessageIdfield tobuildInboundPayload()insrc/server.js - Mirror on outbound-capture
sendMessagehandler - Add
replyTocomposer input + "Reply" button inui.htmlandas-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 tailpipe-paneoutput) - Reply parsing (terminal → clean text)
- Attention gating mode
alwaysonly 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 0700on 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_configtable @mentiondetection for iMessage group markers- Mode
mentions-only,discriminate,discriminate-quiet,silent .picortex/prompts/discriminator.mddefault 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>insiderunuser -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.
BridgeEventtable- "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.shone-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
reactionstable + REST + WS events (closesOpenChat-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.mdentry 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 |