PRD — picortex v1
Status: Draft — provisional pending Q0-Q4 closure Owner: Jacob Date: 2026-04-23 Last reviewed: 2026-04-23
⚠️ This PRD describes Option 1 (tmux-centered single-host with web terminal + file browser) from the prototype options brainstorm. Late in the planning session, the product was reframed as "awesome texting experience" with dev surface optional — that may demote several functional requirements here (especially FR-9..FR-12, FR-18..FR-20) to "one branch of the decision tree." Treat this PRD as the maximal version until Q0 (
picortex-adb) defines measurable success criteria and Q2 (picortex-2b4) decides whether the dev surface is in v0.1. Reconciliation tracked inpicortex-357.
1. Problem
Jacob wants to chat with Claude Code from his phone — via real iMessage, including group texts — the same way Cortex enables, but with a lighter-weight personal stack. The existing Cortex deployment is a shared business product carrying design constraints (Docker-container-per-workspace, session dashboard, multi-tenant billing) that aren't needed for a single user. Meanwhile, Cortex has just spent a month solving the actual hard problems (attention gating, per-chat isolation, sharing bridge, HMAC webhook ingestion, linq-sim harness) and all of those are directly reusable.
There is no tool today that gives Jacob:
- A persistent Claude Code tmux session per iMessage conversation,
- Per-chat Unix-user isolation so group-chat compromise doesn't leak across,
- A mobile-first web UI to see the file tree and attach a live terminal mid-conversation,
- The lightest-weight spin-up possible so provisioning a new chat costs cents not dollars.
2. Users & personas
v1 has one user: Jacob. Everything else is future.
Scenarios:
- Jacob in an Uber, wants to check whether the Cortex deploy succeeded. Opens iMessage, texts the bot. Bot replies with status. No computer needed.
- Jacob in a group text with two collaborators, asks the bot to summarize the thread and file a ticket. Bot responds inside the group thread (reply, not new message).
- Jacob at desk, wants to see what Claude Code is doing in his chat with "research-bot". Opens web UI on laptop, clicks that chat, sees split view: messages on left, file tree + file viewer in middle, live terminal attached to the chat's tmux session on the right.
- Jacob wants to import a file from his personal chat into a group chat. Group agent proposes the import; picortex DMs Jacob a challenge; Jacob replies "yes" from the DM; the file is copied with a
BridgeEventaudit row.
3. Goals
G1. Text the bot from iMessage and get a useful Claude Code response within 10 seconds (warm chat) / 60 seconds (cold provision). G2. Every chat has its own Unix user + home dir; compromising a group chat's workspace cannot read Jacob's personal workspace. G3. Mobile Safari experience: a user on iPhone viewing any chat sees a usable, touch-first UI that does not require pinch-zoom. G4. Web terminal attaches to any chat's tmux session in < 2 s and displays the live Claude Code output. G5. linq-sim E2E suite covers at minimum: send/receive 1:1, send/receive group, reaction add/remove, reply-in-thread, attention-gating switch. G6. Operating cost of an idle chat is effectively zero (tmux session + a small home dir); an active chat's cost is dominated by the Anthropic API.
4. Non-goals
- NG1. No Docker / docker-compose / Kubernetes. ADR-0002
- NG2. No Cortex-style session-management dashboard. A web terminal attach to the current chat is the entire session UI. [Constraint from user]
- NG3. No billing, multi-tenant, or team accounts.
- NG4. No native mobile apps. Mobile-first web UI only.
- NG5. No group-chat-specific mobile UI in v1 — 1:1 threading works; groups render on desktop first.
- NG6. No MCP tools for cross-chat queries in v1 (Cortex R6 deferred; sharing bridge R7 is enough).
- NG7.
No OpenClaw. No OpenClaw. No OpenClaw.Softened 2026-04-23: OpenClaw was initially ruled out, butdocs/plans/2026-04-23-prototype-options.mdOption 5 reopened it on merit. Current status: not chosen, but not ruled out. Final decision is Q4 (picortex-3mk). - NG8. No shared knowledge graph backend yet. Canonical log is SQLite on the picortex server. Integration with noos deferred.
5. User stories
- US-1. As Jacob, I text my iMessage-only bot "What's the status of the ListHub deploy?" and get a coherent answer that used
run_commandon the server. - US-2. As Jacob in a group text with two friends, I @mention the bot ("@picortex summarize this"). The bot replies in the thread (not a new message) with a 3-bullet summary.
- US-3. As Jacob, I change a group's attention mode to "discriminate" and the bot stops responding to off-topic messages but still speaks up when the conversation is about something it can help with.
- US-4. As Jacob on mobile Safari, I tap a chat and see messages, swipe-right gets file browser, swipe-further-right gets live terminal.
- US-5. As Jacob, I ask in a group chat "bring in the
refs/2026-plan.mdfrom my DM workspace." The bot DMs me a challenge; I reply "yes"; the file appears in the group chat's workspace and aBridgeEventis logged. - US-6. As Jacob deploying picortex, I run
./deploy.shand the latest version appears in the UI's footer within seconds, with an update-available badge cleared.
6. Functional requirements
Numbered for citation. Every one is pass/fail testable.
Linq integration
- FR-1. Bot receives inbound iMessage via
POST /api/linq/inbound, signature verified withHMAC-SHA256("{timestamp}.{raw_body}", LINQ_WEBHOOK_SECRET). Reject unsigned / skew > 5 min / replay. - FR-2. Bot sends replies via
POST $LINQ_BASE_URL/api/partner/v3/sendMessage. - FR-3. Bot supports all event types the linq-sim publishes:
message.{received, delivered, read, edited, failed},reaction.{added, removed},chat.typing_indicator.{started, stopped},chat.{created, updated, group_name_updated},participant.{added, removed}. - FR-4. Bot supports in-thread replies (
data.reply_to_message_id). Requires linq-sim to be upgraded — see Stage S2.
Workspace isolation
- FR-5. Every durable chat ID maps to exactly one Unix user (
chat-<8-hex-of-sha256>) and one home dir ($CHAT_WORKSPACE_ROOT/<chat_id>). - FR-6. Home dirs are
chmod 0700, owned by the chat user. No other chat user can read them. - FR-7. The picortex backend runs as a privileged service user; shells into each chat's user via
sudo -u chat-<hex> -H …(no shared filesystem write path). - FR-8. Backend SQLite is the canonical message log. The workspace FS is a cache only and is never used for authorization.
Sessions
- FR-9. Each chat has a tmux session named
picortex:<chat_id>. If missing, backend creates it on first inbound message. - FR-10. The tmux session runs
claude --dangerously-skip-permissions(or equivalent) with cwd = chat home dir. Flag choice TBD in S3. - FR-11. Idle tmux sessions are killed after 7 days of zero activity; home dir preserved for 30 days, then archived.
- FR-12. Web terminal (xterm.js) attaches to a chat's tmux session via
tmux attach -t picortex:<chat_id>through a backend WebSocket PTY bridge.
Attention gating
- FR-13. Each chat has an attention mode:
always | mentions-only | discriminate | discriminate-quiet | silent. Default for 1:1:always; default for groups:mentions-only. - FR-14. Mode
discriminateruns an LLM over the message with a git-versioned prompt at$CHAT_HOME/.picortex/prompts/discriminator.mdand only proceeds if the model scores ≥ threshold. - FR-15. Mode changes are visible in the mobile UI's chat-settings panel.
Sharing bridge
- FR-16. When chat A asks to import a file from chat B, backend DMs the user-intersection (same Jacob here) a challenge; only a reply from that DM approves the import.
- FR-17. Every bridge op logs a
BridgeEventrow (source chat, dest chat, file path, sha256, approver user, timestamp).
Mobile-first web UI
- FR-18. Mobile Safari: messages pane is usable without pinch-zoom; tap-targets ≥ 44pt;
<meta viewport>correct. - FR-19. Tabbed / swipe-panel layout on mobile:
[Messages | Files | Terminal]. - FR-20. In-thread replies render as a "replying to …" pill above the message.
- FR-21. Footer shows
picortex vX.Y.Zfrompackage.json. WhenHEADonmainis newer than running commit, a dot badge appears next to the version.
Observability
- FR-22. All server logs are structured JSON via
pino. - FR-23. Every HTTP request has an
X-Request-IDheader, echoed to the response. - FR-24.
POST /api/frontend-logaccepts browser error reports and writes them to server logs with the originatingX-Request-ID. - FR-25.
/healthreturns 200 +{version, uptime, db_ok}.
7. Non-functional requirements
- NFR-1. Cold-start new chat: provision Unix user, home dir, tmux session, first Claude turn — < 60 s.
- NFR-2. Warm response (existing tmux, cached Anthropic session): < 10 s first token.
- NFR-3. Idle cost: < 50 MB RAM per paused chat (tmux + bash, no Claude process).
- NFR-4. Server handles ≥ 50 simultaneous chats on a 4 GB VPS.
- NFR-5. Accessibility: WCAG AA contrast, keyboard nav for desktop web UI.
- NFR-6. Secrets never written to disk in plaintext outside
.env.
8. Open questions
| # | Question | Owner | Status |
|---|---|---|---|
| Q1 | What Claude Code command-line flag set do we run tmux sessions with? --dangerously-skip-permissions inside the sandboxed Unix user? |
Jacob | Open — revisit in S3 |
| Q2 | Do we need a backend read model (search) for message history or is grep-over-SQLite enough? | Jacob | Open — revisit post-S5 |
| Q3 | What's the deployment target: Hetzner (jcortex sibling), Fly.io, or Mac Mini (HMA)? | Jacob | Open — revisit in S7 |
| Q4 | OpenChat: upgrade to linq-adapter as a separate project (OpenChat-yg8 continues), or roll into picortex repo? | Jacob | Open — see wiki/openchat-adapter.md |
| Q5 | Does picortex eventually federate with noos (knowledge graph backend)? | Jacob | Open — see wiki/relationship-to-noos.md |
| Q6 | What's the real project name? picortex is provisional. |
Jacob | Open — rename before v0.1.0 |
9. Success metrics
- M1. Jacob uses picortex for ≥ 1 productive conversation per day for 2 consecutive weeks.
- M2. Zero isolation breaches over the first 3 months (measured by: no chat user accessing another chat's home dir).
- M3. Median cold-start time < 60 s (p95 < 120 s).
- M4. Median warm-reply time < 10 s (p95 < 30 s).
- M5. Uptime ≥ 99 % over a 30-day window.
10. Out of scope (reiterated)
Docker, Kubernetes, dashboard, billing, multi-tenant, native apps, group-chat mobile UI, cross-chat MCP, knowledge-graph integration, native iOS push.