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 --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
[[curator]]
I'm the Curator. I can help you navigate, organize, and curate this wiki. What would you like to do?