Update docs/plans/2026-04-23-prototype-options.md @ ca36b1b108f9
a13b3a142091 wikihub 2026-04-23 36 files
a13b3a142091e40168bf4ebaf7ffb1bbb2fa6dc6
new file mode 100644
index 0000000..8cb257c
@@ -0,0 +1,135 @@
+---
+visibility: public
+---
+
+# AGENTS.md — picortex
+
+Operational instructions for coding agents (Codex CLI, Claude Code, Cursor, Amp, etc.). Prepended to every conversation — keep this file lean. For any detail beyond the essentials, link out and let the agent fetch on demand.
+
+## Project overview
+
+**picortex** is a personal variant of [Cortex](https://cortex.ideaflow.app) for iMessage + group texting through the [Linq](https://linq.so) partner API. It spawns Claude Code sessions per chat, isolated by Linux user + filesystem permissions (not Docker), with a mobile-first web UI for monitoring, file browsing, and live terminal attach.
+
+*Name is provisional.*
+
+## Status
+
+**Planning phase (2026-04-23, post-pivot).** The project has PRD, plan, ADRs, specs, and an LLM wiki — but **no code yet**.
+
+⚠️ **Don't start implementation at S1.** The initial roadmap (S1-S9) was built around Option 1 (tmux-centered single-host). Late in the planning session, Jacob reframed the product as "awesome texting experience" and reopened the architecture choice — see [docs/plans/2026-04-23-prototype-options.md](docs/plans/2026-04-23-prototype-options.md). The actual next action is **Q0** (`picortex-adb`) — define the success criteria that constrain the architecture choice. Then Q1-Q4 choose the option bundle, then `picortex-357` reconciles the docs with the chosen option, then a revised roadmap replaces S1-S9.
+
+## Primary doc pointers (progressive disclosure)
+
+Don't inline content from these — link and let the reader fetch:
+
+- **PRD:** [docs/prd/001-picortex-v1.md](docs/prd/001-picortex-v1.md)
+- **Roadmap:** [docs/plans/2026-04-23-initial-roadmap.md](docs/plans/2026-04-23-initial-roadmap.md)
+- **Architecture:** [docs/wiki/architecture.md](docs/wiki/architecture.md)
+- **Cortex inheritance map:** [docs/wiki/cortex-inheritance.md](docs/wiki/cortex-inheritance.md)
+- **ADRs:** [docs/adrs/](docs/adrs/)
+- **Specs:** [docs/specs/](docs/specs/)
+- **LLM wiki (Karpathy-style):** [docs/wiki/index.md](docs/wiki/index.md)
+
+## Dev environment
+
+- Node 20+, tmux 3.3+, ripgrep, sudo, SQLite 3.40+
+- `npm install`
+- `cp .env.example .env` then fill in `LINQ_API_KEY`, `LINQ_WEBHOOK_SECRET`, `ANTHROPIC_API_KEY` (or rely on `claude` CLI auth)
+- `npm run dev` — backend (7823) + frontend (7824) + linq-sim orchestrator
+
+## Stack
+
+- **Backend:** Node.js + Fastify (TypeScript, strict mode)
+- **Frontend:** Vite + React + Tailwind (mobile-first)
+- **Terminal:** xterm.js (client), `node-pty` + `tmux` (server)
+- **Storage:** SQLite (canonical message log, per-chat config); per-chat filesystem workspaces at `/srv/picortex/chats/<chat_id>/`
+- **Auth (web UI):** Noos OAuth SSO; the iMessage/Linq path does not require UI auth
+- **Observability:** `pino` JSON logs, `X-Request-ID` middleware, `/api/frontend-log` browser error forwarder
+
+## Ports
+
+- Dev backend: **7823**
+- Dev frontend: **7824**
+- linq-sim (from Cortex): **8447**
+- Never use 3000/5000/8080/8000. See Jacob's `~/.claude/rules/dev-patterns.md`.
+
+## Code style
+
+- TypeScript strict. No `any` without a comment justifying it.
+- Conventional commits (`feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`).
+- One concern per PR. Reference beads ticket: `[picortex-xxx]`.
+- Line length: soft 100, hard 120.
+
+## Testing
+
+- **vitest** for unit + integration
+- E2E runs against **linq-sim**, not real Linq
+- `npm test` — unit
+- `npm run test:e2e` — E2E (starts a linq-sim instance)
+- A feature isn't done until: (a) tests pass, (b) a linq-sim scene demo exists, (c) beads ticket closed.
+
+## Security invariants
+
+1. **Canonical message log lives on backend** (SQLite). Per-chat workspace filesystem is a **cache**, never authoritative. ([Cortex R5.1](docs/wiki/cortex-inheritance.md#r5-backend-authority))
+2. **Every cross-chat op** (e.g. "import X from my personal chat into this group") requires an out-of-band challenge/response: the group agent DMs the user, the user's reply is the approval. ([Cortex R5.4-5.5](docs/wiki/cortex-inheritance.md#r5-backend-authority))
+3. **Linq inbound webhooks** verified with `HMAC-SHA256("{timestamp}.{raw_body}", LINQ_WEBHOOK_SECRET)`. Reject on bad signature, skew > 5 min, or replay.
+4. **No secrets in workspace FS.** Env vars stay in the backend process. Per-chat Unix users have no ambient credentials.
+5. **Per-chat Unix user can't see other chats' files** — enforced by POSIX permissions (owner-only home dirs, 0700). See [ADR-0002](docs/adrs/0002-linux-users-over-docker.md).
+6. **Never disable isolation for convenience.** If provisioning breaks, stop and ask; do not fall through to "run as shared user."
+
+## Commit / PR conventions
+
+- Branch: `<type>/<short-desc>` e.g. `feat/attention-discriminator`
+- PR body: **Summary**, **Test plan**, beads link
+- Squash merge preferred
+
+## Deployment
+
+TBD — candidates: Hetzner VPS, Fly.io, HMA (Mac Mini). See [docs/runbooks/deploy.md](docs/runbooks/deploy.md) once chosen.
+
+## Dev patterns (per Jacob's global rules)
+
+- **Version on screen:** display app version in user-menu footer (from `package.json`)
+- **Update-available indicator:** non-intrusive badge when a newer commit is on `main`
+- **Frontend error logging:** `POST /api/frontend-log` endpoint that forwards browser errors to the server's structured log
+- **Port conflict handling:** auto-increment if 7823/7824 are taken, display actual port
+- **System deps:** proactively detect `tmux`, `rg`, `sudo`, `sqlite3`; print `brew install …` hint if missing
+
+## Beads
+
+- Prefix: `picortex-`
+- Init: `bd init picortex` (done at project setup)
+- List open: `bd list --status=open`
+- Create: `bd create "Title" --type task|feature|bug|epic --priority 0-4`
+
+## Cortex inheritance rule
+
+Where Cortex already solved it, **inherit, don't re-derive**. See [docs/wiki/cortex-inheritance.md](docs/wiki/cortex-inheritance.md) for the requirement-by-requirement map.
+
+**Cortex research cutoff (revised 2026-04-23):** Don't *inherit* patterns from pre-`af3a76f5` Cortex (the Piyush-era EC2/SSH/Vercel design, 2026-01-20 → 2026-01-23), but *do* study it deliberately — several of its patterns (bot/workspace physical split, `claude -c -p` per turn) are exactly what the texting-first picortex wants. See `docs/wiki/piyush-era-design.md` and `docs/plans/2026-04-23-prototype-options.md` (Option 2).
+
+## Landing the Plane (Session Completion)
+
+**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.
+
+**MANDATORY WORKFLOW:**
+
+1. **File issues for remaining work** - Create issues for anything that needs follow-up
+2. **Run quality gates** (if code changed) - Tests, linters, builds
+3. **Update issue status** - Close finished work, update in-progress items
+4. **PUSH TO REMOTE** - This is MANDATORY:
+ ```bash
+ git pull --rebase
+ bd sync
+ git push
+ git status # MUST show "up to date with origin"
+ ```
+5. **Clean up** - Clear stashes, prune remote branches
+6. **Verify** - All changes committed AND pushed
+7. **Hand off** - Provide context for next session
+
+**CRITICAL RULES:**
+- Work is NOT complete until `git push` succeeds
+- NEVER stop before pushing - that leaves work stranded locally
+- NEVER say "ready to push when you are" - YOU must push
+- If push fails, resolve and retry until it succeeds
\ No newline at end of file
new file mode 100644
index 0000000..0eac1ce
@@ -0,0 +1,25 @@
+---
+visibility: public
+---
+
+# Changelog
+
+All notable changes to picortex will be documented here.
+
+Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+- Initial project scaffold (docs-only, no code)
+- PRD v1 (`docs/prd/001-picortex-v1.md`)
+- Phased roadmap S0–S9 (`docs/plans/2026-04-23-initial-roadmap.md`)
+- ADRs 0001–0005
+- Specs 001–008
+- LLM wiki (Karpathy-style) under `docs/wiki/`
+- `llms.txt` sitemap and `AGENTS.md` agent instructions
+
+## [0.0.1] — 2026-04-23
+
+### Added
+- Project initialized. Planning phase begins.
\ No newline at end of file
new file mode 100644
index 0000000..fad698d
@@ -0,0 +1,50 @@
+---
+visibility: public
+---
+
+# ADR-0001: Standalone project, not a noos or Cortex fork
+
+**Status:** Accepted
+**Date:** 2026-04-23
+**Deciders:** Jacob
+
+## Context
+
+Three plausible homes for this work existed:
+
+1. **Fork Cortex** (`IdeaFlowCo/cortex`). Cortex already implemented 90% of what picortex needs (per-chat workspaces, attention gating, scoped tokens, sharing bridge, linq-sim, HMAC webhook ingestion). This past week Cortex shipped the J1-J11 texting-bot + groups plan.
+2. **Fork the noos Slack bot** (`~/code/noos/src/slack/`). noos already has a working Slack bot that spawns Claude Code via `claude --print --resume` and streams tool-use progress back. It has reaction handlers (stubbed), per-workspace config, and a Noos OAuth backbone.
+3. **Standalone project**, borrowing patterns but owning its own code.
+
+## Decision
+
+**Standalone project**, hosted at `~/code/picortex/`.
+
+## Consequences
+
+### Positive
+
+- Jacob can experiment with a substantially different isolation model (Linux users vs Docker containers) without destabilizing Cortex's production deployment.
+- No entanglement with Cortex's enterprise design pressures (multi-tenant, billing, workspace dashboard, Slack plugin, Linq channel plugin framework).
+- picortex can target iMessage-first and mobile-first without Cortex's Slack/Web dual-surface constraint.
+- Clear IP boundary — picortex is Jacob's personal stack; Cortex is IdeaFlowCo's product. No attribution confusion.
+- Noos stays focused on being the knowledge-graph + SSO identity layer; picortex doesn't drag noos's release cadence around.
+
+### Negative
+
+- Duplicates initial backend scaffolding (Fastify, auth, logger, SQLite migrations) that Cortex already has.
+- Risk of divergent attention-gating implementations that drift from Cortex's prompt library over time.
+- No shared velocity — a fix Cortex makes doesn't automatically reach picortex.
+
+### Mitigations
+
+- **Inherit-don't-re-derive rule**: see [`docs/wiki/cortex-inheritance.md`](../wiki/cortex-inheritance.md). When picortex implements a subsystem Cortex already has, the PR must link to the Cortex file it drew from and note divergences.
+- **Quarterly diff review**: every 3 months, skim Cortex commits since last review and port anything relevant.
+- **linq-sim is shared** (lives in `~/code/cortex/cloudcli/dev-tools/linq-sim`). picortex's linq-sim changes (S2 thread support) go upstream as PRs to Cortex.
+- **noos integration deferred** but kept on the table. See [`docs/wiki/relationship-to-noos.md`](../wiki/relationship-to-noos.md).
+
+## Alternatives considered
+
+- **Fork Cortex into a private tmad4000 repo and strip.** Rejected — too entangled with enterprise concerns; strip would take ~1 week and the remainder would still carry Cortex's Docker-per-workspace assumption.
+- **Extend noos-slackbot to iMessage.** Rejected — noos's Slack bot is a worker inside a Noos-centric knowledge graph, not a standalone chat agent. Bolting Linq support onto it would muddle noos's identity.
+- **Build it inside voice-assistant.** Tempting (voice-assistant already has file system, run_command, Claude spawning, Noos OAuth) but voice-assistant is voice-first and its deployment is on Hetzner jcortex alongside a conversational ElevenLabs pipeline. Text-first chat logic would be a shoehorn. Revisit in D1 if Hetzner ends up the deployment target.
\ No newline at end of file
new file mode 100644
index 0000000..772d592
@@ -0,0 +1,83 @@
+---
+visibility: public
+---
+
+# ADR-0002: Linux users over Docker for per-chat isolation
+
+**Status:** Accepted (provisional — revisit after [D2 isolation report](../plans/2026-04-23-initial-roadmap.md#d2-security-isolation-report))
+**Date:** 2026-04-23
+**Deciders:** Jacob
+
+## Context
+
+Each chat (1:1 or group) needs an isolated workspace so compromise of chat A cannot read chat B's files, exfiltrate secrets, or influence chat B's Claude Code sessions. Cortex uses **Docker containers per workspace**. picortex explicitly rejects that model. Alternatives:
+
+| Option | Spin-up cost | Memory per idle | Security | Complexity |
+|---|---|---|---|---|
+| Docker container per chat | seconds + image pull | 100-500 MB | strong | high (daemon, network, volumes) |
+| Linux user + home dir + POSIX perms | milliseconds | ~ a few MB (tmux+bash) | moderate | low |
+| firejail / bubblewrap namespace | milliseconds | ~ a few MB | strong | medium (seccomp policy tuning) |
+| nsjail | milliseconds | ~ a few MB | strong | medium |
+| Landlock (6.1+ kernel) | ~ nothing | ~ a few MB | strong | medium (new-ish API) |
+| VM per chat (microVM, Firecracker) | 100s ms | 128+ MB | very strong | high |
+
+Jacob's context:
+
+- **Single user (Jacob).** Threat model is "sloppy group-chat prompt injection" not "adversarial tenant."
+- **Small machine.** Target is a 4 GB VPS or Mac Mini.
+- **Iteration speed matters more than defense-in-depth** at v0.1.
+- **Prior research** in `~/memory/research/openclaw-group-chat-security.md` and `openclaw-security-audit-2026-02-20.md` documents the prompt-injection threat model in depth.
+
+## Decision
+
+**v1: per-chat Unix user + home dir with POSIX permissions (0700) + `runuser`/`sudo -u`.**
+
+This will be hardened in D2 by potentially wrapping the tmux session entry point in **bubblewrap** or **Landlock** — those are cheap composable additions to the user-based base, not replacements for it.
+
+## Consequences
+
+### Positive
+
+- Sub-second chat provisioning (`useradd`, `mkdir`, `chown`, `tmux new-session`).
+- Idle cost: one tmux + one bash ≈ a few MB RAM.
+- No Docker daemon, no image registry, no volume orchestration.
+- POSIX is boring and well-understood; less novel surface area.
+- Trivially composes with bubblewrap / Landlock later without re-architecting.
+
+### Negative
+
+- Shared kernel. A kernel-level exploit affects everyone.
+- Shared `/tmp`, shared networking by default. Needs mitigation.
+- No easy "rm -rf the container" if a chat goes bad — teardown means `userdel -r` + cleanup.
+- Cap'ed by `useradd` uid range on some systems; unlikely to matter at Jacob's scale.
+
+### Mitigations
+
+- **`/tmp` per user:** set `pam_namespace` so `/tmp` is per-user-isolated.
+- **Network:** Claude Code processes run without `NET_BIND_SERVICE` and behind a firewall that blocks outbound except `api.anthropic.com` and a known allowlist. Egress filter via `iptables` owner-match.
+- **FS allowlist:** chat user can't write outside its home. Enforced by owner-only permissions on the home dir and read-only bind mounts for shared tools (`/usr/local/bin/picortex-shims`).
+- **Resource limits:** `cgroups` v2 per chat user — CPU share, memory cap (256 MB default), pids.max.
+- **Seccomp / Landlock:** added in D2 if threat model changes.
+- **Teardown path:** `userdel -rf chat-<hex>` + remove sudoers drop-in. Scripted in `scripts/destroy-chat.sh`.
+
+## Trigger for revisit
+
+- D2 report finds Linux-user perms insufficient for the threat model.
+- First isolation incident.
+- Jacob invites a second user to picortex.
+- picortex opens to multi-tenant.
+
+## Alternatives considered in detail
+
+- **Docker containers per chat:** rejected (see top of file). Would add ~50 MB image-cache baseline per chat and >1 s spin-up; operationally heavy for a personal tool.
+- **OpenClaw sandbox:** rejected per explicit user directive ("no OpenClaw"). Also: sandbox model is whole-agent, not per-chat.
+- **Firejail:** considered; keep as a possible D2 addition. Its default profiles are friendlier than bubblewrap for interactive shells.
+- **bubblewrap (`bwrap`):** the strongest "cheap" hardening option; composes well with Linux users. Primary D2 candidate.
+- **nsjail:** more flexible than bwrap but more config; hold for D2.
+- **Microsandbox / gVisor / Firecracker:** overkill for v1.
+
+## References
+
+- `~/memory/research/openclaw-group-chat-security.md`
+- `~/memory/research/openclaw-security-audit-2026-02-20.md`
+- [Cortex R2.1](../wiki/cortex-inheritance.md#r2-workspace-identity) — "each chat = own filesystem" invariant
\ No newline at end of file
new file mode 100644
index 0000000..57c740f
@@ -0,0 +1,55 @@
+---
+visibility: public
+---
+
+# ADR-0003: tmux for per-chat session persistence
+
+**Status:** Accepted
+**Date:** 2026-04-23
+**Deciders:** Jacob
+
+## Context
+
+Each chat needs a persistent shell/REPL that holds Claude Code context across turns. Options:
+
+| Option | Persistence | Attach from browser? | Claude Code fit |
+|---|---|---|---|
+| Short-lived `claude --print` per turn | none | N/A | simplest but loses agentic state |
+| Raw PTY + `node-pty` keep-alive | in-memory | via WS | loses on process restart |
+| tmux session per chat | disk-backed, survives restart (with `tmux resurrect`) | `tmux attach` inside WS PTY | excellent |
+| screen / dtach | similar to tmux | similar | OK but tmux has better tooling |
+
+Cortex uses tmux (see `cloudcli/server`). Claude Code itself is often used inside a tmux split by Jacob.
+
+## Decision
+
+**tmux, one session per chat, session name `picortex:<chat_id>`.**
+
+## Consequences
+
+### Positive
+
+- Survives backend restarts.
+- Web terminal (xterm.js) can attach via a backend WebSocket that shells into `tmux attach -t picortex:<chat_id>`.
+- Multiple browser tabs can attach to the same session (tmux natively multiplexes).
+- `tmux capture-pane` / `pipe-pane` gives a structured way to scrape Claude Code's output for reply routing.
+- Zero memory cost when idle (bash + tmux).
+- Consistent with how Jacob already works.
+
+### Negative
+
+- tmux protocol has quirks when streamed over WebSocket (scrollback, resize, escape sequences).
+- `send-keys` + output tailing is fragile for structured reply parsing — need a small protocol on top (e.g. unique sentinel before/after each turn).
+- tmux must be installed on the server (trivial but still an explicit dep).
+
+### Mitigations
+
+- Use `pipe-pane -o` to stream output to a logfile per chat; parse the logfile for reply extraction, not the terminal.
+- Use a delimiter protocol: before each turn, `tmux send-keys "echo '<<PICORTEX-TURN-$n-START>>'"`; after, `"echo '<<PICORTEX-TURN-$n-END>>'"`. Reply = content between delimiters, stripped of ANSI.
+- Resize: send `tmux refresh-client -S` on WS resize events; xterm.js reports cols/rows.
+- Fallback plan: if reply parsing turns out unreliable, swap to `claude --print` per turn in S3.1 and keep tmux only for the **user-visible terminal attach**, not for reply capture.
+
+## Alternatives considered
+
+- **One long-lived `claude --print` per turn, no tmux**: Simpler, but loses the web-terminal-attach feature, which is an explicit v1 goal (FR-12). tmux covers both.
+- **Reuse Cortex's cloudcli PTY mux**: Heavier; carries Cortex's container assumptions. Not worth the integration cost for v1.
\ No newline at end of file
new file mode 100644
index 0000000..dd52857
@@ -0,0 +1,56 @@
+---
+visibility: public
+---
+
+# ADR-0004: Linq as the primary channel
+
+**Status:** Accepted
+**Date:** 2026-04-23
+**Deciders:** Jacob
+
+## Context
+
+picortex could target several messaging protocols:
+
+- **Linq** — iMessage + SMS partner API; the same one Cortex uses.
+- **Slack** — noos already has a Slack bot; well-trodden path.
+- **Telegram** — OpenClaw uses it; good bot API.
+- **Native iMessage via BlueBubbles / AppleScript** — sketchy, not scalable.
+- **OpenChat** — Jacob's own messaging app; would need a linq-compatible adapter (candidate for D3).
+
+Jacob's explicit goal: "chat with Claude Code from my phone, via iMessage, including group texts."
+
+Only Linq offers a legitimate iMessage + iMessage-groups surface without running macOS + JavaScript-for-Automation chicanery.
+
+## Decision
+
+**Linq is the primary channel. linq-sim (Cortex `cloudcli/dev-tools/linq-sim`) is the dev/testing channel. OpenChat with a linq-compatible adapter is a deferred secondary channel (D3).**
+
+Interface abstraction in picortex is shaped to Linq's event vocabulary (14 event types). Anything talking to picortex over its inbound webhook must conform — this way linq-sim, real Linq, and a future OpenChat adapter are interchangeable.
+
+## Consequences
+
+### Positive
+
+- Dev loop is Cortex-quality from day 1: linq-sim provides iMessage-shaped traffic without real phone numbers.
+- Reuses Cortex's HMAC webhook contract exactly (same `{timestamp}.{raw_body}` signing).
+- No phone-tree / no carrier setup until Jacob is ready for real Linq.
+- Future OpenChat work pays dividends because adapter can also serve picortex.
+
+### Negative
+
+- Linq is a paid partner API; real deployment costs money.
+- Locks event model to Linq's shape. If we later want Slack-first, we'd need an adapter.
+- Linq's group-text behavior is specific — no cross-channel semantics.
+
+### Mitigations
+
+- Interface: picortex defines a `Channel` interface; `LinqChannel` is the v1 implementation. Adapters for Slack/Telegram/OpenChat can plug in without touching core logic.
+- No hard Linq-specific calls in business logic. All outbound goes via the Channel abstraction.
+- linq-sim parity check: automated tests emit every event type linq-sim supports.
+
+## Related
+- [Spec: Linq integration](../specs/006-linq-integration.md)
+- [Wiki: Linq protocol](../wiki/linq-protocol.md)
+- [Wiki: OpenChat adapter](../wiki/openchat-adapter.md)
+- [Cortex inheritance — linq webhook shape](../wiki/cortex-inheritance.md#linq-webhook-shape)
\ No newline at end of file
new file mode 100644
index 0000000..4d47805
@@ -0,0 +1,56 @@
+---
+visibility: public
+---
+
+# ADR-0005: Inherit from Cortex patterns (R1-R19)
+
+**Status:** Accepted
+**Date:** 2026-04-23
+**Deciders:** Jacob
+
+## Context
+
+Cortex's `docs/future-plans/texting-bot-groups/` directory contains ~100 numbered requirements (R1-R19) developed over the past month, covering per-chat workspace isolation, attention gating, sharing bridge, phone-OTP identity, and so on. They are the most thought-through design for this problem anywhere Jacob has access to.
+
+The options are:
+
+1. **Re-derive from scratch.** Rejected: wastes weeks re-litigating decisions Cortex already made.
+2. **Copy-paste R1-R19 into picortex docs verbatim.** Rejected: stale-on-day-one; no attribution; muddles diff-with-Cortex reviews.
+3. **Inherit by reference.** Mark each Cortex requirement as one of: `adopt`, `adapt`, `reject`, `defer`. Document divergences in an inheritance map.
+
+## Decision
+
+Inherit by reference. Maintain [`docs/wiki/cortex-inheritance.md`](../wiki/cortex-inheritance.md) as a living map. Every Cortex requirement gets one of four labels:
+
+- **adopt** — use as-is; cite Cortex doc path.
+- **adapt** — use the idea, change implementation (e.g. Linux users instead of Docker).
+- **reject** — does not apply to picortex (e.g. enterprise billing flow).
+- **defer** — applies but post-v0.1.
+
+When Cortex updates a requirement, the inheritance map gets re-reviewed at quarterly diff checkpoints (see [ADR-0001](0001-standalone-project-not-noos-fork.md) "Mitigations").
+
+## Consequences
+
+### Positive
+
+- Zero re-derivation cost.
+- Clear audit trail for why each decision was made: "we adopted R4.3" or "we adapted R2.1 from Docker to Linux users (see ADR-0002)."
+- Cortex's language (e.g. `WorkspaceAccessResolver`, `BridgeEvent`) can be reused — agents writing code for picortex recognize it.
+- Forces explicit decision on every requirement — no silent drift.
+
+### Negative
+
+- Requires reading Cortex's plan-jacob-unified.md + requirements.md in full to build the map.
+- If Cortex's R-numbers ever reshuffle, the map breaks. Needs timestamped snapshot column.
+- Divergences have to be maintained in *two* places (ADR + inheritance map) — unavoidable but notable.
+
+### Mitigations
+
+- Inheritance map pins Cortex git SHA on adoption (e.g. "as of `e55f129`, 2026-04-23").
+- Every 3 months, re-diff against latest Cortex and update.
+
+## Related
+
+- [Cortex inheritance map](../wiki/cortex-inheritance.md) — the actual mapping
+- [ADR-0001](0001-standalone-project-not-noos-fork.md) — why standalone not fork
+- Cortex source: `IdeaFlowCo/cortex` @ `~/code/cortex/docs/future-plans/texting-bot-groups/`
\ No newline at end of file
new file mode 100644
index 0000000..93631be
@@ -0,0 +1,48 @@
+---
+visibility: public
+---
+
+# Mockups
+
+picortex inherits UI from Jacob's prior "betterGPT" mockups and Cortex's recent J7-J11 stage-mocks. Rather than re-host, link:
+
+## BetterGPT (Jacob's older mockups)
+
+**Location:** `~/code/cortex/mockups/bettergpt/`
+
+Relevant HTML files:
+
+- `index-split-view-filebrowser.html` — **primary reference for file browser + split view**. This is the layout picortex is implementing.
+- `memory-v2.html` → `memory-v6-notes.html` — evolution of chat + context panel
+- `memory-v4-unified-groups.html` — unified groups variant (not v1 for picortex but useful later)
+
+Mobile screenshots:
+- `~/code/cortex/mockups/bettergpt/screenshots/mobile_final-*.png` (0-18) — primary mobile UI reference
+- `mobile_view.png`, `mobile_scroll.png` — mobile scroll behavior
+
+## Cortex stage-mocks (2026-04-21)
+
+**Location:** `~/code/cortex/docs/future-plans/texting-bot-groups/stage-mocks/`
+
+These are J7-J11 high-fidelity mockups — most relevant:
+
+- **J7 dashboard** — solo vs group workspace cards (picortex explicitly does NOT do a dashboard, but the per-chat cards and empty-state pattern inform the chat-list view)
+- **J8 phone-otp** — SMS OTP login on `/<slug>` (picortex: Noos OAuth only for v1)
+- **J9 per-chat settings** — attention mode, spend cap, sharing toggles, member list — **directly usable for picortex Spec 005**
+- **J10 bridge** — file picker to pull from personal workspace into group — **directly usable for picortex Spec 009 (sharing bridge)**
+- **J11 discriminator** — markdown editor for `.cortex/prompts/discriminator.md` with versions + live-test + backtest — **directly usable for picortex Spec 005**
+
+## Claude Code Web UI
+
+**Location:** `~/code/cortex/mockups/claude-code-web/`
+
+- `index.html`, `index-dark.html` — Cortex's web-based Claude Code mockup
+- Relevant for: web terminal surround, command bar, output rendering
+
+## Snapshots (pinned for reference)
+
+Until picortex has its own designs, the Cortex mockups are the canonical reference. If Cortex reorganizes or deletes, we'll capture snapshots here. As of 2026-04-23 the paths above are live.
+
+## Goal for picortex-native mockups
+
+After S7 (web UI scaffold), picortex gets `docs/mockups/*.html` for its own design variants. Reference these in later spec revisions.
\ No newline at end of file
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
new file mode 100644
index 0000000..c687ec2
@@ -0,0 +1,136 @@
+---
+visibility: public
+---
+
+# 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](../plans/2026-04-23-prototype-options.md). 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 in `picortex-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:
+1. A persistent Claude Code tmux session per iMessage conversation,
+2. Per-chat Unix-user isolation so group-chat compromise doesn't leak across,
+3. A mobile-first web UI to see the file tree and attach a live terminal mid-conversation,
+4. 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 `BridgeEvent` audit 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](../adrs/0002-linux-users-over-docker.md)
+- **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](../wiki/cortex-inheritance.md#r6-mcp-cross-chat) deferred; sharing bridge R7 is enough).
+- **NG7.** ~~No OpenClaw. No OpenClaw. No OpenClaw.~~ **Softened 2026-04-23:** OpenClaw was initially ruled out, but `docs/plans/2026-04-23-prototype-options.md` Option 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_command` on 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.md` from my DM workspace." The bot DMs me a challenge; I reply "yes"; the file appears in the group chat's workspace and a `BridgeEvent` is logged.
+- **US-6.** As Jacob deploying picortex, I run `./deploy.sh` and 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 with `HMAC-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](../plans/2026-04-23-initial-roadmap.md#s2-linq-sim-thread-support).*
+
+### 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 `discriminate` runs an LLM over the message with a git-versioned prompt at `$CHAT_HOME/.picortex/prompts/discriminator.md` and 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 `BridgeEvent` row (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.Z` from `package.json`. When `HEAD` on `main` is 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-ID` header, echoed to the response.
+- **FR-24.** `POST /api/frontend-log` accepts browser error reports and writes them to server logs with the originating `X-Request-ID`.
+- **FR-25.** `/health` returns 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](../wiki/openchat-adapter.md) |
+| Q5 | Does picortex eventually federate with noos (knowledge graph backend)? | Jacob | Open — see [wiki/relationship-to-noos.md](../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.
\ No newline at end of file
new file mode 100644
index 0000000..0a3f00c
@@ -0,0 +1,180 @@
+---
+visibility: public
+---
+
+# PRD 002 — Awesome Texting Experience (Q0)
+
+**Status:** Draft — answers `picortex-adb` / Q0 of the [prototype-options brainstorm](../plans/2026-04-23-prototype-options.md)
+**Owner:** Jacob
+**Date:** 2026-04-23
+
+> This PRD defines what "awesome texting experience" means in measurable terms. It is the product target every architecture option (Options 1-5 in the brainstorm) must hit. If an option can't hit these criteria, the option is out. Supersedes the product section of [PRD 001](001-picortex-v1.md) (which was architecture-first and scoped around dev surfaces).
+
+## 1. Vision (one paragraph)
+
+Jacob adds a bot to any ongoing texting discussion — iMessage via Linq OR OpenChat — and it works pretty well. It feels native on the platform (replies threaded, reactions supported, typing indicator, not spammy in groups). It has access to whatever Jacob has told it about himself, but private information never leaks from a 1:1 DM into a group without Jacob's explicit out-of-band approval. When a group member asks something that needs private data ("is Jacob free Tuesday?"), the bot pauses, DMs Jacob privately with the proposed disclosure, waits for approval, and only then answers in the group — with a clear audit trail of what got shared and why.
+
+## 2. The product in one test
+
+**The single test that proves we've shipped it:**
+
+> A friend adds the bot to a group text with three other people. Over the next hour:
+>
+> 1. The bot stays quiet during normal chatter.
+> 2. When someone @mentions it with a question that's public-friendly, it replies usefully in under 15 seconds, in-thread.
+> 3. When someone asks "hey, is Jacob free to meet this week?", the bot says "let me check" in the group, DMs Jacob privately with the question + its proposed response, and only answers the group once Jacob approves.
+> 4. No one in the group sees anything from Jacob's private DM workspace, calendar, or knowledge base that Jacob didn't explicitly approve.
+>
+> If this loop works end-to-end, consistently, the product is live.
+
+## 3. Five pillars
+
+### P1 — Native platform feel
+
+The bot must not feel like a web chatbot bolted onto SMS. Specifically:
+
+- **P1.1 Threading.** In-thread replies preserve `reply_to_message_id` when replying to a specific message (not just the most recent).
+- **P1.2 Reactions.** Bot reacts with tapbacks to acknowledge tasks (👍 "understood", 🤔 "thinking", ✅ "done"). Reactions **on** the bot trigger behaviors (⭐ save, 💡 remember, 🔖 bookmark, ❌ retract last message).
+- **P1.3 Typing indicator.** On within 2s of inbound when the bot plans to respond; off when done.
+- **P1.4 Edits.** If a user edits their message before the bot replies, the bot uses the latest version. After the bot replies, user edits are acknowledged but don't re-trigger.
+- **P1.5 Formatting.** No markdown walls on iMessage. Code blocks render as quoted plain text; bullets as short dashes; long answers split across messages with context ("1/3 — ", "2/3 — ").
+- **P1.6 Participant changes.** Bot handles add/remove/rename gracefully — notes them, doesn't panic.
+- **P1.7 Channel parity.** Linq (iMessage) and OpenChat identical behavior. One event normalization layer.
+
+### P2 — Smart attention (not spammy)
+
+Groups die when bots interrupt. Defaults:
+
+- **P2.1 Default mode in groups: `mentions-only`** — bot responds only to `@bot`, reply-to-bot, or `/slash` commands.
+- **P2.2 Default mode in DMs: `always`** — bot responds to every message.
+- **P2.3 Upgradable in-place.** `@bot louder` / `@bot quieter` / `@bot silent` adjusts in-line; Jacob's-DM-only admin commands.
+- **P2.4 Discriminator mode** — optional per-chat LLM gate (from Cortex R4) that decides whether to respond to non-mentions based on topic relevance. Default OFF for groups.
+- **P2.5 Rate ceiling.** Bot never sends > 1 message per 20 human messages in a group over any 10-minute window (hard cap, logged).
+- **P2.6 Silence on unclear.** If a group message is ambiguous ("did you see that?"), the bot does NOT try to guess — silent.
+
+### P3 — Knowledge with boundaries (per-chat manifest)
+
+Every chat has a `knowledge_manifest` defining what subsets of Jacob's world the bot may draw on. **Default for a new group: nothing private.** The bot has only the group's own transcript + public info.
+
+Jacob can expand a group's manifest explicitly:
+
+- **P3.1 Granular grants.** Subsets of the noos graph (specific tags, specific note-collections), specific calendar windows ("next 2 weeks only"), specific contact tags, specific files.
+- **P3.2 Ephemeral vs permanent.** A grant can be "for this thread only" (expires when the thread dies), "for 24h", or permanent.
+- **P3.3 Default DM manifest:** everything. 1:1 chat with Jacob = full trust.
+- **P3.4 Per-chat manifest is versioned and auditable.** Changes log as `ManifestEvent` rows: who, when, what was added/removed.
+- **P3.5 Bot discloses its boundaries on request.** "What do you know about me in this chat?" → bot responds with the manifest summary.
+
+### P4 — Consent-mediated disclosure (the banger)
+
+When the bot needs private data the chat's manifest doesn't cover, it pauses and asks Jacob out-of-band.
+
+The loop:
+
+- **P4.1 In-group:** bot replies "let me check with Jacob" within 5s. Typing indicator stays on.
+- **P4.2 Out-of-band DM to Jacob:** within 5s of the group trigger. Message contains:
+ - The question as asked
+ - The full group context (which chat, who asked, last N messages)
+ - The specific private data needed ("calendar Tuesday 9am-9pm")
+ - A **proposed response** to send back to the group
+ - Buttons / text shortcuts: `approve`, `approve-subset`, `modify`, `deny` + optional reason
+- **P4.3 Jacob's approval has three granularities:**
+ - "Approve exact" — send exactly the proposed response, no more
+ - "Approve subset" — edit the response first, then send
+ - "Permanent grant" — also upgrade the group's manifest so future similar asks don't need approval
+- **P4.4 Timeout.** If Jacob doesn't respond in 10 minutes, bot tells the group "I need to check on that offline — will follow up later" and closes the loop. No indefinite waits.
+- **P4.5 Audit trail.** Every disclosure loop logs a `DisclosureEvent` row: question, source chat, destination chat, data accessed, approver, approval mode, timestamp, final message sent.
+- **P4.6 No leak on deny.** If Jacob denies, bot says "I don't have access to answer that" — does NOT say "Jacob said no" or reveal the internal loop.
+- **P4.7 Revocable.** Jacob can retract a permanent grant at any time; next similar ask goes through approval again.
+
+This is Cortex's R5.4-5.5 + R7 sharing bridge **turned outward**: instead of importing files into a chat, we're authorizing knowledge to flow from private → group at read time.
+
+### P5 — Graceful degradation
+
+- **P5.1 Tool-use failure.** "I tried to check X but the tool failed — want me to try again or skip?" Never silent-swallow an error.
+- **P5.2 Knowledge denied.** "I don't have access to that" (truthful but not oversharing).
+- **P5.3 Bot offline.** Linq inbound still queued; when back, bot says "I was offline — here's my reply to your earlier message". Never drop inbound silently.
+- **P5.4 Ambiguous intent.** "I'm not sure which X you mean — A or B?" Ask, don't assume.
+- **P5.5 Over-capacity.** If the bot is saturated and Jacob's pending approvals queue is deep, it says so in the group: "I'm backed up — answer in a few minutes."
+
+## 4. Latency budget
+
+| Scenario | Target | Ceiling |
+|---|---|---|
+| Typing indicator appears after inbound | 2s | 5s |
+| Warm 1:1 first token | 5s | 15s |
+| Warm group first token (when bot decides to speak) | 10s | 20s |
+| Cold provisioning (first message ever to a new chat) | 30s | 60s |
+| "Let me check" ack in consent loop | 5s | 10s |
+| End-to-end consent loop (group ask → Jacob approve → group reply) | 30-120s | 10 min (timeout) |
+| Bot recovery from crash | 30s | 2 min |
+
+## 5. Measurable success metrics
+
+Gate these at 30 days of real use:
+
+- **M1.** Jacob sends ≥ 5 messages/day to the bot (1:1) for 14 consecutive days.
+- **M2.** Bot is in ≥ 2 real group chats (friends / family / coworkers — not test groups) for ≥ 7 days each.
+- **M3.** ≥ 1 consent-loop successfully completed end-to-end (group ask → Jacob approve → group reply, all in audit log).
+- **M4.** Zero unapproved private-info disclosures across all groups (audited).
+- **M5.** p95 warm 1:1 first token ≤ 15s.
+- **M6.** p95 warm group first token ≤ 20s.
+- **M7.** Bot's message ratio in groups stays ≤ 1:20 vs humans over any 10-min window.
+- **M8.** Friends in group chats describe bot as "useful" or "not annoying" in unstructured feedback (qualitative; ≥ 3 positive statements, 0 "please remove it" requests).
+
+## 6. Pass/fail acceptance tests
+
+These are the tests that must green before v0.1 ships:
+
+1. **T-native.** Bot replies in-thread, types typing indicator, uses tapback reactions. Verified on iMessage via Linq AND on OpenChat via its WebSocket.
+2. **T-attention.** In a 50-message group conversation where bot is NOT mentioned, bot sends 0 messages. When mentioned once, bot sends 1 reply. Over-ceiling guard triggers if bot attempts to exceed the 1:20 ratio.
+3. **T-manifest-default.** New group, default manifest. Someone asks "what's in Jacob's calendar?" — bot declines without DMing Jacob (it's not in scope for this group, and the default is deny).
+4. **T-consent-approve.** Someone asks "is Jacob free Tuesday?" in a group with calendar access not pre-authorized. Bot → "let me check". DM to Jacob within 5s with proposed response. Jacob replies "yes". Group gets the answer. `DisclosureEvent` logged.
+5. **T-consent-deny.** Same setup; Jacob replies "no". Group gets "I don't have access to answer that" — no hint about the DM loop.
+6. **T-consent-timeout.** Same setup; Jacob doesn't reply. After 10 min, group gets "I need to check on that offline — will follow up later".
+7. **T-retract.** Jacob reacts ❌ on a bot message in any chat. Bot deletes / retracts that message (platform-specific: iMessage edit to "[retracted]"; OpenChat actual delete).
+8. **T-offline.** Backend restarted mid-message. On recovery, bot sends "I was offline — here's my reply".
+9. **T-parity.** All tests above pass identically on Linq iMessage AND OpenChat.
+
+## 7. Explicit non-goals for v0.1
+
+- **NG1.** Voice memos, images, attachments beyond text. v0.2.
+- **NG2.** Native group UI (any web UI) — 1:1 web UI OK if it falls out for free; group UI is v0.2.
+- **NG3.** Multi-user bot operation (other people running their own picortex pointing at shared infra). v1.0+.
+- **NG4.** Cross-channel unification ("same Jacob on Linq DM and OpenChat DM is the same context"). v0.3.
+- **NG5.** Proactive messages from the bot (bot spontaneously texting based on calendar etc.). v0.2.
+- **NG6.** Shared bot in both a Jacob-only DM and a group — where the DM context influences the group. v0.3.
+- **NG7.** Fine-grained per-person-in-group policy ("Alice can ask about calendar, Bob can't"). v0.3+.
+
+## 8. Open questions → traceability
+
+| Q ticket | Question | How this PRD constrains it |
+|---|---|---|
+| Q1 `picortex-g0u` | Physical layout | Consent loop (P4) needs reliable DM delivery while group is waiting — argues against single-box failure domains. |
+| Q2 `picortex-2b4` | Dev surface v0.1? | Not required by any pillar. Can be deferred. |
+| Q3 `picortex-xmd` | Workspace granularity | Per-chat matters because manifest is per-chat. Per-user would collapse manifests. |
+| Q4 `picortex-3mk` | Agent executor | Must support deferred-response pattern (start a turn, pause for out-of-band approval, resume). `claude -c -p` per turn handles this cleanly; tmux REPL is awkward. |
+| Q5 `picortex-5gi` | noos graph drop-in | Directly feeds P3 knowledge manifests. Strongly preferred. |
+| Q6 `picortex-2qk` | Privacy threat boundary | P3 + P4 are the primary privacy features; threat model must protect both. |
+| Q7 `picortex-qrc` | Deployment target | DM-while-group-waiting implies low-latency + always-on. Fly Machine or Hetzner win over HMA (home internet flakiness). |
+| Q8 `picortex-f7r` | v0.1 slice | This PRD IS Q0's answer; Q8 is "which pillars + which tests are v0.1, and which are v0.2". |
+
+## 9. v0.1 minimum slice (proposed — to be finalized in Q8)
+
+Ship these first:
+- **P1** (native feel) — minus P1.2 reactions-to-trigger-behaviors (that's v0.2)
+- **P2** (attention) — mentions-only + DM always; discriminator mode deferred to v0.2
+- **P3** (manifests) — default-deny for groups, default-allow for DMs; granular grants deferred to v0.2
+- **P4** (consent loop) — approve/deny only; "approve-subset" edit + permanent-grant upgrade deferred to v0.2
+- **P5** (graceful degradation) — all of it
+
+Tests that must green for v0.1: T-native, T-attention, T-manifest-default, T-consent-approve, T-consent-deny, T-consent-timeout, T-offline, T-parity.
+
+Metrics gating v0.1 → v0.2: M1, M3, M4, M5, M7. (M2 and M8 require v0.1 in real use for 30+ days.)
+
+## 10. Related
+
+- [PRD 001](001-picortex-v1.md) — original architecture-first PRD (now provisional; this supersedes its product section)
+- [Prototype options](../plans/2026-04-23-prototype-options.md) — architecture options that must hit these criteria
+- [Piyush-era study](../wiki/piyush-era-design.md) — Option 2 candidate
+- [Codex session review](../reviews/session-review-codex-2026-04-23.md) — flagged "awesome texting experience is unpinned" as the biggest gap
+- Cortex R4 (attention gating) + R5.4-R5.5 (out-of-band consent) + R7 (sharing bridge) — ancestors of P2 + P4
\ No newline at end of file
new file mode 100644
index 0000000..f514f48
@@ -0,0 +1,48 @@
+---
+visibility: public
+---
+
+# Codex Review — 2026-04-23
+
+## 1. Strengths
+
+- The docs are unusually coherent for a zero-code project. `README.md`, `AGENTS.md`, the PRD, and the roadmap all describe the same product and the same constraints without obvious drift.
+- The project has a sharp thesis: inherit from Cortex where possible, cut Docker, keep the surface small, and optimize for one user. That discipline shows up clearly in `docs/prd/001-picortex-v1.md` and `docs/plans/2026-04-23-initial-roadmap.md`.
+- Security invariants are stated early and bluntly: backend SQLite is canonical, cross-chat actions need explicit approval, and per-chat filesystem isolation is non-negotiable (`AGENTS.md`, `docs/specs/001-workspace-isolation-linux-users.md`).
+- The tmux choice is pragmatic and aligned with the actual user workflow. `docs/adrs/0003-tmux-for-session-persistence.md` is a good decision memo: clear tradeoffs, clear fallback.
+- The roadmap is demo-driven instead of feature-list-driven. That is the right shape for a risky systems product (`docs/plans/2026-04-23-initial-roadmap.md`).
+
+## 2. Risks and blind spots
+
+- S3 is the biggest planning mistake. A single global tmux session before per-chat isolation (`docs/plans/2026-04-23-initial-roadmap.md`) bakes the wrong architecture into the first real Claude integration, then forces a migration one stage later. That is avoidable churn.
+- The plan delays the canonical backend model too long. The PRD says SQLite is authoritative (`docs/prd/001-picortex-v1.md`), but S1 only mentions echoing webhooks and does not explicitly require minimal persistent schema, idempotency keys, or event normalization.
+- The platform story is unresolved but the isolation spec assumes deep Linux features now: `useradd`, cgroups v2, `pam_namespace`, `iptables` owner-match (`docs/specs/001-workspace-isolation-linux-users.md`, `docs/adrs/0002-linux-users-over-docker.md`). That conflicts with “Mac Mini” still being a deployment candidate in the PRD and roadmap.
+- The sudo model is too loose on paper. Allowing `tmux`, `runuser`, and `bash` through sudoers (`docs/specs/001-workspace-isolation-linux-users.md`) is broader than an audited entrypoint wrapper. That will be painful to harden later.
+- Reply capture is still speculative. The sentinel protocol in `docs/specs/002-tmux-session-spawning.md` might work, but it is not a small assumption; it is the core turn-routing mechanism. The roadmap treats it as implementation detail, not as a risk needing proof.
+- Webhook correctness is under-specified. I do not see a concrete plan for duplicate delivery, replay storage, outbound retry semantics, message edits, or ordering when two inbound messages race (`docs/prd/001-picortex-v1.md`, `docs/plans/2026-04-23-initial-roadmap.md`).
+- Attention gating depends on semantics not yet proven in the simulator. `mentions-only` and reply-based triggering lean on thread/reply support, but that is deferred to S2 and lives in another repo (`docs/specs/005-attention-gating.md`, `docs/plans/2026-04-23-initial-roadmap.md`).
+- The isolation story still has hand-wavy gaps around egress and shell capability. “No secrets in workspace FS” is good, but `claude --dangerously-skip-permissions` plus a real shell is still a lot of power unless the wrapper and network policy are nailed down (`docs/prd/001-picortex-v1.md`, `docs/specs/001-workspace-isolation-linux-users.md`).
+- Warm pool in S6 looks like premature optimization. It adds lifecycle complexity before you have any latency measurements proving provisioning is the bottleneck (`docs/plans/2026-04-23-initial-roadmap.md`).
+
+## 3. Concrete plan changes recommended
+
+1. Delete S3 as currently written, or merge it into S4. The first time Claude Code is integrated, it should already run in the per-chat model. If that is too much for one stage, do a non-persistent `claude --print` spike first, not a shared tmux architecture you already know you do not want.
+2. Add an S1.5 focused on backend authority: define the minimal SQLite schema, inbound event normalization, replay/idempotency storage, and outbound delivery records before any Claude/session work.
+3. Make the host requirement explicit now. If v0.1 depends on Linux-only controls, say “Linux only” and remove Mac Mini as an equal candidate until a reduced feature set is defined.
+4. Add a proof stage for tmux reply capture. Build a tiny harness that runs the real Claude CLI in tmux, injects sentinels, captures logs, and measures failure modes. Do not let the main roadmap depend on an unproven parser.
+5. Replace the broad sudoers plan with one narrow wrapper binary or script owned by root. The backend should invoke audited subcommands, not raw `bash`/`tmux` capability.
+6. Move queueing and concurrency rules into the core plan now: one active turn per chat, explicit duplicate-event handling, and outbound retry behavior. This is message infrastructure, not cleanup work.
+7. Push a minimal hardening gate earlier. If bubblewrap/Landlock is truly post-v0.1, then at least require a concrete egress-deny story before group-chat execution lands.
+8. Drop warm pool from the baseline roadmap. Reintroduce it only if measured cold-start data says S4/S6 misses the PRD latency target.
+
+## 4. Open questions before S1 starts
+
+- Is picortex v0.1 officially Linux-only, yes or no? If yes, update the docs to stop implying parity across Hetzner and HMA.
+- What exact records must exist in SQLite after S1: raw webhook payloads, normalized messages, replay cache, outbound attempts?
+- What is the retry/idempotency contract with Linq for inbound and outbound traffic?
+- Has anyone proven that Claude Code in tmux can be parsed reliably enough for automated reply extraction, or is that still a hypothesis?
+- Is the project willing to forbid the shared-session S3 path now, before implementation momentum makes it sticky?
+
+## 5. Verdict
+
+This is a strong planning package with a real product thesis, good inheritance discipline, and better-than-average security thinking for an early personal tool. The weak point is sequencing, not intent. The current roadmap introduces the wrong session architecture too early, postpones backend-authority details that should exist from day one, and assumes Linux controls that are still not reconciled with deployment ambiguity. Fix those three things before S1 starts and the plan looks solid. Ignore them and you are likely to spend the first implementation week building scaffolding you already know you will rip out.
\ No newline at end of file
new file mode 100644
index 0000000..8e4d53e
@@ -0,0 +1,43 @@
+---
+visibility: public
+---
+
+# Session Review — 2026-04-23 picortex planning
+
+## 1. Unresolved questions
+
+- The biggest unanswered question is still the one Jacob explicitly reframed toward at the end: what is the minimum definition of an "awesome texting experience"? The session created `picortex-adb`/Q0 to answer it, but no answer exists yet, so every architecture choice still floats on top of an unpinned product target.
+- OpenChat stayed unresolved. The session estimated reactions at about a day and a Linq-compatible adapter at 1-2 weeks, but it also discovered that the live `chat.globalbr.ai` deployment has no git remote and the public GitHub repo is stale, so the estimate rests on missing source control.
+- The privacy/isolation question was scoped but not decided: Linux users only, Linux users plus `bwrap`/Landlock, two-box remote-host separation, or something more ephemeral. `picortex-1cy` exists, but Jacob's "if we really care about privacy" question remained open.
+- The final user ask was left hanging: interview agents about WikiHub UX, preserve the session history, and ask Codex to review the full session for misses. The earlier Codex review covered the docs package, not the whole conversation.
+
+## 2. Contradictions or pivots we should reconcile
+
+- The docs still have two incompatible product stories. `README.md`, `llms.txt`, the PRD, ADR-0003, and Specs 002/003 all describe a tmux-centered architecture as the plan. But `docs/plans/2026-04-23-prototype-options.md` and `docs/wiki/piyush-era-design.md` explicitly reopen the core choice and even say Option 2 (`claude -c -p` / `--session-id`, two-box) is probably the best v0.1 shape.
+- "No OpenClaw" was initially stated as a hard rule, including PRD NG7, but the later prototype-options doc reopens OpenClaw as Option 5. That is a valid pivot, but it is not reconciled. Right now the written docs simultaneously say "explicit non-goal" and "still on the table."
+- The project status is stale at the top level. `AGENTS.md` says the first implementation stage is S1 of the initial roadmap, while the later Q0-Q8 tickets say implementation should not begin until the option bundle is chosen and the roadmap is replaced.
+
+## 3. Missing tickets
+
+- There should be a `picortex-` blocker ticket specifically for locating and/or reconstructing the real OpenChat source of truth. D3 assumes an adapter can be built later, but the session discovered a more basic blocker: nobody knows where the live code actually lives.
+- There should be a `picortex-` or meta cleanup ticket for "choose option bundle, then reconcile README/PRD/ADRs/specs with the chosen architecture." Q0-Q8 decide the future, but no ticket retires stale tmux assumptions afterward.
+- There should be a `wikihub-` ticket for agent-experience research itself: interview agents, summarize friction points, and turn them into a standing UX backlog. Several specific Agent UX tickets were filed, but the user's explicit interview/synthesis request was not.
+
+## 4. Documentation gaps
+
+- The docs package never absorbed the documentation-best-practices research that informed it. The report existed in `~/memory/research`, but nothing in `~/code/picortex/` records which conventions were adopted deliberately.
+- The WikiHub publication outcome is missing from picortex's own docs. The session published the planning set to `wikihub.globalbr.ai/@jacobcole/picortex`, but the local README does not record that public mirror.
+- The transcript's strongest reframing, "texting and mediating is the product; dev surface is optional," lives mainly in `prototype-options.md`. It never got promoted into the PRD itself, which still reads as if terminal/file-browser capability is part of the default answer rather than one branch of the decision tree.
+
+## 5. Risks not acknowledged
+
+- The plan underestimates product drift in Claude CLI itself. Both the tmux design and the Piyush-style per-turn design depend on exact CLI behavior (`-c`, `--session-id`, output formatting, auth persistence). That is a platform risk, not just an implementation detail.
+- Group-text privacy risk is framed mostly as filesystem isolation, but not as social/data-retention risk. Saving other people's messages, files, and maybe knowledge-graph enrichments for long periods may create a trust problem even if the POSIX isolation story is sound.
+- The simulator gap is bigger than the docs admit. linq-sim lacking reply/thread support was flagged, but real Linq/iMessage semantics for edits, reactions, threading, and delivery ordering may still diverge where this product cares most.
+
+## 6. Net positive surprises
+
+- The session produced a genuinely strong planning base in one pass: PRD, roadmap, ADRs, specs, wiki, repo-version pinning, and a second-opinion review. That is unusually disciplined for a zero-code day.
+- The best move was the willingness to pivot instead of defending the first plan. The Codex review changed the roadmap, and the Piyush study reopened a simpler, probably better architecture instead of being dismissed because it was "old."
+- Ticketing quality was high. The Q0-Q8 chain is the clearest part of the whole plan because it converts a vague product rethink into an ordered decision process.
+- Using WikiHub immediately exposed real agent-UX pain, and that pain turned into concrete platform tickets the same day. That feedback loop is worth preserving.
\ No newline at end of file
new file mode 100644
index 0000000..8d9dcf7
@@ -0,0 +1,89 @@
+---
+visibility: public
+---
+
+# Runbook: Deploy
+
+**Status:** Stub — finalized in [D1](../plans/2026-04-23-initial-roadmap.md#d1-deployment-target--runbook).
+
+Deployment target is open question Q3 in the PRD. Candidates:
+
+## Option A — Hetzner sibling to jcortex
+
+- VPS at Hetzner, x86_64, Ubuntu 22.04+
+- Alongside voice-assistant (jcortex)
+- Caddy for TLS; systemd for process supervision
+- Domain: `picortex.globalbr.ai` (Cloudflare A record, DNS-only)
+- Pros: Linux, cheap, full control; cgroups v2 + bubblewrap available
+- Cons: one more server to babysit
+
+## Option B — Fly.io
+
+- Fly app, multi-region or single
+- Persistent volumes for chat home dirs
+- Fly handles TLS
+- Pros: minimal ops
+- Cons: systemd-free (use supercronic), Linux user model is the same, but Fly's per-instance FS is ephemeral unless volume-backed; need to mount volumes for chat homes
+
+## Option C — HMA (Mac Mini)
+
+- Jacob's always-on Mac Mini (`sdmm291`)
+- Already runs many services; public access via Cloudflare Tunnel
+- Pros: no extra server
+- Cons: macOS doesn't have Linux users in the same way; `useradd`/`chmod 0700` model has to use macOS dscl + dseditgroup, more awkward; cgroups v2 unavailable
+
+**Working assumption:** Option A. Decide in D1.
+
+---
+
+## Deploy script (draft, Option A)
+
+```bash
+# On laptop:
+git push origin main
+
+# On Hetzner VPS as root:
+cd /opt/picortex
+sudo -u picortex git pull
+sudo -u picortex npm ci
+sudo -u picortex npm run build
+systemctl restart picortex
+```
+
+## Systemd unit (draft)
+
+```
+[Unit]
+Description=picortex
+After=network.target
+
+[Service]
+User=picortex
+WorkingDirectory=/opt/picortex
+Environment=NODE_ENV=production
+EnvironmentFile=/opt/picortex/.env
+ExecStart=/usr/bin/node dist/server.js
+Restart=on-failure
+RestartSec=5
+
+[Install]
+WantedBy=multi-user.target
+```
+
+## Caddy config (draft)
+
+```
+picortex.globalbr.ai {
+ reverse_proxy 127.0.0.1:7823
+}
+```
+
+## Health checks
+
+- `https://picortex.globalbr.ai/health` returns `{status:"ok"}`
+- Journalctl `journalctl -u picortex -f`
+- `bd list --status=open` for blocker tickets
+
+## Rollback
+
+`git reset --hard <previous-sha> && systemctl restart picortex`
\ No newline at end of file
new file mode 100644
index 0000000..dc5cc17
@@ -0,0 +1,85 @@
+---
+visibility: public
+---
+
+# Spec 001 — Workspace isolation via Linux users
+
+**Status:** Draft
+**Related:** [PRD FR-5..FR-8](../prd/001-picortex-v1.md#workspace-isolation), [ADR-0002](../adrs/0002-linux-users-over-docker.md)
+
+## Goal
+
+Each chat (1:1 or group) has its own Unix user, home directory, and filesystem. No chat can read another chat's files. The picortex backend runs as a dedicated service user and enters each chat's context via `runuser`/`sudo -u`.
+
+## Identifiers
+
+- **Chat ID** — from Linq's `chat.id` (stable for the lifetime of the chat). Stored in SQLite `chats.id`.
+- **Chat user** — Linux username `chat-<n>` where `<n>` is the first 8 hex chars of `sha256(chat_id)`. Collisions detected at creation; collisions get `chat-<n>-1`, `-2` etc.
+- **Home directory** — `$CHAT_WORKSPACE_ROOT/<chat_id>` (default `/srv/picortex/chats/<chat_id>`). Symlinked from `/home/chat-<n>` for standard shell behavior.
+
+## Provisioning
+
+Triggered on first inbound message to an unknown chat. Backend (as root via sudoers) runs:
+
+```bash
+useradd --create-home \
+ --home-dir "$CHAT_WORKSPACE_ROOT/$CHAT_ID" \
+ --shell /bin/bash \
+ --user-group \
+ "chat-$HEX"
+chmod 0700 "$CHAT_WORKSPACE_ROOT/$CHAT_ID"
+chown "chat-$HEX:chat-$HEX" "$CHAT_WORKSPACE_ROOT/$CHAT_ID"
+
+# Seed files
+sudo -u "chat-$HEX" -H bash -c '
+ mkdir -p .picortex/prompts
+ cp /usr/local/share/picortex/discriminator.default.md .picortex/prompts/discriminator.md
+ git init -q
+ git add -A && git commit -q -m "init"
+'
+
+# Per-chat cgroup (cgroups v2)
+mkdir -p "/sys/fs/cgroup/picortex/$CHAT_ID"
+echo "256M" > "/sys/fs/cgroup/picortex/$CHAT_ID/memory.max"
+echo "200" > "/sys/fs/cgroup/picortex/$CHAT_ID/pids.max"
+```
+
+All steps idempotent (check existence first). Failures roll back via a single `userdel -rf`.
+
+## Teardown
+
+Soft: `tmux kill-session -t picortex:$CHAT_ID` + archive home dir to `$CHAT_WORKSPACE_ROOT/_archive/$CHAT_ID.tar.zst` + `userdel -r chat-$HEX`.
+
+Hard: `scripts/destroy-chat.sh <CHAT_ID>` removes user, home, archive, cgroup. Logged to SQLite `events` table.
+
+## Sudoers
+
+A single drop-in at `/etc/sudoers.d/picortex`:
+
+```
+picortex ALL=(%picortex-chats) NOPASSWD: /usr/bin/tmux, /usr/bin/runuser, /usr/bin/bash
+picortex ALL=(root) NOPASSWD: /usr/sbin/useradd, /usr/sbin/userdel, /usr/bin/chown, /usr/bin/chmod, /bin/mkdir
+```
+
+The `picortex-chats` group auto-includes every `chat-*` user (added at `useradd` time).
+
+## Security invariants
+
+1. Backend can read any chat's home (runs as root *via sudoers*, not ambient). Chat users cannot read other chat users' homes (`0700`).
+2. Chat users have no sudo, no passwordless privilege escalation.
+3. Chat users' `PATH` contains `/usr/local/bin/picortex-shims` + system defaults — the shims directory holds allowlisted wrappers (e.g. `claude`, `git`, `rg`, `jq`) and denies everything else via a restrictive `bash --restricted` profile when using the web terminal.
+4. `/tmp` is per-user (via `pam_namespace`).
+5. Network egress is firewalled; see [Spec 008](008-observability.md) for allowlist.
+
+## Testing
+
+- **Unit:** provisioning pure functions (user-name derivation, path composition).
+- **Integration:** spin up, spin down, assert `0700`; assert chat-A can't `cat` chat-B's `~/.picortex/prompts/discriminator.md`.
+- **Stress:** 50 concurrent provisions; no collisions, no orphan users.
+- **Chaos:** kill the backend mid-provision; assert cleanup on next start.
+
+## Open questions
+
+- OQ1: Should we tighten with `bubblewrap` in v1 or wait for D2? — Leaning wait.
+- OQ2: How to handle Linq chat-name changes? (Chat ID is stable, so probably ignore.)
+- OQ3: Per-chat cgroups require cgroups v2; does our deploy target support? — Hetzner does; HMA macOS does not. D1 question.
\ No newline at end of file
new file mode 100644
index 0000000..2140aef
@@ -0,0 +1,78 @@
+---
+visibility: public
+---
+
+# Spec 002 — tmux session spawning
+
+**Status:** Draft
+**Related:** [PRD FR-9..FR-11](../prd/001-picortex-v1.md#sessions), [ADR-0003](../adrs/0003-tmux-for-session-persistence.md)
+
+## Goal
+
+Each chat owns a tmux session running a Claude Code process in its chat-user home dir. Sessions persist across backend restarts, are visible from the web terminal, and have clean reply-capture semantics.
+
+## Session naming
+
+`picortex:<chat_id>` — colon is tmux's session-name separator but also legal. Using full chat_id (not hex) so the name is recognizable when Jacob lists sessions manually.
+
+## Creation
+
+Triggered on first inbound message, after chat-user provisioning (see [Spec 001](001-workspace-isolation-linux-users.md)):
+
+```bash
+sudo -u "chat-$HEX" -H tmux new-session -d -s "picortex:$CHAT_ID" -x 120 -y 40
+sudo -u "chat-$HEX" -H tmux pipe-pane -t "picortex:$CHAT_ID" -o \
+ "cat >> $HOME/.picortex/session.log"
+sudo -u "chat-$HEX" -H tmux send-keys -t "picortex:$CHAT_ID" \
+ "cd ~ && claude --dangerously-skip-permissions" Enter
+```
+
+(`--dangerously-skip-permissions` is tentative — see PRD Q1. Likely OK because the chat user is already sandboxed to `$HOME` and can't escalate.)
+
+## Message dispatch
+
+Per inbound message that passes attention gating:
+
+```
+1. Emit start sentinel: tmux send-keys "<<PICORTEX-TURN-$TURN_ID-START>>" Enter
+2. Emit user text: tmux send-keys <escaped-payload> Enter
+3. Emit end sentinel: tmux send-keys "<<PICORTEX-TURN-$TURN_ID-END>>" Enter
+4. Tail session.log waiting for the end sentinel to appear.
+5. Extract bytes between start and end sentinels; strip ANSI; that's the reply.
+6. POST to Linq (or return to channel abstraction).
+```
+
+The sentinel protocol is robust against Claude Code's streaming output as long as the model doesn't generate sentinels verbatim (probability ~0 in practice; we seed `$TURN_ID` with a UUID so sentinels are unique).
+
+Fallback: if an `end` sentinel doesn't appear within 120 s, send a `claude-stop` keystroke (Ctrl-C) and reply with an apology + request-id.
+
+## Lifecycle
+
+- **Active:** at least one message in the last 7 days.
+- **Idle:** tmux session exists but no message in 7+ days. On next inbound, send a "good morning" to the existing session and keep going.
+- **Hibernated:** after 30 days idle, archive the home dir and `userdel -r`. Next inbound triggers re-provisioning (cold path).
+
+Cron job `scripts/cron/lifecycle.sh` runs hourly. All state changes write to `events` table.
+
+## Warm pool (stretch goal for S6)
+
+Keep N=3 pre-provisioned "unclaimed" chat users with tmux sessions and Claude Code booted. On first inbound for a new chat, rename + assign instead of provisioning from scratch.
+
+Worth it only if cold-start P95 exceeds NFR-1. Skip in v0.1 unless observed.
+
+## Web terminal integration
+
+See [Spec 003](003-web-terminal-xtermjs.md). The WS terminal bridge runs `tmux attach -t picortex:$CHAT_ID` inside `sudo -u chat-$HEX`.
+
+## Testing
+
+- **Unit:** sentinel protocol (mock `tmux capture-pane` outputs).
+- **Integration:** real tmux; send-keys a message; assert reply extraction matches.
+- **Stress:** 50 concurrent chats; latency budget.
+- **Recovery:** kill backend during a turn; restart; assert the session survives and the in-flight turn logs a recovery event.
+
+## Open questions
+
+- OQ1: What if Claude Code crashes inside tmux? (Respawn, log an event, reply "I had a glitch, try again".)
+- OQ2: What if the user sends a message while the previous turn is still running? (Queue. Only one turn at a time per chat.)
+- OQ3: Do we ever want a long-lived streaming reply (partial messages to Linq as it generates)? Probably post-v0.1.
\ No newline at end of file
new file mode 100644
index 0000000..bb0d0cd
@@ -0,0 +1,76 @@
+---
+visibility: public
+---
+
+# Spec 003 — Web terminal (xterm.js)
+
+**Status:** Draft
+**Related:** [PRD FR-12](../prd/001-picortex-v1.md#sessions), [Spec 002](002-tmux-session-spawning.md)
+
+## Goal
+
+Any authenticated user can open a web terminal in a chat and see/control its live tmux session (attached to Claude Code). Works on mobile Safari.
+
+## Architecture
+
+```
+[Browser] [Backend] [Chat user]
+xterm.js <-- WS --> /ws/terminal/:chat_id --> runuser + tmux attach
+ |
+ +--> node-pty
+```
+
+## Endpoint
+
+`GET /ws/terminal/:chat_id` — upgrades to WebSocket. Authorization checked before upgrade:
+
+- Cookie-based Noos OAuth session
+- User must own the chat (v1: always Jacob)
+
+On connection:
+1. Spawn `sudo -u chat-$HEX -H tmux attach -t picortex:$CHAT_ID` under `node-pty`
+2. Wire: stdin from WS text frames → pty; pty stdout → WS binary frames (base64 optional)
+3. Handle `resize` message type from client: `{cols, rows}` → `pty.resize` + `tmux refresh-client -S`
+
+Close path: client disconnect → `pty.kill('SIGHUP')` (this detaches from tmux without killing the session).
+
+## Client
+
+- `@xterm/xterm` v5+
+- Addons: `@xterm/addon-fit`, `@xterm/addon-web-links`
+- Touch keyboard support: a small toolbar with ⌃ / Tab / Esc / ↑ / ↓ buttons for mobile
+
+## Security
+
+- Read-write for v1 (Jacob is the only user).
+- Post-v1: add a `mode=readonly` query param that starts `tmux attach -r`.
+- Do not accept arbitrary commands from WS — it's pure PTY bytes. No JSON/RPC layer.
+- Rate-limit connections to 5/sec per user to prevent tmux-spam DoS.
+- WebSocket path is under the same origin + cookie as the main UI — CSRF safe.
+
+## Resize protocol
+
+Client sends a JSON control frame (distinct from PTY data frames):
+```json
+{"type": "resize", "cols": 120, "rows": 40}
+```
+
+Data frames are plain text (UTF-8) for client→server, binary (Uint8Array) for server→client.
+
+## Mobile considerations
+
+- Font: SF Mono, 13px on mobile
+- Lock horizontal scroll; let tmux handle horizontal overflow
+- Swipe-left from terminal snaps back to file browser pane
+- Copy-on-select with a long-press menu
+
+## Testing
+
+- **Unit:** protocol framing (resize, data, ping).
+- **Integration:** real tmux behind real WS; type `echo hi` and assert output.
+- **E2E:** Playwright on mobile Safari viewport; attach, type, detach, re-attach (see same scrollback).
+
+## Open questions
+
+- OQ1: How to detect "user typed a meaningful command" vs "user is just looking"? (For activity tracking / lifecycle.) Answer: any stdin byte counts.
+- OQ2: Do we want to log every keystroke for audit? Probably no in v1 (privacy). Log session open/close only.
\ No newline at end of file
new file mode 100644
index 0000000..7e3fa19
@@ -0,0 +1,72 @@
+---
+visibility: public
+---
+
+# Spec 004 — File browser
+
+**Status:** Draft
+**Related:** [PRD FR-19](../prd/001-picortex-v1.md#mobile-first-web-ui), [bettergpt mockups](../mockups/README.md)
+
+## Goal
+
+For any chat, show the chat's home directory as a tree. Tapping a file shows its contents (syntax-highlighted for code, rendered for markdown, image-preview for images). Inspiration: Cortex's `mockups/bettergpt/index-split-view-filebrowser.html`.
+
+## Routes
+
+- `GET /api/chats/:chat_id/files/tree?path=<subpath>` — JSON: `[{name, path, type:"dir"|"file", size, mtime}]`
+- `GET /api/chats/:chat_id/files/content?path=<subpath>` — raw bytes; client picks renderer from MIME/extension
+- `POST /api/chats/:chat_id/files?path=<subpath>` — write (later)
+- `DELETE /api/chats/:chat_id/files?path=<subpath>` — delete (later)
+
+Backend runs each as `sudo -u chat-<hex>` → reads the chat user's home. Paths are always relative to home; `..` is rejected via realpath-and-prefix check.
+
+## UI
+
+### Desktop / tablet
+
+Three columns: `[Messages | File tree | File viewer or Terminal]`. Tree + viewer share ~50/50 of the right two-thirds; messages fixed 1/3.
+
+### Mobile
+
+Swipe-tab layout. Tab 2 is the file browser; it splits vertically into tree (top 40%) + viewer (bottom 60%) once a file is selected. Back-gesture returns to messages.
+
+Mockup references in [docs/mockups/README.md](../mockups/README.md).
+
+## Renderers
+
+| Type | Renderer |
+|---|---|
+| Markdown | `react-markdown` with `remark-gfm` |
+| Code | `react-syntax-highlighter` with Prism themes |
+| JSON/YAML | syntax-highlighted, collapsible tree |
+| Image | `<img>` with width 100% |
+| PDF | `<iframe>` or `react-pdf` (post-v1) |
+| Binary | "N bytes, not previewed" + download link |
+
+## Hidden files
+
+Show dotfiles by default (Jacob wants to see `.picortex/prompts/discriminator.md`). Exclude `.git/objects/*` (noise) behind a "show all" toggle.
+
+## Security
+
+- Path check: `realpath(home + path)` must start with `realpath(home)`.
+- Enforce read-size cap (10 MB). Above that, return `413`.
+- Content-type sniff server-side; never echo untrusted `Content-Type` headers from the filesystem.
+- HTML content is returned as `text/plain` unless explicitly requested as rendered markdown.
+
+## Caching
+
+- Tree responses cached with `ETag` (home-dir mtime + size-of-listing hash)
+- File content cached with `ETag` (file mtime + size)
+- Browser cache: `Cache-Control: private, max-age=5`
+
+## Testing
+
+- **Unit:** path-sanitizer rejects `..`, symlinks-out, absolute paths, null bytes.
+- **Integration:** real filesystem; navigate, assert sorted, assert rejected on another chat's home.
+- **E2E:** Playwright on mobile; tap through tree, render markdown.
+
+## Open questions
+
+- OQ1: Do we want write support in v1? (Yes, for editing `.picortex/prompts/discriminator.md` directly.) Post-S7.
+- OQ2: Search within a chat's files? (Defer to v0.2 — the terminal can `rg`.)
\ No newline at end of file
new file mode 100644
index 0000000..794bd85
@@ -0,0 +1,85 @@
+---
+visibility: public
+---
+
+# Spec 005 — Attention gating
+
+**Status:** Draft
+**Related:** [PRD FR-13..FR-15](../prd/001-picortex-v1.md#attention-gating), [Wiki: attention-gating](../wiki/attention-gating.md)
+
+## Goal
+
+In group chats especially, the bot should speak only when wanted. Adopt Cortex R4's five-level ladder.
+
+## Modes
+
+| Mode | Behavior |
+|---|---|
+| `always` | Respond to every message. Default for 1:1 DMs. |
+| `mentions-only` | Respond only if `@picortex` appears or message is a direct reply to a bot message. Default for groups. |
+| `discriminate` | Run LLM classifier on message; respond iff `should_respond == true`. |
+| `discriminate-quiet` | Same as `discriminate` but if `should_respond == false`, also skip reactions/typing indicators. |
+| `silent` | Record only; never respond. Useful for eavesdropping / training data. |
+
+## Rule-first pipeline
+
+Before the LLM discriminator runs, hard rules evaluate in order:
+
+1. **Slash command** (`/picortex <x>`) → always respond (mode-independent).
+2. **Direct reply to a bot message** → always respond.
+3. **`@picortex` mention** → respond.
+4. **Admin override message from Jacob's DM** → respond (even in `silent` mode).
+5. **Non-text payload** (image, voice memo) → depends on mode; `always` responds, others skip.
+
+If no rule fires and mode is `discriminate`, run the LLM classifier. If mode is `mentions-only` and no rule fired, skip.
+
+## LLM discriminator
+
+- Prompt lives at `$CHAT_HOME/.picortex/prompts/discriminator.md`, git-tracked inside the chat's home.
+- Default template seeded on provisioning (from `/usr/local/share/picortex/discriminator.default.md`).
+- Prompt receives: last N=6 messages (user+bot), the new message, chat metadata.
+- Response must be JSON: `{"should_respond": bool, "reason": "..."}`.
+- Model: default `claude-3-5-haiku` via Anthropic API (cheap + fast).
+- Failure: if the model returns invalid JSON or times out (> 5 s), fail-open (respond) for 1:1, fail-closed (skip) for groups.
+
+Cost budget: < $0.001 per classification.
+
+## Configuration UI
+
+In chat settings panel (Spec 007):
+
+- Current mode (radio)
+- "Edit discriminator prompt" → opens file-browser on `.picortex/prompts/discriminator.md`
+- "Test discriminator" → run on recent N messages and show decisions
+- "Version history" → `git log` on the chat's home repo
+
+## Admin commands
+
+Parsed from message body:
+
+- `/picortex attention always|mentions-only|discriminate|discriminate-quiet|silent`
+- `/picortex attention show` — print current mode + prompt path
+- Only accepted from the chat owner (Jacob's phone number).
+
+## Schema
+
+```sql
+CREATE TABLE chat_config (
+ chat_id TEXT PRIMARY KEY,
+ attention_mode TEXT NOT NULL DEFAULT 'mentions-only',
+ discriminator_model TEXT DEFAULT 'claude-3-5-haiku',
+ discriminator_threshold REAL DEFAULT 0.5,
+ updated_at INTEGER NOT NULL
+);
+```
+
+## Testing
+
+- **Unit:** rule pipeline (mention detection, slash command parsing).
+- **Integration:** discriminator prompt harness with golden tests.
+- **E2E:** linq-sim group; toggle modes; assert responses vs non-responses.
+
+## Open questions
+
+- OQ1: How to expose "why did you respond / not respond" to Jacob? (Debug panel showing discriminator reason.)
+- OQ2: Per-user attention overrides inside a group? (v0.2.)
\ No newline at end of file
new file mode 100644
index 0000000..f6ae3c7
@@ -0,0 +1,85 @@
+---
+visibility: public
+---
+
+# Spec 006 — Linq integration
+
+**Status:** Draft
+**Related:** [PRD FR-1..FR-4](../prd/001-picortex-v1.md#linq-integration), [ADR-0004](../adrs/0004-linq-primary-channel.md), [Wiki: linq-protocol](../wiki/linq-protocol.md)
+
+## Goal
+
+Speak fluent Linq, both inbound (webhook ingest) and outbound (partner API), with HMAC signing parity. Testable end-to-end against `linq-sim` without needing real phone numbers.
+
+## Inbound
+
+`POST /api/linq/inbound`
+
+Headers:
+- `Linq-Signature: t=<unix_ts>,s=<hex_hmac_sha256>` (Cortex's exact shape)
+- `Linq-Event-Id: <uuid>`
+- `Content-Type: application/json`
+
+Verification:
+1. Parse `t` and `s` from `Linq-Signature`.
+2. Compute `hmac_sha256("{t}.{raw_body}", LINQ_WEBHOOK_SECRET)`; constant-time compare.
+3. Reject if timestamp skew > 5 min (replay guard).
+4. Dedup via `Linq-Event-Id` seen within 24h.
+5. Log with `X-Request-ID` tagged to event id.
+
+Supported event types (from linq-sim):
+```
+message.received message.delivered message.read message.edited message.failed
+reaction.added reaction.removed
+chat.typing_indicator.started chat.typing_indicator.stopped
+chat.created chat.updated chat.group_name_updated
+participant.added participant.removed
+```
+
+**New for v1:** `message.received` with `data.reply_to_message_id` set (requires [S2 linq-sim PR](../plans/2026-04-23-initial-roadmap.md#s2-linq-sim-thread-support)).
+
+Dispatch table: each event → a handler in `src/channels/linq/handlers/*.ts`. Handlers are pure wrt Linq — they call into the core chat service.
+
+## Outbound
+
+Single client at `src/channels/linq/client.ts`:
+
+```ts
+class LinqClient {
+ sendMessage({ chatId, text, replyToMessageId?, attachments? })
+ createChat({ participants })
+ getChat({ chatId })
+ addParticipant({ chatId, participant })
+ removeParticipant({ chatId, participant })
+ updateChat({ chatId, patch })
+}
+```
+
+Base URL from `LINQ_BASE_URL`. For dev, points at linq-sim (`http://127.0.0.1:8447`). Calls to sim still succeed but are captured for inspection in the sim UI.
+
+Retry: exponential backoff up to 3 attempts on 5xx. 4xx never retried.
+
+## Channel abstraction
+
+```ts
+interface Channel {
+ name: string
+ verifyInbound(req): Promise<ParsedEvent>
+ send(msg: OutboundMessage): Promise<{id: string}>
+ supports(feature: "reactions" | "threads" | "typing"): boolean
+}
+```
+
+`LinqChannel` implements this. Future `OpenChatChannel` ([Wiki: openchat-adapter](../wiki/openchat-adapter.md)) will too.
+
+## Testing
+
+- **Unit:** HMAC signer/verifier, timestamp skew, signature format parsing.
+- **Integration:** linq-sim roundtrip — send via sim's admin UI, picortex handles, responds via sim-captured `/api/partner/v3/sendMessage`.
+- **E2E:** full conversation against linq-sim.
+
+## Open questions
+
+- OQ1: Do we store inbound raw event JSON or normalized? (Store raw + normalized both — Cortex does this.)
+- OQ2: What attachment types must we support at v0.1? (Text-only is fine; images/voice memos are v0.2.)
+- OQ3: Linq's rate limits?
\ No newline at end of file
new file mode 100644
index 0000000..dd2737d
@@ -0,0 +1,95 @@
+---
+visibility: public
+---
+
+# Spec 007 — Mobile-first web UI
+
+**Status:** Draft
+**Related:** [PRD FR-18..FR-21](../prd/001-picortex-v1.md#mobile-first-web-ui), [bettergpt mockups](../mockups/README.md)
+
+## Goal
+
+Jacob on iPhone Safari can manage picortex. Swipe panels between messages, files, and terminal. Desktop gets a three-column split view.
+
+## Stack
+
+- Vite + React 18 + TypeScript
+- Tailwind CSS (mobile-first by convention — base classes target phones; `md:`/`lg:` add up)
+- TanStack Query for server state
+- React Router v7
+
+## Layouts
+
+### Mobile (< 768px)
+
+```
+┌───────────────────────────────┐
+│ [< back] ChatName [⚙] │
+├───────────────────────────────┤
+│ │
+│ (messages | files | term) │
+│ swipe-tabs, one at a time │
+│ │
+├───────────────────────────────┤
+│ [compose box] [send] │
+└───────────────────────────────┘
+```
+
+Swipe-tabs: three panels side-by-side in a horizontally-scrolling container with snap points. Tab bar shows `●○○` indicator at top.
+
+### Tablet / desktop (≥ 768px)
+
+```
+┌──────────────┬──────────┬──────────────┐
+│ Chats │ Messages │ Files │
+│ list │ │ & Terminal │
+│ │ │ split │
+└──────────────┴──────────┴──────────────┘
+```
+
+## Routes
+
+- `/` — chat list (mobile) or three-column (desktop)
+- `/c/:chat_id` — single chat, current panel = messages
+- `/c/:chat_id/files` — files panel
+- `/c/:chat_id/files/*` — specific file
+- `/c/:chat_id/terminal` — terminal panel
+- `/settings/:chat_id` — chat settings (attention, sharing bridge history)
+
+## Messages panel
+
+- Message list virtualized (`@tanstack/react-virtual`)
+- Reply-to pill: shows quoted source with tap-to-jump
+- Reactions stacked under message bubbles
+- Long-press (mobile) / right-click (desktop) → react, reply, copy
+- Compose supports multi-line (Shift+Enter)
+
+## Version + update badge
+
+Footer:
+
+```
+picortex v0.0.1 [●]
+```
+
+Dot badge visible when a newer commit is on main vs running commit. Clicking the footer shows the diff summary and "refresh to get updates" (or triggers a deploy webhook — v0.2).
+
+Version read from `/api/version` (returns `package.json` version + git commit + build time).
+
+## Accessibility
+
+- Tap targets ≥ 44pt
+- Keyboard nav on desktop (j/k to move between messages, Tab cycles panels)
+- Focus rings visible
+- Dark mode via `prefers-color-scheme`
+
+## Testing
+
+- Vitest + Testing Library
+- Storybook for components
+- Playwright for E2E (mobile viewport + desktop viewport)
+
+## Open questions
+
+- OQ1: PWA (installable)? Defer to v0.2 but leave manifest stub.
+- OQ2: Group chat UI on mobile — explicit non-goal for v1. Desktop-only renders.
\ No newline at end of file
new file mode 100644
index 0000000..8dd8834
@@ -0,0 +1,108 @@
+---
+visibility: public
+---
+
+# Spec 008 — Observability
+
+**Status:** Draft
+**Related:** [PRD FR-22..FR-25](../prd/001-picortex-v1.md#observability), Jacob's global [dev-patterns](https://github.com/tmad4000/jacob-computer-config-private)
+
+## Goal
+
+Everything observable without standing up a paid SaaS. Logs useful for the user ("what happened in chat X last Tuesday?") and for the developer ("why did the discriminator skip this message?").
+
+## Structured logs
+
+- **Logger:** `pino` with `pino-pretty` in dev, JSON in prod.
+- **Level:** default `info`; `debug` via `LOG_LEVEL=debug`.
+- **Location:** stdout (journald / docker-less systemd captures); no separate log files.
+- **Fields every log carries:**
+ - `time` (ISO)
+ - `level` (`info`/`warn`/`error`)
+ - `request_id` (see below)
+ - `chat_id` (if in chat context)
+ - `event_type` (e.g. `linq.inbound`, `tmux.turn.start`, `discriminator.decision`)
+ - `msg` (free text)
+
+## Request IDs
+
+- Fastify middleware generates `X-Request-ID` (uuid v7) for every inbound HTTP request.
+- Response headers echo it.
+- Logs in that request's async context include it.
+- Child-process spawns inherit it via env (`PICORTEX_REQUEST_ID`).
+- Linq inbound events tag the request ID into the `events` SQLite row.
+
+## `/api/frontend-log`
+
+Per Jacob's global rules. Client-side:
+
+```ts
+window.addEventListener('error', ev => fetch('/api/frontend-log', {
+ method: 'POST',
+ body: JSON.stringify({
+ level: 'error',
+ message: ev.message,
+ error: ev.error?.toString(),
+ stack: ev.error?.stack,
+ context: { url: location.href, ua: navigator.userAgent, build: __VERSION__ }
+ })
+}))
+```
+
+Server-side endpoint:
+
+- Accepts up to `FRONTEND_LOG_MAX_BYTES` (default 64 KB)
+- Rate-limited to 30/min per IP
+- Logs under `event_type: "frontend"` with the browser-supplied fields plus the request ID tying it to the current user session
+
+## Metrics
+
+No Prometheus in v1. Instead, lightweight counters in SQLite `metrics` table that `/health` exposes:
+
+```
+chats_total
+chats_active_7d
+turns_total
+turns_last_24h
+discriminator_skipped_24h
+errors_last_24h
+```
+
+`/health` returns:
+```json
+{
+ "status": "ok",
+ "version": "0.0.1",
+ "commit": "abcd123",
+ "uptime_seconds": 3412,
+ "db_ok": true,
+ "tmux_ok": true,
+ "metrics": { ... }
+}
+```
+
+## Network egress allowlist
+
+Claude Code chat users should only reach:
+- `api.anthropic.com`
+- `registry.npmjs.org` (for tooling, if used by Claude)
+- `pypi.org` (if Python is used)
+- `github.com`, `raw.githubusercontent.com`
+- Anything the user explicitly allowlists in `/etc/picortex/egress-allowlist.txt`
+
+Enforced via iptables `owner` match on the chat-user's UID. Rejected connections log an event — Jacob gets an alert if a new host is attempted (learning mode).
+
+## Sentry (optional, post-v0.1)
+
+If Jacob wants error aggregation: `@sentry/node` + `@sentry/browser`. Keep it off by default.
+
+## Testing
+
+- **Unit:** request-ID middleware; log shape sanity.
+- **Integration:** frontend-log roundtrip.
+- **Manual:** tail logs during E2E; verify every turn has a request ID.
+
+## Open questions
+
+- OQ1: Where are logs archived long-term? (Not in v1 — stdout + journald is fine.)
+- OQ2: Do we want Axiom or Loki integration? (Not for v1. Cortex uses Axiom.)
\ No newline at end of file
new file mode 100644
index 0000000..b705a25
@@ -0,0 +1,125 @@
+---
+visibility: public
+---
+
+# Architecture
+
+## One-paragraph summary
+
+A chat (1:1 or group) arrives via Linq webhook. picortex looks up or provisions a Unix user + home dir + tmux session for that chat's stable ID. The inbound message is appended to the canonical SQLite log, then — after attention gating — injected into the chat's tmux session via `send-keys`, wrapped in turn sentinels. Claude Code's response is parsed out of the tmux pipe-pane log and sent back via Linq. A mobile-first web UI attaches to the same tmux session through an xterm.js WebSocket bridge and displays a browseable file tree of the chat's home.
+
+## Component diagram
+
+```
+┌────────────────────────────────────────────────────────────────────────┐
+│ Linq (or linq-sim) │
+└────────────┬───────────────────────────────────────┬───────────────────┘
+ │ HMAC-signed webhook │ /api/partner/v3/*
+ ▼ ▲
+ ┌───────────────────────────────────────────────────┴────┐
+ │ picortex backend (Fastify, TS, pino) │
+ │ │
+ │ Channel<Linq> ─▶ Router ─▶ AttentionGate ─▶ │
+ │ │ │ │
+ │ ▼ ▼ │
+ │ SQLite log TurnDispatcher
+ │ │ │ │
+ │ │ ▼ │
+ │ │ runuser -u chat-X │
+ │ │ │ │
+ │ ▼ ▼ │
+ │ ┌─────────────────────────────────┐ │
+ │ │ tmux picortex:<chat_id> │ │
+ │ │ └─ claude (Claude Code) │ │
+ │ │ └─ pipe-pane → session.log │ │
+ │ └─────────────────────────────────┘ │
+ │ ▲ ▲ │
+ │ │ │ │
+ │ ReplyCapture ────────┘ │ │
+ │ │ │ │
+ │ ▼ │ │
+ │ Channel<Linq>.send │ │
+ │ │ │
+ │ /ws/terminal/:chat_id ───────────────┘ node-pty │
+ │ │
+ └─────────────────────────────────────────────────────────┘
+ ▲ ▲
+ │ HTTP + WS │ HTTP (files API)
+ │ │
+ ┌───────────────────────────────────────────────────┐ ┌──────┐
+ │ Frontend (Vite + React, :7824) │ │ ... │
+ │ swipe-panels: messages │ files │ terminal │ └──────┘
+ └───────────────────────────────────────────────────┘
+```
+
+## Invariants
+
+- **I1 Canonical log:** the SQLite `messages` table is authoritative. The workspace FS is a cache.
+- **I2 Per-chat user:** no chat's bytes ever reach another chat's process, enforced by POSIX perms.
+- **I3 Sentinel protocol:** every Claude turn is bracketed in tmux by unique start/end sentinels so reply capture is unambiguous.
+- **I4 Backend-as-root:** the backend can enter any chat user via sudoers; chat users have no sudo.
+- **I5 Channel interface:** business logic does not know about Linq, only about the `Channel` interface.
+- **I6 HMAC on inbound:** no unsigned webhook is processed. Full stop.
+
+## Data model (SQLite)
+
+```sql
+CREATE TABLE chats (
+ id TEXT PRIMARY KEY, -- from Linq
+ kind TEXT NOT NULL, -- '1on1' | 'group'
+ display_name TEXT,
+ owner_phone TEXT NOT NULL,
+ unix_user TEXT UNIQUE NOT NULL,
+ home_dir TEXT NOT NULL,
+ created_at INTEGER NOT NULL,
+ last_message_at INTEGER
+);
+
+CREATE TABLE messages (
+ id TEXT PRIMARY KEY,
+ chat_id TEXT NOT NULL REFERENCES chats(id),
+ direction TEXT NOT NULL, -- 'inbound' | 'outbound'
+ author TEXT, -- phone or bot
+ text TEXT,
+ reply_to_message_id TEXT,
+ turn_id TEXT, -- present on outbound, ties to tmux turn
+ request_id TEXT NOT NULL,
+ raw_event JSON,
+ created_at INTEGER NOT NULL
+);
+
+CREATE TABLE events (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ kind TEXT NOT NULL,
+ chat_id TEXT,
+ request_id TEXT,
+ payload JSON,
+ created_at INTEGER NOT NULL
+);
+
+CREATE TABLE bridge_events (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ src_chat_id TEXT NOT NULL,
+ dst_chat_id TEXT NOT NULL,
+ path TEXT NOT NULL,
+ sha256 TEXT NOT NULL,
+ approver_phone TEXT NOT NULL,
+ challenge_message_id TEXT NOT NULL,
+ approval_message_id TEXT NOT NULL,
+ created_at INTEGER NOT NULL
+);
+
+CREATE TABLE chat_config (
+ chat_id TEXT PRIMARY KEY REFERENCES chats(id),
+ attention_mode TEXT NOT NULL DEFAULT 'mentions-only',
+ discriminator_model TEXT,
+ discriminator_threshold REAL,
+ updated_at INTEGER NOT NULL
+);
+```
+
+## Runtime
+
+- **picortex service:** systemd unit `picortex.service`, starts Fastify + Vite-built static frontend.
+- **tmux per chat:** spawned on demand under the chat user.
+- **cron:** `picortex-lifecycle.timer` runs hourly for idle reap + archive.
\ No newline at end of file
new file mode 100644
index 0000000..40aff1d
@@ -0,0 +1,75 @@
+---
+visibility: public
+---
+
+# Attention gating
+
+## Why
+
+In a group chat with four humans and a bot, the bot should not respond every 3 seconds. But it should respond when the conversation is about a bug it can help with, even if no one @mentioned it. That's the "discriminator" lane.
+
+## The ladder
+
+```
+ silent ──▶ discriminate-quiet ──▶ discriminate ──▶ mentions-only ──▶ always
+ (record) (LLM) (LLM) (strict match) (no filter)
+```
+
+Going right = more responsive. Going left = quieter.
+
+## Rule before LLM
+
+The LLM discriminator is slow and costs money. Hard rules short-circuit:
+
+1. `/picortex <cmd>` slash command → always respond
+2. Reply-to a bot message → respond
+3. `@picortex` mention → respond
+4. Admin override from Jacob's DM → respond
+5. Non-text payload → mode-dependent
+
+## Discriminator prompt
+
+Lives at `$CHAT_HOME/.picortex/prompts/discriminator.md`, git-tracked inside the chat's home. Seeded from a default that can be tuned live.
+
+Default (sketched):
+
+> You are deciding whether a chat bot should respond to the most recent message. Only respond if the message:
+> - asks a question that a capable coding / research assistant could help with, OR
+> - is reacting to a previous message from you, OR
+> - contains a task/request that the assistant should pick up.
+>
+> Do NOT respond if the message is:
+> - small talk between humans
+> - private conversation you weren't addressed in
+> - off-topic or tangential
+>
+> Return JSON: `{"should_respond": bool, "reason": "short"}`.
+
+Prompt is git-versioned so Jacob can tune per-chat without redeploying.
+
+## Model choice
+
+Default: `claude-3-5-haiku` (cheap, fast). Swap via `chat_config.discriminator_model`.
+
+Cost: ~200 input tokens + 30 output tokens per classification ≈ $0.0005. 1,000 classifications = $0.50. Cheap.
+
+## Failure modes
+
+- LLM returns non-JSON → 1 retry with reformat prompt; if still bad, fail-open for 1:1, fail-closed for groups.
+- LLM times out (> 5 s) → same fail policy.
+- Cost spike → per-day per-chat budget cap in `chat_config`. Hitting the cap disables `discriminate` until midnight.
+
+## Observability
+
+Every discriminator decision is logged as an `event_type: "discriminator.decision"` with: `chat_id`, `message_id`, `should_respond`, `reason`, `model`, `latency_ms`, `cost_usd`.
+
+## Future
+
+- Personalized discriminator (fine-tune or few-shot on Jacob's own "respond / don't respond" label history). v0.3.
+- Hierarchical gating: per-user overrides inside a group. v0.2.
+
+## References
+
+- [Spec 005](../specs/005-attention-gating.md)
+- Cortex R4 — the pattern we adopted
+- Default template: `/usr/local/share/picortex/discriminator.default.md` (created by installer in S4)
\ No newline at end of file
new file mode 100644
index 0000000..e772f03
@@ -0,0 +1,132 @@
+---
+visibility: public
+---
+
+# Cortex inheritance map
+
+**Pinned to:** `IdeaFlowCo/cortex @ e93bf8c` (2026-04-23, GitHub latest as of verification run)
+**Previous pin:** `e55f129` (2026-04-23 earlier) — superseded. Delta since: `e93bf8c` docs(plan): OpenClaw gateway lifecycle alignment design. Non-breaking for picortex inheritance.
+
+## Research-ingestion cutoff — revised 2026-04-23
+
+**Old rule:** "Ignore Cortex commits before `af3a76f5`."
+**New rule:** **Don't *inherit* patterns from pre-`af3a76f5` Cortex, but *do* study that era deliberately as an alternative-prototype source.**
+
+The pre-container "Piyush-era" window (`238052c4` → `d2d6a534`, 2026-01-20 → 2026-01-23) is 9 commits of EC2 + SSH + Vercel + per-user Linux workspace with `claude -c -p` per turn. Tejas replaced it wholesale starting `af3a76f5` because Cortex-as-product needed stronger per-tenant isolation. But **for picortex-as-a-personal-tool that separation was unnecessary, and several Piyush-era patterns are exactly what we want** — especially the bot/workspace physical split and the `claude -c -p`-per-turn execution model.
+
+See [`piyush-era-design.md`](piyush-era-design.md) for the full study and [`docs/plans/2026-04-23-prototype-options.md`](../plans/2026-04-23-prototype-options.md) for how it feeds into Option 2 of the current option bundle.
+
+### Operational guidelines
+
+| Activity | Rule |
+|---|---|
+| Quarterly Cortex diff-review | Start at `af3a76f5` (still) — these reviews track the Cortex-as-product direction, which is post-container. |
+| Inheriting patterns (R-numbers) | Start at `af3a76f5` — R-numbers are Tejas-era work. |
+| **Studying alternative prototypes** | **Read the Piyush era.** It's a known-good design for the simpler single-user case. |
+| Any new file grep | If it touches `backend/src/services/claudeService.ts` or similar pre-container paths, read `piyush-era-design.md` first to know what you're looking at. |
+
+### Verification queries
+
+```bash
+# Post-container patterns to inherit:
+git -C ~/code/cortex log af3a76f5..HEAD --oneline
+
+# Pre-container prototype study (deliberate):
+git -C ~/code/cortex log 238052c4..d2d6a534 --oneline
+git -C ~/code/cortex show d2d6a534 --stat
+```
+**Primary source:** `~/code/cortex/docs/future-plans/texting-bot-groups/requirements.md`
+**Legend:** adopt • adapt • reject • defer
+
+When Cortex updates R-numbers or adds new Rs, this map is re-reviewed at the quarterly checkpoint. Adoption decisions cite specific picortex files where the implementation lives (or will live).
+
+## R1 — Bot as primary surface
+
+| # | Cortex text (paraphrase) | picortex decision | Notes |
+|---|---|---|---|
+| R1.1 | Bot is the main UI; web is supplementary | **adopt** | |
+| R1.2 | DM with bot = personal agent | **adopt** | iMessage 1:1 = picortex personal chat |
+| R1.3 | Group with bot = group agent | **adopt** | iMessage group with bot = group chat |
+
+## R2 — Workspace identity
+
+| # | Cortex | picortex | Notes |
+|---|---|---|---|
+| R2.1 | Each chat has own filesystem keyed on durable chat ID | **adapt** | Same invariant; Linux user instead of Docker container. [ADR-0002](../adrs/0002-linux-users-over-docker.md) |
+| R2.2 | Workspace survives restart | **adopt** | home dir persists |
+
+## R3 — Lifecycle
+
+| # | Cortex | picortex | Notes |
+|---|---|---|---|
+| R3.1 | Eager provisioning + warm pool | **adapt** | Eager yes; warm pool deferred to S6 |
+| R3.2 | Idle hibernation | **adopt** | 7 days |
+| R3.3 | 7-day destroy-but-keep-volume | **adapt** | 30-day archive-then-delete |
+
+## R4 — Attention gating
+
+| # | Cortex | picortex | Notes |
+|---|---|---|---|
+| R4.1 | Modes: always / mentions-only / discriminate / discriminate-quiet / silent | **adopt** | [Spec 005](../specs/005-attention-gating.md) |
+| R4.2 | Rules-first then LLM | **adopt** | |
+| R4.3 | Discriminator prompt is git-versioned `.cortex/prompts/discriminator.md` | **adapt** | Rename to `.picortex/` |
+
+## R5 — Backend authority
+
+| # | Cortex | picortex | Notes |
+|---|---|---|---|
+| R5.1 | Backend = canonical log; container cache only | **adopt** | SQLite instead of Cortex's DB |
+| R5.2 | Container can't authorize | **adopt** | |
+| R5.3 | Message edits / deletes are backend ops | **adopt** | |
+| R5.4 | Cross-chat ops need out-of-band challenge | **adopt** | |
+| R5.5 | Challenge is a DM reply | **adopt** | [Spec 009](../specs/) pending |
+| R5.6 | Never trust workspace-declared user identity | **adopt** | |
+
+## R6 — MCP cross-chat tools
+
+| # | Cortex | picortex | Notes |
+|---|---|---|---|
+| R6.* | `listMyChats`, `readChatTranscript`, `searchChat`, etc. | **defer** | Not in v0.1 |
+
+## R7 — Sharing bridge
+
+| # | Cortex | picortex | Notes |
+|---|---|---|---|
+| R7.1 | Personal→shared v1 | **adopt** | [Stage S9](../plans/2026-04-23-initial-roadmap.md#s9) |
+| R7.2 | Shared→personal v2 | **defer** | |
+| R7.3 | Every op = BridgeEvent row | **adopt** | |
+
+## R8 — Identity
+
+| # | Cortex | picortex | Notes |
+|---|---|---|---|
+| R8.1 | Phone as primary; SMS OTP login | **adapt** | Jacob's phone is the only admin; no OTP yet |
+| R8.2 | Email optional | **defer** | |
+| R8.3 | Per-chat scoped tokens | **adopt** | Noos OAuth scopes for web UI |
+| R8.4 | N groups = N tokens | **adopt** | |
+
+## R9 — Linq channel
+
+| # | Cortex | picortex | Notes |
+|---|---|---|---|
+| R9.1 | HMAC-SHA256 `{t}.{body}` on inbound | **adopt** | Identical |
+| R9.2 | 5-min skew, replay guard | **adopt** | |
+| R9.3 | Normalize Linq → internal event shape | **adopt** | |
+| R9.4 | Outbound retry with idempotency | **adopt** | |
+
+## R10–R19 — other
+
+Covered in the Cortex docs but not crystallized here. When picortex implements a subsystem, the PR adds a row above.
+
+## Divergences (major)
+
+| Picortex choice | Cortex equivalent | Why |
+|---|---|---|
+| Linux user per chat | Docker container per chat | Spin-up latency, resource cost |
+| SQLite | Postgres | Single-user simplicity |
+| No dashboard, just per-chat web terminal | Session management dashboard | Explicit user directive |
+| Mobile-first UI from day 1 | Desktop-first, mobile responsive | Jacob's use case is phone-led |
+
+## Upstream contributions
+
+- linq-sim thread/reply support (Stage S2) — picortex improves the shared simulator; PR goes back to IdeaFlowCo/cortex.
\ No newline at end of file
new file mode 100644
index 0000000..70519ef
@@ -0,0 +1,55 @@
+---
+visibility: public
+---
+
+# picortex LLM Wiki — Index
+
+This wiki is maintained primarily by coding agents (Claude Code, Codex CLI). It is a graph of cross-linked concept pages — not a linear narrative. When a new source is ingested, the responsible agent updates affected pages and appends an entry to [`log.md`](log.md).
+
+Style: Karpathy-ish. Pages are short, focused, opinionated, cross-referenced. When a claim should link to a file, it links to a file.
+
+## Categories
+
+### Core concepts
+
+- [architecture.md](architecture.md) — system overview
+- [workspace-isolation.md](workspace-isolation.md) — why Linux users; what POSIX guarantees
+- [attention-gating.md](attention-gating.md) — the 5-level ladder, discriminator prompt
+- [linq-protocol.md](linq-protocol.md) — what Linq's events look like
+
+### Inheritance & positioning
+
+- [cortex-inheritance.md](cortex-inheritance.md) — R1-R19 adopt/adapt/reject/defer map
+- [piyush-era-design.md](piyush-era-design.md) — study of pre-container Cortex; source of Option 2
+- [relationship-to-noos.md](relationship-to-noos.md) — what picortex shares with noos, and what it doesn't
+- [openchat-adapter.md](openchat-adapter.md) — future third channel
+- [repo-versions.md](repo-versions.md) — GitHub latest-sha pins for every referenced repo
+
+### Ops
+
+- [observability-patterns.md](observability-patterns.md) — placeholder; populated when implementation starts
+
+## Maintenance protocol
+
+When a coding agent completes a significant unit of work:
+
+1. Update the affected wiki pages (no more than necessary).
+2. Append to [`log.md`](log.md) with format:
+ ```
+ ## [YYYY-MM-DD] <kind> | <short description>
+ - what changed
+ - which pages updated
+ - beads ticket(s)
+ ```
+3. Don't delete pages — mark them deprecated with a header banner and a pointer to the replacement.
+
+## Search
+
+Until a real search exists: `rg -n` against `docs/wiki/`.
+
+## Orphan / lint policy
+
+Quarterly, an agent runs:
+- Find pages with no inbound links (orphans) — decide: keep as reference, link from relevant pages, or deprecate.
+- Find broken cross-references — fix.
+- Find contradictions — flag for human review.
\ No newline at end of file
new file mode 100644
index 0000000..5866d9e
@@ -0,0 +1,100 @@
+---
+visibility: public
+---
+
+# Linq protocol (as observed)
+
+Based on **linq-sim** (`~/code/cortex/cloudcli/dev-tools/linq-sim/`) and Cortex's `backend/src/routes/sms.ts` normalization layer. This is our ground truth until Jacob gets real Linq partner docs.
+
+## Inbound webhook
+
+`POST $OUR_BASE/api/linq/inbound`
+
+### Headers
+- `Content-Type: application/json`
+- `Linq-Signature: t=<unix_ts>,s=<hex_hmac_sha256>`
+- `Linq-Event-Id: <uuid>` (dedup key)
+
+### Body shape (union of all events)
+
+```json
+{
+ "type": "message.received",
+ "id": "evt_...",
+ "timestamp": 1714065000,
+ "chat_id": "chat_abc",
+ "data": {
+ "message_id": "msg_...",
+ "sender": { "phone": "+15551234567", "name": "..." },
+ "text": "...",
+ "reply_to_message_id": "msg_parent", // NEW — requires S2 sim PR
+ "attachments": []
+ }
+}
+```
+
+### Supported event types
+
+```
+message.received
+message.delivered
+message.read
+message.edited
+message.failed
+reaction.added
+reaction.removed
+chat.typing_indicator.started
+chat.typing_indicator.stopped
+chat.created
+chat.updated
+chat.group_name_updated
+participant.added
+participant.removed
+```
+
+## HMAC verification
+
+```
+signed_payload = f"{timestamp}.{raw_body_bytes}"
+expected_sig = hmac_sha256(signed_payload, secret)
+```
+
+Constant-time compare `expected_sig` vs `s=` value.
+
+Reject if:
+- Signature header missing / malformed → 401
+- `expected_sig` mismatch → 401
+- `now - timestamp > 300` → 401 (skew)
+- `Linq-Event-Id` seen in last 24h → 200 (dedup no-op)
+
+## Outbound partner API
+
+`$LINQ_BASE_URL/api/partner/v3/...`
+
+- `POST /sendMessage` — `{ chat_id, text, reply_to_message_id?, attachments? }`
+- `POST /createChat` — `{ participants: [phone, ...] }`
+- `GET /getChat?chat_id=...`
+- `POST /addParticipant` / `removeParticipant` / `updateChat`
+
+Auth: `Authorization: Bearer $LINQ_API_KEY` (assumed — confirm with real Linq docs).
+
+## Reactions
+
+Set: `{heart, thumbs_up, thumbs_down, laugh, exclaim, question}` (iMessage's native tapbacks). `target_message_id` required on `reaction.added`. linq-sim enforces 400 when missing.
+
+## Threads / replies
+
+iMessage has inline replies (`reply_to_message_id`). linq-sim does NOT currently implement this; [S2](../plans/2026-04-23-initial-roadmap.md#s2-linq-sim-thread-support) adds it. picortex requires it before S7 mobile UI can render reply pills properly.
+
+## Things we don't know yet
+
+- Rate limits on outbound `sendMessage`
+- Attachment size caps
+- Whether Linq preserves read receipts across edits
+- Whether `chat.created` fires for a 1:1 on first message or only on explicit creation
+
+## Related
+
+- [Spec 006 — Linq integration](../specs/006-linq-integration.md)
+- [ADR-0004 — Linq as primary channel](../adrs/0004-linq-primary-channel.md)
+- Cortex: `~/code/cortex/docs/future-plans/2026-03-31-linq-channel-plugin.md`
\ No newline at end of file
new file mode 100644
index 0000000..eb2b50f
@@ -0,0 +1,57 @@
+---
+visibility: public
+---
+
+# picortex wiki log
+
+Append-only chronological log of wiki updates. Format:
+
+```
+## [YYYY-MM-DD] <kind> | <short description>
+```
+
+Kinds: `ingest` (new source read), `update` (page edited), `create` (new page), `deprecate`, `lint`.
+
+---
+
+## [2026-04-23] create | Wiki bootstrap
+
+- Created `index.md`, `architecture.md`, `workspace-isolation.md`, `attention-gating.md`, `linq-protocol.md`, `cortex-inheritance.md`, `openchat-adapter.md`, `relationship-to-noos.md`.
+- Sources ingested:
+ - `~/code/cortex/docs/future-plans/texting-bot-groups/*` (plans R1-R19)
+ - `~/code/noos/src/slack/` (existing Slack bot)
+ - `~/code/cortex/cloudcli/dev-tools/linq-sim/` (simulator)
+ - `~/memory/research/openclaw-group-chat-security.md`, `openclaw-security-audit-2026-02-20.md` (prior isolation research)
+ - `~/memory/research/documentation-best-practices-2026-04-23.md`
+- Beads: picortex-S0-1 (plan docs), picortex-S0-2 (LLM wiki).
+
+## [2026-04-23] study | Piyush-era Cortex design + prototype options
+
+- Created `piyush-era-design.md` (full study of pre-container Cortex at `d2d6a534`).
+- Created `docs/plans/2026-04-23-prototype-options.md` — 6 prototype options (Options 1-6) along a 6-axis decision space (physical layout, workspace granularity, agent executor, session memory, knowledge layer, dev surface).
+- Softened the pre-container cutoff rule: was "ignore pre-`af3a76f5`"; now "don't *inherit* pre-container patterns, but *do* study that era — several of its patterns (bot/workspace split, `claude -c -p` per turn) are exactly what the texting-first picortex wants."
+- Filed 9 ordered brainstorm tickets `picortex-Q0` → `picortex-Q8` to drive prototype-option selection.
+
+## [2026-04-23] rule | Research-ingestion cutoff for Cortex (superseded by next entry)
+
+- Added guardrail to `cortex-inheritance.md` and `AGENTS.md`: do NOT ingest Cortex commits before `af3a76f5` (2026-01-26, Tejas DC) — the Piyush-era EC2/SSH/Vercel architecture (9 commits, Jan 20-23) predates containerized workspaces.
+- Verified current picortex research was clean: the initial research agent used `--since="1 week ago"` and never touched pre-cutoff history.
+- **Rule softened same-day** — see entry above.
+
+## [2026-04-23] verify | GitHub latest-version sweep of all referenced repos
+
+- `IdeaFlowCo/cortex` — `e93bf8c` (2026-04-23) — latest
+- `IdeaFlowCo/noos` — private, 404 — confirmed via local `~/code/noos` remote (NOT `tmad4000/noos` as initially assumed)
+- `tmad4000/openchat` / `tmad4000/OpenChat` — last push **2023-06-21** (stale). The deployed openchat at `chat.globalbr.ai` is `name: "openchat" v0.1.0` with **no git remote configured** on the Lightsail server. The actual source repo for the live service is currently unaccounted-for — flagged for Jacob as open question.
+- `tmad4000/voice-assistant` — `89870e1` (2026-02-11) — stable
+- `tmad4000/listhub` — `9ec8dbf` (2026-04-22) — active
+- `tmad4000/Thoughtstreams` — `e3d447e` (2026-01-26) — stable
+- `tmad4000/vibe-coding-guide` — `4b4d644` (2026-04-22) — active
+- `tmad4000/tmad4000.github.io` — `5d4a5e2` (2026-04-18) — stable
+- Affected wiki pages: `relationship-to-noos.md` (fix noos URL), `openchat-adapter.md` (add deployed-source unknown caveat), `cortex-inheritance.md` (pin bump).
+
+## [2026-04-23] ingest | Cortex commits e93bf8c (2026-04-23) back through ~Apr 16
+
+- Extracted: J1-J11 plan, R1-R19 requirements, linq-sim event vocabulary, bettergpt mockup references, stage-mocks naming, attention discriminator prompt idea.
+- Cortex pins reference: `IdeaFlowCo/cortex @ e93bf8c` (2026-04-23; verified via `gh api repos/IdeaFlowCo/cortex/commits`).
+- Affected pages: `cortex-inheritance.md`, `architecture.md`, `attention-gating.md`, `linq-protocol.md`.
\ No newline at end of file
new file mode 100644
index 0000000..4e72a37
@@ -0,0 +1,87 @@
+---
+visibility: public
+---
+
+# OpenChat adapter (candidate third channel)
+
+**Status:** Deferred — [Stage D3](../plans/2026-04-23-initial-roadmap.md#d3-openchat-linq-adapter).
+
+## Premise
+
+picortex's `Channel` interface is shaped to Linq. If we teach OpenChat (`chat.globalbr.ai`) to speak the same dialect — inbound HMAC-signed webhooks, outbound `/api/partner/v3/*` surface, same 14 event types — then picortex gets a **third** channel "for free":
+
+1. Real Linq (iMessage, SMS)
+2. linq-sim (dev)
+3. **OpenChat-as-Linq** (Jacob's own app)
+
+The payoff: Jacob can test picortex end-to-end through OpenChat without needing Linq, and the OpenChat UI becomes a real client for picortex.
+
+## Current OpenChat surface (from research)
+
+- `/api/chat/conversations` (list / create / get / list-messages / send-message)
+- `/api/chat/contacts`
+- `/api/chat/users/by-email/:email`
+- `/api/chat/presence`
+- WebSocket at `/ws/chat` for real-time delivery
+- ~**40% overlap** with Linq's surface
+
+## Gap analysis
+
+| Linq feature | OpenChat today | Work |
+|---|---|---|
+| reactions | none | new schema + 2 routes + WS event + UI |
+| message edits | none | new route + WS event + UI state |
+| delivery / read receipts | partial (`presence`) | add message-status transitions |
+| typing indicator | none | WS event + throttle |
+| thread replies (`reply_to_message_id`) | none | schema + UI affordance |
+| participant add/remove after creation | none | new routes |
+| group rename event | none | new route |
+| HMAC webhook signing | none | new outbound emitter w/ retry + dedup |
+| outbound partner API (`/api/partner/v3/*`) | none | new adapter layer |
+
+## Two strategies
+
+### A. Adapter layer (recommended)
+
+Leave OpenChat's own API alone. Add `src/adapter/linq/` that translates:
+- OpenChat internal events → outbound HMAC-signed `/api/partner/v3/*` webhooks
+- Inbound `/api/partner/v3/sendMessage` calls → OpenChat's `postMessage` internal call
+
+**Pro:** OpenChat keeps its own identity. Adapter is isolated and replaceable.
+**Con:** Some behaviors (reactions, edits) don't exist in OpenChat yet — those are *real* features to add.
+
+### B. Native upgrade
+
+Rebuild OpenChat routes to match Linq names; make Linq the primary internal shape.
+
+**Pro:** Simpler long-term.
+**Con:** Disruptive; breaks existing OpenChat clients; not in scope for picortex.
+
+**Decision:** adapter layer. Even with adapter, the missing features (reactions, edits, typing, threads) must still be built. But the naming stays OpenChat-native.
+
+## Rough effort
+
+- Reactions (schema + routes + WS + UI): ~1 day (already planned in `OpenChat-yg8`-adjacent work)
+- Outbound HMAC webhook emitter + retry + dedup: ~2 days
+- Edits + read receipts + typing: ~2-3 days
+- Thread replies: ~1 day
+- `/api/partner/v3/*` adapter routes: ~1-2 days
+- End-to-end tests against linq-sim shape: ~1-2 days
+- **Total: 1-2 weeks focused work**
+
+## Decision gate for picortex v0.1
+
+**Defer.** v0.1 ships with real Linq + linq-sim. OpenChat adapter lives in `~/code/openchat` on its own timeline. picortex consumes it once available.
+
+## Repo status (2026-04-23)
+
+- `tmad4000/openchat` and `tmad4000/OpenChat` on GitHub are **stale Next.js scaffolds from June 2023** — not the live service.
+- Deployed openchat at `chat.globalbr.ai` on Lightsail is `name: "openchat" v0.1.0, workspaces: [server, client]` but has **no git remote configured** (`/opt/openchat/.git/config`).
+- **Open question for Jacob:** where does the deployed openchat's source live? Possibilities: (a) unpublished private repo; (b) only exists on the Lightsail server + Jacob's M3 laptop; (c) different GitHub name we haven't found. This must be resolved before D3 can start.
+- Until resolved, D3 work is blocked on repo discovery. Candidate action: `cd ~/code/openchat` on M3 laptop and `git remote -v`.
+
+## Related
+
+- [ADR-0004 — Linq as primary channel](../adrs/0004-linq-primary-channel.md)
+- Research: second agent's openchat report (this session)
+- Beads: `picortex-D3-*` (created when D3 kicks off)
\ No newline at end of file
new file mode 100644
index 0000000..b8eaeac
@@ -0,0 +1,157 @@
+---
+visibility: public
+---
+
+# Piyush-era Cortex design (2026-01-20 → 2026-01-23)
+
+A study, not an inheritance source. Nine commits under **Piyush Jha** that preceded Tejas's containerized rewrite. Worth reading in full because the shape is closer to what picortex-as-a-personal-tool wants than the current Cortex is.
+
+**Final SHA:** `d2d6a534` (2026-01-23 03:45 PST). Replaced wholesale starting `af3a76f5` (Tejas, 2026-01-26).
+
+## Architecture
+
+```
+┌───────────────────────────────────────────────┐
+│ Frontend (Vercel) │
+│ React 19 + Vite + Socket.IO + xterm.js │
+│ Auth │ File Browser │ Chat │ Terminal │
+└──────────────┬────────────────────────────────┘
+ │ REST + WebSocket
+┌──────────────┴────────────────────────────────┐
+│ Backend (separate server) │
+│ Node.js + Express + Prisma + Socket.IO │
+│ • auth • files • claudeService • ssh │
+└──────────────┬────────────────────────────────┘
+ │ SSH (ed25519, keyed)
+┌──────────────┴────────────────────────────────┐
+│ EC2 workspace host (single shared box) │
+│ cortex_admin service user │
+│ └─ sudoers drop-in: useradd/userdel/chmod │
+│ cortex_user_<id> per signup │
+│ ├─ ~/workspace/ │
+│ ├─ ~/.claude/ (API key via apiKeyHelper) │
+│ └─ claude CLI pre-installed │
+└───────────────────────────────────────────────┘
+```
+
+Three machines, three roles. **This separation is what picortex has been missing** — Jacob's question "could the workspace just live on a remote box?" is the Piyush-era answer.
+
+## The turn loop (the important bit)
+
+`backend/src/websocket/handlers/chatHandler.ts` + `services/claudeService.ts`:
+
+```ts
+// Per inbound message:
+socket.on('chat:message', async ({ content }) => {
+ const workspace = await prisma.workspace.findUnique({ where: { userId } });
+ const username = workspace.linuxUsername;
+
+ await prisma.message.create({ data: { userId, role: 'user', content } });
+
+ // The actual Claude call:
+ const cmd = `claude -c --dangerously-skip-permissions -p "${escaped}"`;
+ const result = await sshService.execAsUser(username, cmd);
+
+ await prisma.message.create({ data: { userId, role: 'assistant', content: result.stdout } });
+ socket.emit('chat:response', { content: result.stdout });
+});
+```
+
+No tmux. No sentinel protocol. No long-lived REPL to babysit. Each turn:
+
+1. Backend picks the chat's Linux username from Prisma.
+2. Backend opens an SSH session as that user on the EC2 host.
+3. Runs `claude -c --dangerously-skip-permissions -p "<prompt>"`.
+4. `-c` is Claude CLI's **"continue the previous conversation"** flag — session memory is managed by Claude itself in `~/.claude/`, not by the backend.
+5. stdout is the reply. Close SSH. Done.
+
+This is the design codex told picortex to try (the `claude --print` spike — `picortex-b92`, `picortex-3vj`). Piyush already ran the experiment and it worked.
+
+## What else Piyush built
+
+From `backend/src/` at `d2d6a534`:
+
+- **`services/sshService.ts`** — pooled SSH connections to the workspace host, one keyed connection per chat-user. Mock mode for dev.
+- **`services/workspaceService.ts`** — 4-step provisioning with WebSocket progress events: `create_user` → `install_cli` → `configure_api` → `complete`. Each user gets their Anthropic key stored encrypted server-side; written to `~/.claude/config.json` via `apiKeyHelper`.
+- **`services/fileService.ts`** / **`controllers/fileController.ts`** — read/list/write over SSH.
+- **`services/voiceService.ts`** — OpenAI-backed voice chat.
+- **`websocket/handlers/terminalHandler.ts`** — xterm.js ↔ SSH PTY bridge.
+- **`websocket/handlers/provisioningHandler.ts`** — emits provisioning progress.
+- **`routes/authRoutes.ts`** + **`middleware/auth.ts`** — JWT auth with user-supplied Anthropic API key at signup.
+- **`prisma/schema.prisma`** — `User`, `Workspace`, `Message`. SQLite-first.
+
+Frontend `frontend/src/components/`:
+
+- `chat/{ChatContainer, MessageInput, MessageList, VoiceButton}.tsx`
+- `files/{FileBrowser, FilePreview, FileTree}.tsx`
+- `terminal/WebTerminal.tsx`
+- `provisioning/ProvisioningScreen.tsx`
+- `auth/{LoginForm, SignupForm, ProtectedRoute}.tsx`
+- `sidebar/Sidebar.tsx` (chat list)
+
+## What Piyush got right that picortex should steal
+
+1. **Three-tier physical layout.** Frontend (Vercel) ↔ backend (whatever) ↔ workspace host (separate). The bot box and the workspace box are different machines. This is the natural home for Jacob's "remote box for privacy" instinct.
+2. **`claude -c -p` per turn, no tmux.** Session continuity is Claude's own feature. Replace the sentinel-protocol design in `spec/002-tmux-session-spawning.md` with this. Simpler, survives backend restarts, fewer moving parts.
+3. **Per-user provisioning with real-time progress events.** `provisioning:status` WebSocket events with `{step, progress, message}` is a nice UX pattern; picortex's mobile UI should mirror it for cold-start chats.
+4. **`apiKeyHelper` via Claude CLI config.** Lets the backend inject/rotate keys without writing them into workspace files. Decoupled secret management.
+5. **Mock mode for SSH.** The `sshService.isMockMode()` check lets the whole frontend+chat flow run without real EC2. picortex needs the same — linq-sim for the Linq side, an `sshMock` or local-exec fallback for the workspace side.
+6. **Backend's sudoers are scoped to user-management binaries only**: `useradd`, `userdel`, `chown`, `chmod`, `mkdir`, `sudo -u *`. No `bash`, no `tmux`. That's tighter than picortex's current spec and closer to what codex asked for (`picortex-5sc`).
+
+## What Piyush got wrong (and why it was replaced)
+
+1. **Per-user, not per-chat workspaces.** Fine for "I log in and chat with one Claude." Useless for group texts where each chat needs its own context and filesystem. Cortex pivoted on this.
+2. **No attention gating / group model.** Piyush's chat is a single web interface, not a group-text agent.
+3. **Single shared EC2 box** with Linux users for isolation — acceptable for a v0 but blocked the enterprise multi-tenant direction. Fly.io Docker containers replaced it.
+4. **No Linq / iMessage / SMS.** Web-chat-only. Adding texting was a wholesale rewrite, not an add-on.
+5. **No per-file ACL, no sharing bridge, no cross-chat context.** All deferred.
+6. **SQLite-first schema** would have needed a migration to scale; Tejas moved to Postgres.
+7. **Claude's `-c` continuation is global per user**, not per-topic — subtle UX problem: one user's chats all share the same Claude memory. Needs `--session-id` instead of `-c` to get per-chat isolation.
+
+## Mapping Piyush's design onto the "awesome texting" framing
+
+Piyush's design was "web chat + dev surface for one user." Jacob wants "iMessage for Jacob + group texts with shared brain." The overlap:
+
+| Piyush element | picortex need | Fit |
+|---|---|---|
+| Separate workspace host | "remote box for privacy" | ✅ perfect |
+| `claude -c -p` per turn | replaces tmux sentinel fragility | ✅ perfect |
+| Per-user Linux isolation | generalize to per-chat | ⚠️ adapt |
+| apiKeyHelper | keeps keys out of workspace | ✅ steal |
+| Sudoers scoped to useradd etc. | replaces picortex's broad sudoers | ✅ steal |
+| Provisioning progress WS events | mobile UI UX | ✅ steal for web UI |
+| File browser / xterm terminal | may or may not be needed for texting-first | 🤷 defer, probably skip in v0.1 |
+| Web chat UI | redundant if Linq is the primary surface | ❌ skip |
+| User signup / own API keys | Jacob is the only user | ❌ skip |
+| Voice | covered by existing voice-assistant project | ❌ skip |
+
+## Verbatim quotes worth remembering
+
+From the original README at `d2d6a534`:
+
+> Each user gets their own isolated Linux environment on EC2
+
+> Messages sent via WebSocket, executed as `claude -p "..."` via SSH
+
+From `claudeService.ts`:
+
+> `-c` continues the previous conversation session
+> `--dangerously-skip-permissions` allows file operations without interactive prompts
+
+From `setup-ec2.sh` sudoers block:
+
+```
+cortex_admin ALL=(ALL) NOPASSWD: /usr/sbin/useradd
+cortex_admin ALL=(ALL) NOPASSWD: /usr/sbin/userdel
+cortex_admin ALL=(ALL) NOPASSWD: /bin/chown
+cortex_admin ALL=(ALL) NOPASSWD: /bin/chmod
+cortex_admin ALL=(ALL) NOPASSWD: /bin/mkdir
+cortex_admin ALL=(ALL) NOPASSWD: /usr/bin/sudo -u *
+```
+
+## References
+
+- Piyush's commits: `238052c4` → `d2d6a534` in `IdeaFlowCo/cortex`
+- Tejas's replacement: starting `af3a76f5` (2026-01-26, "Implement Fly.io workspace infrastructure")
+- Related brainstorm: [docs/plans/2026-04-23-prototype-options.md](../plans/2026-04-23-prototype-options.md)
+- The old "skip Cortex pre-container history" rule in [cortex-inheritance.md](cortex-inheritance.md) is **softened** — pre-container is still not a source of patterns to inherit, but **is** a source of ideas to *study*.
\ No newline at end of file
new file mode 100644
index 0000000..85549a8
@@ -0,0 +1,84 @@
+---
+visibility: public
+---
+
+# Relationship to noos
+
+## Short version
+
+**picortex is NOT a noos sub-project.** It's a separate codebase with its own auth path, SQLite, and deployment. But several noos facilities are useful and will be consumed — via the normal public API, not deep imports.
+
+## What noos offers that picortex wants
+
+| noos | picortex use | When |
+|---|---|---|
+| OAuth / SSO (`globalbr.ai/auth/authorize`) | Web UI login (non-iMessage path) | S7 |
+| User graph (contacts, follows) | Future cross-chat MCP (who am I allowed to message?) | Deferred (R6) |
+| Knowledge-graph node storage | Maybe — picortex message-log could double as graph nodes | v0.3+ |
+| `NOOS_BOT_API_KEY` pattern | picortex backend calling noos for identity checks | S7 |
+
+## What picortex keeps to itself
+
+- Canonical message log (SQLite)
+- Chat configuration (attention modes, discriminator prompts)
+- Workspace filesystem state
+- Tmux session state
+- Linq event dedup cache
+- Unix user provisioning
+
+These are **runtime** state, not **knowledge** state. noos is the knowledge system; picortex is a chat runtime. They meet at the identity boundary.
+
+## What noos already has in this space (2026-04-23)
+
+noos's existing Slack bot (`~/code/noos/src/slack/`) does some of what picortex does:
+
+- Captures messages, extracts hashtags, creates knowledge-graph nodes
+- @mentions spawn `claude --print --resume` on the server and stream tool-use back
+- Reaction handlers (stubbed)
+
+But:
+- It's Slack-only, not Linq.
+- It does not have per-chat Unix isolation.
+- It does not have a mobile-first UI attached.
+- It does not have a web terminal.
+- It's coupled to Neo4j / the noos API.
+
+If picortex ever needs Slack as a channel (probably v0.3), it could either:
+- (a) add a `SlackChannel` adapter and re-implement what noos's slack bot does, or
+- (b) have noos's Slack bot relay through picortex (noos handles Slack; picortex handles sessions).
+
+Leaning (b) long-term because it preserves separation of concerns.
+
+## Overlap with voice-assistant
+
+voice-assistant (`~/code/voice-assistant`) also has:
+- file system read/write tools
+- run_command
+- Claude Code / Codex spawning
+- Noos OAuth SSO
+- daily logs / system context / preferences
+
+voice-assistant is voice-first and its deployment is on jcortex (Hetzner). Text-first picortex is structurally different — message log, tmux, web terminal attach — but the tool surface overlaps. Possible futures:
+
+- picortex and voice-assistant co-deploy on jcortex and share the `chat-*` user space
+- voice-assistant becomes a *consumer* of picortex for text (i.e. when a voice conversation drops into text, picortex handles the session)
+- they stay separate
+
+**Decision for v0.1:** stay separate, don't worry about it. Revisit in D1.
+
+## Testing noos integration
+
+If/when picortex calls noos:
+- Use `NOOS_BOT_API_KEY` with `x-api-key` header
+- Target `https://globalbr.ai/api/*` (production) or the dev port in `~/code/noos/.env`
+- Never import from `noos/` at the module level — always HTTP.
+
+## Repo location note
+
+`~/code/noos` tracks `IdeaFlowCo/noos` (private), **not** `tmad4000/noos` (which does not exist). Verified 2026-04-23 via `git remote -v` in the local clone. If picortex ever opens a PR to noos, it's against the IdeaFlowCo org.
+
+## Related
+
+- `~/code/PROJECTS.md` — ecosystem diagram
+- [ADR-0001](../adrs/0001-standalone-project-not-noos-fork.md) — why standalone
+- [openchat-adapter.md](openchat-adapter.md) — a third touchpoint on shared infra
\ No newline at end of file
new file mode 100644
index 0000000..fd56fd6
@@ -0,0 +1,37 @@
+---
+visibility: public
+---
+
+# Repo versions — verified 2026-04-23
+
+GitHub-latest sweep of every repo referenced in picortex docs. Keep this up to date at quarterly diff checkpoints.
+
+| Reference | Actual repo | Latest SHA | Date | Status | Notes |
+|---|---|---|---|---|---|
+| Cortex (pattern source) | `IdeaFlowCo/cortex` | `e93bf8c` | 2026-04-23 | active | Docs(plan): OpenClaw gateway lifecycle alignment design |
+| noos (identity / knowledge graph) | `IdeaFlowCo/noos` | — | — | private (404) | NOT `tmad4000/noos`. Local: `~/code/noos` |
+| openchat (candidate D3 channel) | **unknown** | — | — | repo-not-found | Deployed at `chat.globalbr.ai` from `/opt/openchat` with NO git remote. The public `tmad4000/openchat` is a 2023 scaffold, not the live source. Open question for Jacob. |
+| voice-assistant | `tmad4000/voice-assistant` | `89870e1` | 2026-02-11 | stable | Text chat thinking indicator |
+| listhub (ecosystem) | `tmad4000/listhub` | `9ec8dbf` | 2026-04-22 | active | bd stderr fix |
+| Thoughtstreams | `tmad4000/Thoughtstreams` | `e3d447e` | 2026-01-26 | stable | Feedback widget v2.1 |
+| vibe-coding-guide | `tmad4000/vibe-coding-guide` | `4b4d644` | 2026-04-22 | active | Background poller pattern |
+| tmad4000.github.io | `tmad4000/tmad4000.github.io` | `5d4a5e2` | 2026-04-18 | stable | Open Asks section |
+| ai-os-apple-data | `tmad4000/ai-os-apple-data` | `f290bd0` | 2026-04-01 | quiet | `bd sync` auto-commits |
+| claude-mind | `tmad4000/claude-mind` | `190b728` | 2026-01-01 | quiet | |
+| thoughtstream-gemini-jacob | — | — | — | private-or-missing | Listed in `PROJECTS.md` but `gh api` returns 404 |
+| OpenClaw (`opentoolshub/openclaw`) | `opentoolshub/openclaw` | — | — | private-or-missing | npm package `openclaw`; not used by picortex (explicit non-goal) |
+
+## Re-verify command
+
+```bash
+for repo in IdeaFlowCo/cortex IdeaFlowCo/noos tmad4000/voice-assistant tmad4000/listhub tmad4000/Thoughtstreams tmad4000/vibe-coding-guide tmad4000/tmad4000.github.io tmad4000/ai-os-apple-data tmad4000/claude-mind; do
+ echo "--- $repo ---"
+ gh api "repos/$repo/commits?per_page=1" --jq '.[] | "\(.commit.author.date[:10]) \(.sha[:7]) \(.commit.message | split("\n")[0])"'
+done
+```
+
+## Corrections made vs initial docs
+
+- noos repo URL corrected to `IdeaFlowCo/noos` (not `tmad4000/noos`) in `relationship-to-noos.md`
+- openchat GitHub ambiguity flagged in `openchat-adapter.md`
+- Cortex pin bumped from `e55f129` → `e93bf8c` in `cortex-inheritance.md`
\ No newline at end of file
new file mode 100644
index 0000000..3994bc0
@@ -0,0 +1,70 @@
+---
+visibility: public
+---
+
+# Workspace isolation
+
+## The question
+
+"When a compromised message in chat A tries to read chat B's files, what stops it?"
+
+## The answer (v1)
+
+**POSIX permissions.** Chat A's processes run as a different Unix user (`chat-<hexA>`) from chat B's (`chat-<hexB>`). Chat B's home dir is `chmod 0700` and owned by `chat-<hexB>`. Chat A's user has no read access.
+
+## What this buys us
+
+- Fast spin-up (< 1 s: `useradd`, `mkdir`, `chown`, `chmod`).
+- Tiny idle footprint (a tmux + bash — few MB).
+- Composes with cheap hardening (bubblewrap, Landlock, firejail) later.
+- Works on any Linux; no daemon.
+
+## What this does NOT buy us
+
+- Kernel-level exploits cross boundaries.
+- Shared `/tmp`, shared `/dev`, shared networking by default.
+- Shared `/proc` can expose sibling-user process info (`ps aux`).
+- Resource limits need explicit cgroups.
+
+## Minimum hardening (v1)
+
+1. `chmod 0700` on home dirs, owned by the chat user
+2. `pam_namespace` → per-user `/tmp`
+3. `iptables` `owner match` → egress allowlist per UID
+4. cgroups v2 → 256 MB RAM, 200 pids, quarter-CPU per chat
+5. No sudo for chat users; no setuid binaries in their `PATH`
+6. Restricted PATH: shims at `/usr/local/bin/picortex-shims` first, then nothing of consequence
+
+## Known gaps (documented, not fixed in v1)
+
+- A chat user can `ps aux` and see other chat users' command lines. → Fix with `hidepid=2` mount option on `/proc`.
+- Shared DNS resolver config. → Fix with per-chat `resolv.conf` via namespaces.
+- Memory pressure from one chat can evict another (page cache). → Live with it.
+
+## Stronger options (evaluated for D2)
+
+- **bubblewrap (`bwrap`)**: add to the tmux-entry wrapper; cheap, composable, solid defaults.
+- **Landlock (kernel ≥ 6.1)**: programmatic FS ACLs per-process; newer API, smaller ecosystem.
+- **nsjail**: more config knobs than bwrap; use if bwrap isn't granular enough.
+- **firejail**: friendliest defaults for interactive shells.
+- **gVisor**: overkill, kernel surface reduction but cost.
+- **Firecracker microVM per chat**: overkill but the strongest.
+
+D2 will rank these with Jacob's real threat model. Start simple; add layers empirically.
+
+## Threat model (v1)
+
+| Threat | Likelihood | Impact | Controlled by |
+|---|---|---|---|
+| Prompt injection via a group member's message causes bot to exfil Jacob's DM files | high | high | per-chat user + `0700` |
+| Prompt injection causes bot to delete chat's own files | medium | low | acceptable — files are a cache |
+| Kernel exploit from chat user | low | high | keep kernel patched; consider bwrap/Landlock |
+| Bot accidentally posts a private file to the wrong chat | medium | high | sharing bridge R7 with BridgeEvent audit |
+| Someone gets Linq webhook secret | low | very high | HMAC + replay guard; rotate secret on suspicion |
+
+## References
+
+- [Spec 001](../specs/001-workspace-isolation-linux-users.md) — concrete provisioning
+- [ADR-0002](../adrs/0002-linux-users-over-docker.md) — the Linux-users-not-Docker decision
+- `~/memory/research/openclaw-group-chat-security.md` — prior research on prompt-injection in group chats
+- D2 ticket — isolation-model comparison report
\ No newline at end of file
new file mode 100644
index 0000000..9a465a8
@@ -0,0 +1,94 @@
+---
+visibility: public
+---
+
+# picortex
+
+*Working name — subject to rename. See [ADR-0001](docs/adrs/0001-standalone-project-not-noos-fork.md).*
+
+A personal variant of [Cortex](https://cortex.ideaflow.app), tuned for one developer: **iMessage + group texting via the Linq partner API**, per-chat **Linux-permission** workspace isolation (no Docker), mobile-first web UI with file browser + web terminal attaching live to the chat's tmux + Claude Code session.
+
+**Status:** Planning phase — no code yet. See [PRD](docs/prd/001-picortex-v1.md) and the [initial roadmap](docs/plans/2026-04-23-initial-roadmap.md).
+
+> ⚠️ **Architecture provisional (2026-04-23).** This README, the PRD, ADR-0003, and Specs 002/003 describe a tmux-centered single-host design. Later in the same session, Jacob reframed the product as "awesome texting experience" (not dev-surface-first), and [prototype-options.md](docs/plans/2026-04-23-prototype-options.md) reopened the core architecture choice. The tmux path is **one option (#1)** of six under consideration. A [Piyush-era study](docs/wiki/piyush-era-design.md) argues Option 2 (`claude -c -p` per turn, two-box physical split) is probably the better v0.1 shape. Do not start implementation until [Q0-Q4 tickets](https://wikihub.globalbr.ai/@jacobcole/picortex) close and the [reconciliation sweep](https://wikihub.globalbr.ai/@jacobcole/picortex) (picortex-357) rewrites this doc against the chosen option.
+
+## Elevator pitch
+
+Cortex (IdeaFlowCo/cortex) has spent the last month solving 90% of the problems this project needs solved — per-chat workspaces, attention gating, scoped tokens, sharing bridge, Linq webhook ingestion, linq-sim dev harness. picortex is a personal spin that keeps Cortex's architecture but:
+
+- **Lighter isolation** — Unix user + filesystem permissions per chat instead of Docker containers
+- **Mobile-first from day one** — the split-view betterGPT mockups Jacob already drew
+- **Slimmer surface** — no enterprise multi-tenant, no billing, one developer
+- **Openchat as a third testing channel** — if/when openchat gets upgraded with a linq-compatible adapter, picortex can run against it without even going through Linq
+
+## Why not fork Cortex directly?
+
+- Cortex is a team/business product with design constraints picortex doesn't have
+- Docker-container-per-workspace is the single biggest source of spin-up latency and resource cost — Linux-user isolation is ~100× cheaper and sufficient for single-tenant use
+- Picortex is an R&D vehicle for isolation alternatives, which would drag on Cortex's main line
+- See [ADR-0001](docs/adrs/0001-standalone-project-not-noos-fork.md) and [ADR-0002](docs/adrs/0002-linux-users-over-docker.md)
+
+## Core capabilities (planned v1)
+
+| Capability | Primary ref |
+|---|---|
+| iMessage + group text interface via Linq | [spec/006](docs/specs/006-linq-integration.md) |
+| Per-chat Linux-user + home-dir isolation | [spec/001](docs/specs/001-workspace-isolation-linux-users.md) |
+| Tmux session per chat running Claude Code | [spec/002](docs/specs/002-tmux-session-spawning.md) |
+| Web terminal (xterm.js) attaching to live tmux | [spec/003](docs/specs/003-web-terminal-xtermjs.md) |
+| File browser (betterGPT split-view) | [spec/004](docs/specs/004-file-browser.md) |
+| Attention gating (always / mentions-only / discriminate / silent) | [spec/005](docs/specs/005-attention-gating.md) |
+| Mobile-first web UI with thread/reply | [spec/007](docs/specs/007-mobile-first-webui.md) |
+| Structured logs + `/api/frontend-log` + version display | [spec/008](docs/specs/008-observability.md) |
+
+## Explicit non-goals (v1)
+
+- No Docker, no Kubernetes, no docker-compose
+- No session-management dashboard (Cortex has one; picortex starts with "web terminal attach to tmux session for current chat" only)
+- No billing / multi-tenant / team accounts
+- No native mobile apps (mobile-first *web* UI only)
+- No group-text mobile UI yet (1:1 first)
+
+## Documents
+
+- `docs/prd/001-picortex-v1.md` — PRD v1 (provisional)
+- `docs/plans/2026-04-23-initial-roadmap.md` — phased roadmap (Option-1 shape; provisional)
+- `docs/plans/2026-04-23-prototype-options.md` — **the live brainstorm** comparing 6 architecture options
+- `docs/wiki/piyush-era-design.md` — study of pre-container Cortex; source of Option 2
+- `docs/reviews/codex-2026-04-23.md` — codex review of the initial plan
+- `docs/specs/` — technical specs per subsystem (provisional)
+- `docs/adrs/` — architecture decisions (one per file, Nygard format)
+- `docs/wiki/` — Karpathy-style LLM wiki (concept graph)
+- `docs/runbooks/` — ops playbooks
+- `AGENTS.md` — operational instructions for coding agents (Codex, Claude Code, etc.)
+- `CLAUDE.md` → symlink to `AGENTS.md`
+- `llms.txt` — LLM sitemap
+
+### Session history
+- `~/memory/session-logs/2026-04-23-picortex-planning/` — full JSONL session + cleaned transcript + codex's session-level review (`session-review.md`)
+- Public mirror: https://wikihub.globalbr.ai/@jacobcole/picortex
+- Documentation best practices that informed this doc set: `~/memory/research/documentation-best-practices-2026-04-23.md`
+
+## Ports
+
+Per [dev-patterns](https://github.com/tmad4000/jacob-computer-config-private) rule: no default ports.
+
+- Dev backend: **7823**
+- Dev frontend: **7824**
+- Local linq-sim (via Cortex): **8447**
+
+## Quick start (eventual)
+
+```bash
+npm install
+cp .env.example .env
+npm run dev # backend + frontend + linq-sim orchestrator
+npm test # vitest
+```
+
+## Related projects
+
+- [Cortex (IdeaFlowCo)](https://github.com/IdeaFlowCo/cortex) — pattern source
+- [noos](~/code/noos) — potentially overlapping (knowledge graph + Slack bot); see [wiki/relationship-to-noos.md](docs/wiki/relationship-to-noos.md)
+- [openchat](~/code/openchat) — candidate third channel once linq-adapter lands
+- [voice-assistant](~/code/voice-assistant) — predecessor with overlapping tool set (file system, run_command, Claude spawning); see [wiki/relationship-to-noos.md](docs/wiki/relationship-to-noos.md)
\ No newline at end of file
new file mode 100644
index 0000000..ce3edcd
@@ -0,0 +1,12 @@
+---
+visibility: public
+---
+
+# log
+
+For the picortex wiki log (ingestion / maintenance entries), see
+[docs/wiki/log.md](docs/wiki/log.md).
+
+This root `log.md` was created by the wikihub scaffold and is intentionally
+shallow — the canonical log lives with the rest of the LLM wiki under
+`docs/wiki/`.
\ No newline at end of file
new file mode 100644
index 0000000..9c75729
@@ -0,0 +1,21 @@
+---
+visibility: public
+---
+
+# schema
+
+this wiki has no imposed structure. pages are markdown files with optional YAML frontmatter.
+
+## frontmatter
+
+```yaml
+---
+title: Page Title
+visibility: public | private | unlisted
+tags: [topic1, topic2]
+---
+```
+
+## wikilinks
+
+link pages with `[[page-name]]` or `[[page-name|Display Text]]`. links resolve by filename — `[[linear-algebra]]` finds `wiki/courses/linear-algebra.md`.
\ No newline at end of file