Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c1241ee
feat(collab): add packages/shared/collab protocol contract (Slice 1)
backnotprop Apr 18, 2026
91d732d
feat(collab): add apps/room-service Worker + Durable Object skeleton …
backnotprop Apr 18, 2026
b673ec4
feat(collab): durable room engine — event sequencing, admin, lifecycl…
backnotprop Apr 18, 2026
3166f08
feat(collab): browser/direct-agent client runtime + React hook (Slice 4)
backnotprop Apr 19, 2026
f68c8cd
WIP: pre-consolidation snapshot (anchor for Live Rooms V1 cleanup)
backnotprop Apr 19, 2026
a6380a2
refactor(collab): move 5 collab hooks to packages/ui/hooks/collab/
backnotprop Apr 19, 2026
78481d7
refactor(editor): extract useStartLiveRoom from App.tsx (Phase 1)
backnotprop Apr 19, 2026
4e000a5
refactor(editor): move checkbox pending-state derivation next to useC…
backnotprop Apr 19, 2026
62e97e8
chore(collab): clean stale comments + dedup fake-presence palette (Ph…
backnotprop Apr 19, 2026
68ab47a
refactor(collab): consolidate admin error-code contract (Phase 4)
backnotprop Apr 19, 2026
2a0bca9
fix(collab): hoist ThemeProvider + align room dialogs with canonical …
backnotprop Apr 19, 2026
236be66
feat(collab-agent): package skeleton + dep graph verification (Phase 1)
backnotprop Apr 19, 2026
97006d8
feat(collab-agent): pure agentIdentity module + admin URL guard + hea…
backnotprop Apr 19, 2026
c4920ec
feat(collab-agent): join + read-plan + read-annotations + read-presen…
backnotprop Apr 19, 2026
0c11551
feat(collab): visual marker for agent cursors + avatars (Phase 4)
backnotprop Apr 19, 2026
a3fc8f0
feat(collab-agent): comment subcommand — block-level COMMENT posting …
backnotprop Apr 19, 2026
e21470f
feat(collab-agent): demo subcommand — walk headings with block-space …
backnotprop Apr 19, 2026
a2a2e10
docs(collab-agent): AGENT_INSTRUCTIONS.md + README.md (Phase 7)
backnotprop Apr 19, 2026
cb8b272
test(ui): selection-accuracy matrix + follow-up spec note (Phase 8)
backnotprop Apr 19, 2026
911046b
fix(collab-agent): demo confirms per-heading echoes + cursor x/y rand…
backnotprop Apr 19, 2026
c37055a
feat(collab): Room menu → Copy agent instructions (shell-safe payload)
backnotprop Apr 19, 2026
2827d35
chore: add wrangler to root devDeps
backnotprop Apr 19, 2026
aedde9e
feat(collab): agent instructions nudge default behavior (demo-first)
backnotprop Apr 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ jobs:
run: bun run typecheck

- name: Run tests
run: bun test
# See .github/workflows/test.yml for why this is `bun run test`
# and not raw `bun test`.
run: bun run test

build:
needs: test
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ jobs:
run: bun run typecheck

- name: Run tests
run: bun test
# Use the root `test` script (splits non-UI + UI-cwd) so the
# packages/ui/bunfig.toml happy-dom preload is loaded. Raw
# `bun test` from the repo root doesn't pick up that package-
# scoped preload, so UI hook tests would hit "document is not
# defined".
run: bun run test

install-cmd-windows:
# End-to-end integration test for scripts/install.cmd on real cmd.exe.
Expand Down
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,14 @@ opencode.json
plannotator-local
# Local research/reference docs (not for repo)
/reference/

# Cloudflare Wrangler local state (Miniflare SQLite, caches)
.wrangler/

# Room-service Vite build output (chunked editor bundle served by
# Cloudflare's [assets] binding; regenerated by `bun run build:shell`).
apps/room-service/public/

# Claude Code local scratch files (per-session locks, etc.). Intentionally
# ignored so they can't be committed accidentally.
.claude/scheduled_tasks.lock
61 changes: 56 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ plannotator/
│ │ ├── index.html
│ │ ├── index.tsx
│ │ └── vite.config.ts
│ ├── room-service/ # Live collaboration rooms (Cloudflare Worker + Durable Object)
│ │ ├── core/ # Handler, DO class, validation, CORS, log, types, csp
│ │ ├── targets/cloudflare.ts # Worker entry + DO re-export
│ │ ├── entry.tsx # Browser shell entry — mounts AppRoot for /c/:roomId
│ │ ├── index.html # Vite template; produces hashed chunks under /assets/
│ │ ├── vite.config.ts # Browser shell build (bun run build:shell)
│ │ ├── tsconfig.browser.json # DOM-lib tsconfig for the shell
│ │ ├── static/ # Root-level static assets copied into public/ by build:shell (favicon.svg)
│ │ ├── scripts/smoke.ts # Integration test against wrangler dev
│ │ └── wrangler.toml # SQLite-backed DO binding + ASSETS binding for built shell
│ └── vscode-extension/ # VS Code extension — opens plans in editor tabs
│ ├── bin/ # Router scripts (open-in-vscode, xdg-open)
│ ├── src/ # extension.ts, cookie-proxy.ts, ipc-server.ts, panel-manager.ts, editor-annotations.ts, vscode-theme.ts
Expand All @@ -51,16 +61,33 @@ plannotator/
│ │ ├── components/ # Viewer, Toolbar, Settings, etc.
│ │ │ ├── icons/ # Shared SVG icon components (themeIcons, etc.)
│ │ │ ├── plan-diff/ # PlanDiffBadge, PlanDiffViewer, clean/raw diff views
│ │ │ └── sidebar/ # SidebarContainer, SidebarTabs, VersionBrowser, ArchiveBrowser
│ │ ├── utils/ # parser.ts, sharing.ts, storage.ts, planSave.ts, agentSwitch.ts, planDiffEngine.ts, planAgentInstructions.ts
│ │ ├── hooks/ # useAnnotationHighlighter.ts, useSharing.ts, usePlanDiff.ts, useSidebar.ts, useLinkedDoc.ts, useAnnotationDraft.ts, useCodeAnnotationDraft.ts, useArchive.ts
│ │ │ ├── sidebar/ # SidebarContainer, SidebarTabs, VersionBrowser, ArchiveBrowser
│ │ │ └── collab/ # RoomPanel, RoomStatusBadge, ParticipantAvatars, AdminControls, JoinRoomGate, StartRoomModal, RemoteCursorLayer, ImageStripNotice
│ │ ├── utils/ # parser.ts, sharing.ts, storage.ts, planSave.ts, agentSwitch.ts, planDiffEngine.ts, planAgentInstructions.ts, adminSecretStorage.ts, blockTargeting.ts
│ │ ├── hooks/ # useAnnotationHighlighter.ts, useSharing.ts, usePlanDiff.ts, useSidebar.ts, useLinkedDoc.ts, useAnnotationDraft.ts, useCodeAnnotationDraft.ts, useArchive.ts, useCollabRoom.ts, useCollabRoomSession.ts, useAnnotationController.ts, useRoomMode.ts, usePresenceThrottle.ts
│ │ └── types.ts
│ ├── ai/ # Provider-agnostic AI backbone (providers, sessions, endpoints)
│ ├── shared/ # Shared types, utilities, and cross-runtime logic
│ │ ├── storage.ts # Plan saving, version history, archive listing (node:fs only)
│ │ ├── draft.ts # Annotation draft persistence (node:fs only)
│ │ └── project.ts # Pure string helpers (sanitizeTag, extractRepoName, extractDirName)
│ ├── editor/ # Plan review App.tsx
│ │ ├── project.ts # Pure string helpers (sanitizeTag, extractRepoName, extractDirName)
│ │ └── collab/ # Live Rooms protocol, crypto, validators, client runtime, React hook
│ │ ├── types.ts # Protocol types + runtime validators (isRoomAnnotation, isRoomSnapshot, isPresenceState, ...)
│ │ ├── crypto.ts # HKDF key derivation, HMAC proofs, AES-GCM payload encrypt/decrypt
│ │ ├── ids.ts # roomId/secret/opId/clientId generators
│ │ ├── url.ts # parseRoomUrl / buildRoomJoinUrl / buildAdminRoomUrl (client-only)
│ │ ├── constants.ts # ROOM_SECRET_LENGTH_BYTES, ADMIN_SECRET_LENGTH_BYTES, WS_CLOSE_ROOM_UNAVAILABLE, WS_CLOSE_REASON_ROOM_DELETED, WS_CLOSE_REASON_ROOM_EXPIRED
│ │ ├── canonical-json.ts # canonicalJson for admin command proof binding
│ │ ├── encoding.ts # base64url helpers
│ │ ├── strip-images.ts # toRoomAnnotation, stripRoomAnnotationImages (image stripping for room snapshots)
│ │ ├── redact-url.ts # redactRoomSecrets (scrub #key=/#admin= from telemetry/logs)
│ │ ├── validation.ts # isBase64Url32ByteString / isValidPermissionMode
│ │ ├── client.ts # Client barrel re-exports
│ │ └── client-runtime/ # CollabRoomClient class, createRoom, joinRoom, apply-event reducer
│ ├── editor/ # Plan review app (App.tsx) + room-mode shell
│ │ ├── App.tsx # Plan review editor (local + room-mode prop)
│ │ ├── AppRoot.tsx # Mode fork (local | room | invalid-room); package default export
│ │ └── RoomApp.tsx # Room-mode shell — identity gate, session, overlays, delete/expired fallbacks
│ └── review-editor/ # Code review UI
│ ├── App.tsx # Main review app
│ ├── components/ # DiffViewer, FileTree, ReviewSidebar
Expand Down Expand Up @@ -192,6 +219,17 @@ During normal plan review, an Archive sidebar tab provides the same browsing via

### Plan Server (`packages/server/index.ts`)

Live Rooms V1 does NOT support approve/deny from the room origin.
Approvals always happen on the local editor origin (the tab that
started the hook). Room-side annotations flow back to the local
editor via the existing import paths (static share hash, paste short
URL, "Copy consolidated feedback" → paste).

Local external annotations (`/api/external-annotations` + SSE) remain
local to the localhost editor in the current room integration.
Forwarding those annotations into encrypted room ops is later Slice 6
work; it is not part of the room-origin approve/deny surface.

| Endpoint | Method | Purpose |
| --------------------- | ------ | ------------------------------------------ |
| `/api/plan` | GET | Returns `{ plan, origin, previousPlan, versionInfo }` (plan mode) or `{ plan, origin, mode: "archive", archivePlans }` (archive mode) |
Expand Down Expand Up @@ -275,6 +313,19 @@ All servers use random ports locally or fixed port (`19432`) in remote mode.

Runs as a separate service on port `19433` (self-hosted) or as a Cloudflare Worker (hosted).

### Room Service (`apps/room-service/`)

Live-collaboration rooms for encrypted multi-user annotation. Zero-knowledge: the Worker + Durable Object stores and relays ciphertext only. Clients hold the room secret in the URL fragment and derive `authKey`/`eventKey`/`presenceKey`/`adminKey` locally.

| Endpoint | Method | Purpose |
| --------------------- | ------ | ------------------------------------------ |
| `/health` | GET | Worker liveness probe |
| `/c/:roomId` | GET | Room SPA shell — serves the built editor bundle (hashed chunks under `/assets/`). Response carries `ROOM_CSP`, `Cache-Control: no-store` on the HTML, `Referrer-Policy: no-referrer`. `:roomId` is validated against `isRoomId()` before the asset fetch. |
| `/api/rooms` | POST | Create room. Body: `{ roomId, roomVerifier, adminVerifier, initialSnapshotCiphertext, expiresInDays? }`. Returns `201` on success; `409` on duplicate `roomId`. Response body is intentionally not consumed by `createRoom()`. |
| `/ws/:roomId` | GET | WebSocket upgrade into the room Durable Object. `roomId` is validated via `isRoomId()` before `idFromName()` to prevent arbitrary DO instantiation. |

Protocol contract lives in `packages/shared/collab/`; the Worker/DO never imports client-only URL helpers.

## Plan Version History

Every plan is automatically saved to `~/.plannotator/history/{project}/{slug}/` on arrival, before the user sees the UI. Versions are numbered sequentially (`001.md`, `002.md`, etc.). The slug is derived from the plan's first `# Heading` + today's date via `generateSlug()`, scoped by project name (git repo or cwd). Same heading on the same day = same slug = same plan being iterated on. Identical resubmissions are deduplicated (no new file if content matches the latest version).
Expand Down
190 changes: 190 additions & 0 deletions apps/collab-agent/AGENT_INSTRUCTIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# Plannotator Live Rooms — Agent Instructions

This document is prose an AI agent (Claude Code, Codex, OpenCode,
Junie, or another) should have in its prompt when it's being
driven to participate in a Plannotator Live Room. It explains
the identity convention, the CLI subcommand surface, and the
handful of rules that keep agent participation well-behaved.

## 1. Identity

Your identity in the room follows the pattern:

```
<user>-agent-<type>
```

Examples: `swift-falcon-tater-agent-claude`,
`alice-agent-codex`.

- `<user>` is the human you're acting on behalf of. If you've
been given their Plannotator identity (a "tater name" like
`swift-falcon-tater`), use it verbatim.
- `<type>` is one of: `claude`, `codex`, `opencode`, `junie`,
`other`. Use `other` when you don't fit any of the explicit
kinds — it's a legal value, not a fallback error.

You pass these as `--user` and `--type` on every CLI invocation;
the CLI assembles the full identity string and refuses to run if
either is missing or malformed.

Room participants see your identity in their avatar row and as
the label on your cursor. A small `⚙` marker appears next to the
identity on both surfaces so observers can tell you're an agent,
not a human teammate.

## 2. Joining and staying visible

The V1 room protocol has no participant roster. Peers appear on
one another's screens **only after presence is received**. A
client that just connects and stays silent is invisible.

Two subcommands handle this correctly:

- `join` — connect, emit initial presence, heartbeat presence on
a 10s cadence, stream room events to stdout until Ctrl-C. Use
this when you need to be present while you think or wait.
- `demo` — a showcase walk; not for real work.

Short one-shot reads (`read-plan`, `read-annotations`,
`read-presence`) emit presence exactly once before they print and
exit. You briefly flash into the observer's avatar row, then
disappear.

Do **not** implement your own WebSocket or presence loop. The
CLI is the supported entry point.

## 3. Reading the plan

```
bun run apps/collab-agent/index.ts read-plan \
--url "<full room URL including #key=...>" \
--user <name> --type <kind>
```

Add `--with-block-ids` to get each block prefixed with
`[block:<id>]`. You need those ids if you plan to comment.

Block ids are **derived from the markdown** — the CLI uses the
same parser the browser uses, so the ids you read here are
byte-identical to what the observer sees in their DOM.

## 4. Reading existing annotations

```
bun run apps/collab-agent/index.ts read-annotations \
--url "..." --user <name> --type <kind>
```

Prints the full `RoomAnnotation[]` array as pretty JSON. Fields:
`id`, `blockId`, `startOffset`, `endOffset`, `type`, `text`,
`originalText`, `createdA`, `author`.

## 5. Reading recent presence

```
bun run apps/collab-agent/index.ts read-presence \
--url "..." --user <name> --type <kind>
```

Prints `remotePresence` as JSON keyed by opaque per-connection
client ids. **This is NOT a participant roster.** It is
"peers who've emitted presence in the last 30 seconds." A user
who's connected but idle (not moving their mouse) will NOT
appear. Do not infer "who's in the room" from this call.

## 6. Posting a comment

Block-level only in V1.

```
bun run apps/collab-agent/index.ts comment \
--url "..." --user <name> --type <kind> \
--block <blockId> --text "<your comment>"
```

The annotation targets the entire block — its full content is the
"original text", and your `--text` becomes the comment body. Do
**not** attempt to select a sub-range of text. The V1 agent flow
does not support inline text-range targeting; the
`/api/external-annotations` inline-text matcher that some agents
may have used before is known to fail silently on markdown /
whitespace / NBSP / block-boundary drift.

### Choosing a block id

Three ways:

1. Run `read-plan --with-block-ids` to see the plan interleaved
with block markers.
2. Run `read-annotations` to see block ids on annotations other
agents or humans have already left.
3. Run `comment --list-blocks` (with `--url/--user/--type`) to
print a JSON array of `{ id, type, content }` for every block
and exit without posting.

Pick a block whose `content` matches what you want to comment on.

### Referencing specific wording

If your comment is about specific wording within a block, quote
the wording **in the comment body**, not as an anchor:

```
--text 'The phrase "as soon as possible" is ambiguous — what is the deadline?'
```

Do not try to select only `"as soon as possible"`. Select the
whole block, and put the phrase in prose.

### Exit codes

- `0` — comment echoed back from the server (confirmed posted).
- `1` — snapshot / echo timeout, unknown block id, or server
rejected the op (e.g. the room is locked).
- `2` — argv or usage error (missing flag, bad --type, etc.).

## 7. Demo mode

```
bun run apps/collab-agent/index.ts demo \
--url "..." --user <name> --type <kind> \
--duration 120
```

Walks heading blocks in order, anchors the cursor to each, posts
a comment per heading. For showcase only — not a real
participation pattern. Pass `--dry-run` to do the cursor walk
without posting.

## 8. Rules and limits

- **Never run as admin.** The CLI strips any `#admin=<secret>`
fragment from the URL by default and warns on stderr. There is
no opt-in flag. Agents do not perform lock / unlock / delete.
- **No image attachments.** V1 room annotations do not carry
images. If you need to share an image, the flow is via the
local editor's import path, not via the agent CLI.
- **Room annotations are server-authoritative.** Your
`sendAnnotationAdd` queues a local op; the server has the
final say. The `comment` subcommand waits for the echo before
exiting 0.
- **Text appears to peers after server echo.** Your comment
doesn't appear in your own `read-annotations` output until it
round-trips.

## 9. Troubleshooting

- **`Missing --url` / `Missing --user` / `Missing --type`** —
argv check. Add the missing flag.
- **`Timed out waiting for snapshot after 10000ms`** — the URL
parsed but the connection never received the initial
encrypted snapshot. Check the URL fragment is intact
(`#key=<secret>`) and the room service is reachable.
- **`unknown --block "<id>"`** — the block id you passed isn't
in the current plan. Run `comment --list-blocks` to see the
valid set; re-run with a matching id.
- **`<code>: <message>`** on a comment — server-side mutation
rejection. The most common cause is `room_locked` (an admin
locked the room; read-only mode). Wait and retry or target a
different room.
Loading