diff --git a/.beads/.gitignore b/.beads/.gitignore index 31bd94b..9203150 100644 --- a/.beads/.gitignore +++ b/.beads/.gitignore @@ -14,6 +14,22 @@ last-touched # Must not be committed as paths would be wrong in other clones redirect +# Dolt sql-server runtime state (per-machine; the server is auto-started) +dolt-server.lock +dolt-server.log +dolt-server.pid +dolt-server.port +dolt-server.activity + +# Credential key — a SECRET; must never be committed +.beads-credential-key + +# Local backups and transient runtime +backup/ +*.corrupt.backup/ +bd.sock.startlock +ephemeral.sqlite3 + # Sync state (local-only, per-machine) # These files are machine-specific and should not be shared across clones .sync.lock diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 5845a00..50d89e0 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -170,3 +170,25 @@ {"id":"int-6da00f86","kind":"field_change","created_at":"2026-05-23T15:04:20.369272Z","actor":"James Lal","issue_id":"attn-cqk","extra":{"field":"priority","new_value":"0","old_value":"1"}} {"id":"int-497668f9","kind":"field_change","created_at":"2026-05-23T15:17:11.994933Z","actor":"James Lal","issue_id":"attn-134","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Fixed: src/watcher.rs reclassify_atomic_save_remove() — a Remove whose paths all still exist on disk is an atomic-save artifact (WSL 9p/drvfs rename-over or delete+recreate), not a deletion, so it's reclassified to Modify (reload) instead of Remove (which closed the open doc / dropped it from the tree). Genuine deletions (path gone) stay Remove; mixed/partial stays Remove; non-Remove kinds pass through. 4 unit tests added, all watcher tests green."}} {"id":"int-049af5a3","kind":"field_change","created_at":"2026-05-23T18:05:09.669593Z","actor":"James Lal","issue_id":"attn-cqk","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed: ReviewMarginCard had a 'state' prop, so the $state rune added for the reply composer (attn-1rm) compiled as store auto-subscription (store_get($$props.state)) -> 'state.subscribe is not a function' thrown on every card render -> margin overlay never mounted (cards invisible). Renamed prop state->cardState + 2 call sites. Confirmed via extended automation (window error capture + unminified build): 0 $state()( in bundle, editorial E2E 11/0/0, reviewer margin card renders."}} +{"id":"int-d224efb2","kind":"field_change","created_at":"2026-05-28T20:20:16.634307Z","actor":"Angus Bezzina","issue_id":"attn-zb5","extra":{"field":"status","new_value":"in_progress","old_value":"open"}} +{"id":"int-9fcc25d5","kind":"field_change","created_at":"2026-05-28T20:20:17.119515Z","actor":"Angus Bezzina","issue_id":"attn-zb5.1","extra":{"field":"status","new_value":"in_progress","old_value":"open"}} +{"id":"int-ff865669","kind":"field_change","created_at":"2026-05-28T20:20:17.557323Z","actor":"Angus Bezzina","issue_id":"attn-zb5.1.1","extra":{"field":"status","new_value":"in_progress","old_value":"open"}} +{"id":"int-03f5f436","kind":"field_change","created_at":"2026-05-28T20:51:20.525986Z","actor":"Angus Bezzina","issue_id":"attn-zb5.1.1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} +{"id":"int-5bf06810","kind":"field_change","created_at":"2026-05-28T20:51:20.648687Z","actor":"Angus Bezzina","issue_id":"attn-zb5.1.2","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} +{"id":"int-c179ba86","kind":"field_change","created_at":"2026-05-28T20:51:20.77194Z","actor":"Angus Bezzina","issue_id":"attn-zb5.2.1","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} +{"id":"int-6d86ffb5","kind":"field_change","created_at":"2026-05-28T20:51:20.885379Z","actor":"Angus Bezzina","issue_id":"attn-zb5.2.2","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} +{"id":"int-cdb07a1e","kind":"field_change","created_at":"2026-05-28T20:51:20.98813Z","actor":"Angus Bezzina","issue_id":"attn-zb5.2.3","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} +{"id":"int-10e344e1","kind":"field_change","created_at":"2026-05-28T20:51:21.097063Z","actor":"Angus Bezzina","issue_id":"attn-zb5.2.4","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} +{"id":"int-b35c938a","kind":"field_change","created_at":"2026-05-28T20:51:21.210772Z","actor":"Angus Bezzina","issue_id":"attn-zb5.3.1","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} +{"id":"int-e1cf61bc","kind":"field_change","created_at":"2026-05-28T20:51:21.325939Z","actor":"Angus Bezzina","issue_id":"attn-zb5.3.2","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} +{"id":"int-e500ffd5","kind":"field_change","created_at":"2026-05-28T20:51:21.440016Z","actor":"Angus Bezzina","issue_id":"attn-zb5.3.3","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} +{"id":"int-104d56b5","kind":"field_change","created_at":"2026-05-28T20:51:21.54404Z","actor":"Angus Bezzina","issue_id":"attn-zb5.4.1","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} +{"id":"int-b176dada","kind":"field_change","created_at":"2026-05-28T20:51:21.649954Z","actor":"Angus Bezzina","issue_id":"attn-zb5.4.2","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} +{"id":"int-a5426224","kind":"field_change","created_at":"2026-05-28T20:54:25.302218Z","actor":"Angus Bezzina","issue_id":"attn-zb5.1.3","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} +{"id":"int-4679048c","kind":"field_change","created_at":"2026-05-28T20:54:25.406209Z","actor":"Angus Bezzina","issue_id":"attn-zb5.2.5","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} +{"id":"int-62662b0b","kind":"field_change","created_at":"2026-05-28T20:54:25.498276Z","actor":"Angus Bezzina","issue_id":"attn-zb5.3.4","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} +{"id":"int-dc8c8e56","kind":"field_change","created_at":"2026-05-28T20:54:25.583122Z","actor":"Angus Bezzina","issue_id":"attn-zb5.4.3","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} +{"id":"int-9c8abca3","kind":"field_change","created_at":"2026-05-28T20:54:26.118227Z","actor":"Angus Bezzina","issue_id":"attn-zb5.1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}} +{"id":"int-1f4f8a2b","kind":"field_change","created_at":"2026-05-28T20:54:26.223302Z","actor":"Angus Bezzina","issue_id":"attn-zb5.2","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} +{"id":"int-d6c93840","kind":"field_change","created_at":"2026-05-28T20:54:26.306244Z","actor":"Angus Bezzina","issue_id":"attn-zb5.3","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} +{"id":"int-4cc271e7","kind":"field_change","created_at":"2026-05-28T20:54:26.385364Z","actor":"Angus Bezzina","issue_id":"attn-zb5.4","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Closed"}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 2f0738e..a9711a8 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,192 +1,212 @@ -{"id":"attn-cqk","title":"Review margin/panel does not mount on panelOpen toggle (reviewer can't see cards)","description":"Found via real daemon E2E (attn --eval/--query). In a reviewer's shared-doc view, reviewStore.panelOpen flips to true (Cmd+J -\u003e togglePanel(), and the auto-open at App.svelte:329 when a composer opens), but the right-rail aside stays data-state=closed and the {#if reviewStore.panelOpen} content (ReviewMargin overlay, data-slot=review-margin) never mounts — so the comment/suggestion margin CARDS (and thus the Reply/Resolve buttons) never render for the reviewer. The store value reads true; the \u003caside\u003e in the {:else if hasSidebar} branch (App.svelte:2139-2177) doesn't react. Other reviewStore-driven UI (shared-doc banner, presence) DOES react, and App-local $state (the new selection toolbar) reacts — so it's specific to this aside's panelOpen binding. Repro: scripts/test-editorial-e2e.sh ('no margin card rendered'). Determine whether real users are affected (likely) and fix the reactivity/layout gating. Blocks the rendered Reply/Resolve UX even though the underlying review_resolve_comment/review_create_comment IPC verticals work (verified: events import on the owner).","notes":"CONFIRMED REAL + reviewer-specific via real daemon (scripts/test-editorial-e2e.sh + isolated probe). Reviewer in shared-doc view: panelOpen=true (set via the reactive togglePanel() method) BUT the right-rail \u003caside data-state={reviewStore.panelOpen?'open':'closed'}\u003e stays data-state=closed and the {#if reviewStore.panelOpen} content (ReviewMargin, data-slot=review-margin) never mounts -\u003e comment/reply/resolve cards INVISIBLE to the reviewer. Owner is UNAFFECTED: isolated probe (owner on a folder) togglePanel() -\u003e state=open + margin mounts. So the same \u003caside\u003e binding to reviewStore.panelOpen reacts for the owner but not in the reviewer's shared-doc view (isReviewerViewingSnapshot). NOT an automation artifact (the auto-open at App.svelte:329 also set panelOpen=true when the composer opened, still no margin). Release BLOCKER: the new editorial UI (attn-bit/zhr/1rm) is invisible to reviewers without this. Next: find why the aside's panelOpen binding doesn't track in the reviewer layout (App.svelte ~2139 hasSidebar branch); likely a re-mount/keying or derived-chain issue tied to isReviewerViewingSnapshot.","status":"closed","priority":0,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-23T06:03:41Z","created_by":"James Lal","updated_at":"2026-05-23T18:05:10Z","started_at":"2026-05-23T15:10:43Z","closed_at":"2026-05-23T18:05:10Z","close_reason":"Fixed: ReviewMarginCard had a 'state' prop, so the $state rune added for the reply composer (attn-1rm) compiled as store auto-subscription (store_get($$props.state)) -\u003e 'state.subscribe is not a function' thrown on every card render -\u003e margin overlay never mounted (cards invisible). Renamed prop state-\u003ecardState + 2 call sites. Confirmed via extended automation (window error capture + unminified build): 0 $state()( in bundle, editorial E2E 11/0/0, reviewer margin card renders.","dependencies":[{"issue_id":"attn-cqk","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-23T00:03:41Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-7qv","title":"send_collab skip-relay optimization drops data when mesh is partial/flaky","description":"THE no-TURN fix. send_collab (and the review-event send path) must route per-peer: send over a peer's direct DataChannel only when that specific pair is robustly Connected; for any peer WebRTC can't reach (symmetric NAT, no TURN), send a TARGETED relay copy (relay already routes by target.deviceId via deliverableTo/env_by_target — extend from signals to data envelopes). Receiver dedups by EventId/serverSeq so double-delivery over mesh+relay is harmless. Replaces the current all-or-nothing 'mesh complete -\u003e skip relay' gate that silently drops un-meshed peers. Validated by the symmetric-NAT Docker harness (attn-orf).","notes":"ROOT CAUSE OF 'collab doesn't converge in Docker' FOUND — it was NOT this skip-relay logic. It was a re-entrant agent deadlock (see attn-orf, commit f60cde1). With that fixed, collab converges 4/4 and routes channels=true relay=false (complete mesh, WebRTC-primary) — exactly the 'mesh complete -\u003e skip relay' path. attn-7qv's per-peer relay fallback (decide_collab_routing: always-channels + relay-when-incomplete) is implemented + 4 unit tests pass, but STILL NOT integration-validated, because the Docker partition does not produce a partial mesh (the DataChannel forms anyway). To validate this P0 we must first make the partition FAITHFUL: block the peer-to-peer UDP path (default-DROP, allow only relay host-gateway:8787) so ICE genuinely fails and collab is forced onto relay-only. Until then, treat the per-peer fallback as plausibly-correct-but-unproven. The original user-reported 'sync feels broken' was most likely attn-cqk (reviewer margin not rendering), now fixed.","status":"closed","priority":0,"issue_type":"bug","owner":"james@littlebearlabs.io","created_at":"2026-05-23T02:18:06Z","created_by":"James Lal","updated_at":"2026-05-23T20:52:32Z","closed_at":"2026-05-23T20:52:32Z","close_reason":"VALIDATED end-to-end (commit). Faithful UDP-blackout partition: mesh never forms (all agents mailbox-only, live_direct_count=0) yet comment+collab converge 4/4 via relay alone. Baseline converges WebRTC-primary. Per-peer relay fallback proven for no-TURN symmetric-NAT.","dependencies":[{"issue_id":"attn-7qv","depends_on_id":"attn-k3v","type":"parent-child","created_at":"2026-05-22T22:47:13Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-7qv","depends_on_id":"attn-ms7","type":"blocks","created_at":"2026-05-23T13:20:30Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"attn-cqk","title":"Review margin/panel does not mount on panelOpen toggle (reviewer can't see cards)","description":"Found via real daemon E2E (attn --eval/--query). In a reviewer's shared-doc view, reviewStore.panelOpen flips to true (Cmd+J -\u003e togglePanel(), and the auto-open at App.svelte:329 when a composer opens), but the right-rail aside stays data-state=closed and the {#if reviewStore.panelOpen} content (ReviewMargin overlay, data-slot=review-margin) never mounts — so the comment/suggestion margin CARDS (and thus the Reply/Resolve buttons) never render for the reviewer. The store value reads true; the \u003caside\u003e in the {:else if hasSidebar} branch (App.svelte:2139-2177) doesn't react. Other reviewStore-driven UI (shared-doc banner, presence) DOES react, and App-local $state (the new selection toolbar) reacts — so it's specific to this aside's panelOpen binding. Repro: scripts/test-editorial-e2e.sh ('no margin card rendered'). Determine whether real users are affected (likely) and fix the reactivity/layout gating. Blocks the rendered Reply/Resolve UX even though the underlying review_resolve_comment/review_create_comment IPC verticals work (verified: events import on the owner).","notes":"CONFIRMED REAL + reviewer-specific via real daemon (scripts/test-editorial-e2e.sh + isolated probe). Reviewer in shared-doc view: panelOpen=true (set via the reactive togglePanel() method) BUT the right-rail \u003caside data-state={reviewStore.panelOpen?'open':'closed'}\u003e stays data-state=closed and the {#if reviewStore.panelOpen} content (ReviewMargin, data-slot=review-margin) never mounts -\u003e comment/reply/resolve cards INVISIBLE to the reviewer. Owner is UNAFFECTED: isolated probe (owner on a folder) togglePanel() -\u003e state=open + margin mounts. So the same \u003caside\u003e binding to reviewStore.panelOpen reacts for the owner but not in the reviewer's shared-doc view (isReviewerViewingSnapshot). NOT an automation artifact (the auto-open at App.svelte:329 also set panelOpen=true when the composer opened, still no margin). Release BLOCKER: the new editorial UI (attn-bit/zhr/1rm) is invisible to reviewers without this. Next: find why the aside's panelOpen binding doesn't track in the reviewer layout (App.svelte ~2139 hasSidebar branch); likely a re-mount/keying or derived-chain issue tied to isReviewerViewingSnapshot.","status":"closed","priority":0,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-23T06:03:41Z","created_by":"James Lal","updated_at":"2026-05-23T18:05:10Z","closed_at":"2026-05-23T18:05:10Z","close_reason":"Fixed: ReviewMarginCard had a 'state' prop, so the $state rune added for the reply composer (attn-1rm) compiled as store auto-subscription (store_get($$props.state)) -\u003e 'state.subscribe is not a function' thrown on every card render -\u003e margin overlay never mounted (cards invisible). Renamed prop state-\u003ecardState + 2 call sites. Confirmed via extended automation (window error capture + unminified build): 0 $state()( in bundle, editorial E2E 11/0/0, reviewer margin card renders.","dependencies":[{"issue_id":"attn-cqk","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-23T00:03:41Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-7qv","title":"send_collab skip-relay optimization drops data when mesh is partial/flaky","description":"THE no-TURN fix. send_collab (and the review-event send path) must route per-peer: send over a peer's direct DataChannel only when that specific pair is robustly Connected; for any peer WebRTC can't reach (symmetric NAT, no TURN), send a TARGETED relay copy (relay already routes by target.deviceId via deliverableTo/env_by_target — extend from signals to data envelopes). Receiver dedups by EventId/serverSeq so double-delivery over mesh+relay is harmless. Replaces the current all-or-nothing 'mesh complete -\u003e skip relay' gate that silently drops un-meshed peers. Validated by the symmetric-NAT Docker harness (attn-orf).","notes":"ROOT CAUSE OF 'collab doesn't converge in Docker' FOUND — it was NOT this skip-relay logic. It was a re-entrant agent deadlock (see attn-orf, commit f60cde1). With that fixed, collab converges 4/4 and routes channels=true relay=false (complete mesh, WebRTC-primary) — exactly the 'mesh complete -\u003e skip relay' path. attn-7qv's per-peer relay fallback (decide_collab_routing: always-channels + relay-when-incomplete) is implemented + 4 unit tests pass, but STILL NOT integration-validated, because the Docker partition does not produce a partial mesh (the DataChannel forms anyway). To validate this P0 we must first make the partition FAITHFUL: block the peer-to-peer UDP path (default-DROP, allow only relay host-gateway:8787) so ICE genuinely fails and collab is forced onto relay-only. Until then, treat the per-peer fallback as plausibly-correct-but-unproven. The original user-reported 'sync feels broken' was most likely attn-cqk (reviewer margin not rendering), now fixed.","status":"closed","priority":0,"issue_type":"bug","owner":"james@littlebearlabs.io","created_at":"2026-05-23T02:18:06Z","created_by":"James Lal","updated_at":"2026-05-23T20:52:32Z","closed_at":"2026-05-23T20:52:32Z","close_reason":"VALIDATED end-to-end (commit). Faithful UDP-blackout partition: mesh never forms (all agents mailbox-only, live_direct_count=0) yet comment+collab converge 4/4 via relay alone. Baseline converges WebRTC-primary. Per-peer relay fallback proven for no-TURN symmetric-NAT.","dependencies":[{"issue_id":"attn-7qv","depends_on_id":"attn-k3v","type":"parent-child","created_at":"2026-05-22T22:47:13Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-7qv","depends_on_id":"attn-ms7","type":"blocks","created_at":"2026-05-23T13:20:30Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"attn-zb5","title":"In-app HTML file viewing","description":"Display/present HTML files in-app alongside the existing markdown/image/video/audio viewers, primarily to showcase AI-generated HTML (often interactive: inline JS, custom web fonts, CDN animation libraries) — while keeping the user safe.\n\nRender `.html`/`.htm` in a native `\u003ciframe sandbox=\"allow-scripts\"\u003e` (no `allow-same-origin`) loaded via the existing `attn://` custom protocol. Zero new dependencies → no binary-size impact (32 MiB gate).\n\n## Success Criteria\n- [ ] Opening an `.html` from the sidebar renders it in-app; its JS runs; custom fonts + CDN animation libs (Google Fonts, GSAP, anime.js, Lottie) load and it looks polished.\n- [ ] Embedded scripts cannot reach `window.ipc`, write files, or read other local files.\n- [ ] Editing the file on disk live-updates the view.\n- [ ] Release binary stays \u003c 32 MiB; collab/share chrome stays hidden on html tabs.\n\n## Architecture (3 independent safety controls)\n- Iframe sandbox = web boundary (opaque origin; no app DOM/storage access).\n- IPC-bridge fencing = native boundary (subframe guard + main-frame-only capability token gates file-writing IPC).\n- CSP on html responses = content policy (allow https/data fonts+libs; exclude `attn:` from connect-src so JS can't read local file bytes; drop ACAO `*`).\n\n## Plan Reference\n- planning/one-pager.md\n- planning/complete-plan.md","status":"closed","priority":1,"issue_type":"epic","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:10:53Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:54:27Z","closed_at":"2026-05-28T20:54:27Z","close_reason":"HTML viewer implemented, adversarially reviewed (9 findings fixed), and E2E-verified: iframe renders, token blocks embedded-HTML IPC, live-reload works, size 30.20MiB\u003c32","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"attn-syv","title":"Per-file live collab for folder shares (every file independently co-editable)","description":"Owner chose 'every file live at once' for folder shares. Today a room has ONE live doc (CollabAuthority seeded once; submit/broadcast carry no fileId); folder shares create read-only snapshots only, the owner has no file switcher, and ShareDialog short-circuits a second share to re-show the existing invite.\n\nBuild: (1) fileId-tagged collab wire (submit/broadcast) + new resync request; owner holds Map\u003cfileId,CollabHost\u003e; reviewer single live client per active file; switch/join seeds at v0 from the file's BASE snapshot and replays stepsSince(0) (reuses idempotent receive skip). (2) App.svelte+Editor: per-file seed (base snapshot, not latest), re-seed editor collab doc on currentFileId change. (3) owner file switcher (wire sidebar nav -\u003e setCurrentFile). (4) ShareDialog: sharing a different target switches the room over (mint, not re-show). (5) E2E + release.\n\nParent epic: attn-07i (collab editorial UX).","status":"open","priority":1,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-25T16:01:40Z","created_by":"James Lal","updated_at":"2026-05-25T16:01:40Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"attn-d5x","title":"Sharing a single markdown file blanks the owner's editor (no active tab)","description":"On 0.6.2, sharing the file the owner is currently viewing leaves the editor pane blank with the 'Title' placeholder (no active tab). effectiveMarkdown for an owner = hasActiveTab ? rawMarkdown : '' — blank implies the active tab/rawMarkdown is being lost on share, OR collab activation seeds the editor empty. Reproduce + fix so a single-file share always keeps the owner focused on the shared file.","status":"closed","priority":1,"issue_type":"bug","owner":"james@littlebearlabs.io","created_at":"2026-05-24T15:25:42Z","created_by":"James Lal","updated_at":"2026-05-24T15:43:25Z","closed_at":"2026-05-24T15:43:25Z","close_reason":"Fixed + verified in 2a69ca9: collabSeedReady gate (unit tests) for the blank editor; folder ContextMenu + share-target wiring (live-daemon verified) for folder sharing.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-07i.2","title":"(EPIC) Inline suggesting mode (track changes) on prosemirror-suggest-changes","description":"Replace direct reviewer co-typing with Google-Docs-style inline suggestions built on @handlewithcare/prosemirror-suggest-changes (MIT, marks-based, composes with our prosemirror-collab — it skips collab$/history$ trs). Reviewers type in 'suggesting mode' → insertion/deletion marks; owner sees attributed inline suggestions and accept/denies each; owner's file only ever contains accepted content. Attribution via author-encoded suggestionId (marks only carry id). Phases: (1) schema marks + suggestChanges plugin + withSuggestChanges dispatch + reviewer suggesting-on + CSS; (2) owner accept/reject UI + attribution; (3) save-clean-view (file=accepted only) + persistence across reconnect/snapshot. User: no users yet, fine to cut over; can fork the lib later.","notes":"PHASE 1 DONE + verified (reviewer suggestions; file stays clean; live 11/0). PHASE 2 IMPLEMENTED (owner UX), verification partial: SuggestionPopover (click an inline suggestion -\u003e author + Accept/Reject/Comment), attribution decoded from the author-encoded suggestion id, accept=applySuggestion / reject=revertSuggestion via the view, comment=select range + open comment composer. Confirmed via instrumentation that the suggestion RENDERS inline with the correct attribution id (\u003cins data-id='Reviewer~...'\u003eRVDC\u003c/ins\u003e). GAP: the click-to-accept E2E couldn't be asserted because window.__attnPmView is a DETACHED view late in the test flow (document.querySelectorAll('.ProseMirror')=0 on the owner) — i.e. the owner's attached/visible editor vs the collab view that holds the suggestions. NEXT: (a) investigate the owner editor-view attachment (does the owner's VISIBLE editor show suggestions in normal use? likely a dual-Editor-instance / __attnPmView staleness issue), (b) E2E the click-accept once attachment is sorted, (c) fix commenting on a live co-typed/marked doc. PHASE 3 = marked-snapshot persistence.","status":"open","priority":1,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-23T23:38:26Z","created_by":"James Lal","updated_at":"2026-05-24T00:34:07Z","dependencies":[{"issue_id":"attn-07i.2","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-23T17:38:25Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-07i.1","title":"Onboarding: user display name (prompt on first share/join, default from git)","description":"Participants currently appear as generic 'Reviewer'/'Owner'/'Agent' or raw 22-char participant IDs (bootstrap.rs:1017 sets display_name=participant_id; manager.rs:2830 hardcodes kind labels). Add a real display name.\n\nDESIGN (confirmed with user): store displayName in identity.json (DeviceIdentity); resolve a default from git config user.name -\u003e macOS full name (id -F) -\u003e $USER -\u003e 'Anonymous'; show a ONE-TIME in-app modal the first time the user shares OR receives/joins a share, pre-filled with the default, editable later via a profile affordance (NO window.prompt — in-app UI per CLAUDE.md). Thread the chosen name into Participant.display_name so peers see it; frontend prefers the Participant name over the kind label.\n\nPHASES: (1) Rust: DeviceIdentity.display_name + resolve_default_display_name() + get/set-name IPC + include name+default+isSet in init payload + use it in Participant.display_name. (2) Frontend: profile state from init; NamePrompt modal on first share/join; edit-later affordance; prefer Participant.display_name in peer/author resolution. (3) E2E: extend editorial test to set a name and assert the peer sees it.","notes":"CORE SHIPPED (commit 531ebaa on branch editorial-ux-and-collab-sync; NOT yet pushed to main). Rust: DeviceIdentity.display_name + resolve_default_display_name (git user.name -\u003e macOS id -F -\u003e $USER -\u003e Anonymous) + set/load helpers + Participant.display_name uses effective name + review_set_display_name IPC + reviewProfile in init; 4 unit tests. Frontend: userProfile rune store, NamePrompt in-app modal pre-filled with default, one-time prompt intercepts first Share + fires once on first join, Edit affordance in connection-badge popover. Editorial E2E 13-\u003e17. FOLLOW-UP (not blocking): changing the name AFTER already joining/sharing does not retroactively update the published ParticipantJoined (needs a re-publish/profile-update event); mitigated since the auto-default already publishes the real git/OS name and the Share prompt fires BEFORE the owner's publish.","status":"in_progress","priority":1,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-23T22:29:21Z","created_by":"James Lal","updated_at":"2026-05-23T22:46:25Z","started_at":"2026-05-23T22:46:25Z","dependencies":[{"issue_id":"attn-07i.1","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-23T16:29:21Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-07i.2","title":"(EPIC) Inline suggesting mode (track changes) on prosemirror-suggest-changes","description":"Replace direct reviewer co-typing with Google-Docs-style inline suggestions built on @handlewithcare/prosemirror-suggest-changes (MIT, marks-based, composes with our prosemirror-collab — it skips collab$/history$ trs). Reviewers type in 'suggesting mode' → insertion/deletion marks; owner sees attributed inline suggestions and accept/denies each; owner's file only ever contains accepted content. Attribution via author-encoded suggestionId (marks only carry id). Phases: (1) schema marks + suggestChanges plugin + withSuggestChanges dispatch + reviewer suggesting-on + CSS; (2) owner accept/reject UI + attribution; (3) save-clean-view (file=accepted only) + persistence across reconnect/snapshot. User: no users yet, fine to cut over; can fork the lib later.","notes":"PHASE 1 DONE + verified (reviewer suggestions; file stays clean; live 11/0). PHASE 2 IMPLEMENTED (owner UX), verification partial: SuggestionPopover (click an inline suggestion -\u003e author + Accept/Reject/Comment), attribution decoded from the author-encoded suggestion id, accept=applySuggestion / reject=revertSuggestion via the view, comment=select range + open comment composer. Confirmed via instrumentation that the suggestion RENDERS inline with the correct attribution id (\u003cins data-id='Reviewer~...'\u003eRVDC\u003c/ins\u003e). GAP: the click-to-accept E2E couldn't be asserted because window.__attnPmView is a DETACHED view late in the test flow (document.querySelectorAll('.ProseMirror')=0 on the owner) — i.e. the owner's attached/visible editor vs the collab view that holds the suggestions. NEXT: (a) investigate the owner editor-view attachment (does the owner's VISIBLE editor show suggestions in normal use? likely a dual-Editor-instance / __attnPmView staleness issue), (b) E2E the click-accept once attachment is sorted, (c) fix commenting on a live co-typed/marked doc. PHASE 3 = marked-snapshot persistence.","status":"open","priority":1,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-23T23:38:26Z","created_by":"James Lal","updated_at":"2026-05-24T00:34:07Z","dependencies":[{"issue_id":"attn-07i.2","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-23T17:38:25Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-07i.1","title":"Onboarding: user display name (prompt on first share/join, default from git)","description":"Participants currently appear as generic 'Reviewer'/'Owner'/'Agent' or raw 22-char participant IDs (bootstrap.rs:1017 sets display_name=participant_id; manager.rs:2830 hardcodes kind labels). Add a real display name.\n\nDESIGN (confirmed with user): store displayName in identity.json (DeviceIdentity); resolve a default from git config user.name -\u003e macOS full name (id -F) -\u003e $USER -\u003e 'Anonymous'; show a ONE-TIME in-app modal the first time the user shares OR receives/joins a share, pre-filled with the default, editable later via a profile affordance (NO window.prompt — in-app UI per CLAUDE.md). Thread the chosen name into Participant.display_name so peers see it; frontend prefers the Participant name over the kind label.\n\nPHASES: (1) Rust: DeviceIdentity.display_name + resolve_default_display_name() + get/set-name IPC + include name+default+isSet in init payload + use it in Participant.display_name. (2) Frontend: profile state from init; NamePrompt modal on first share/join; edit-later affordance; prefer Participant.display_name in peer/author resolution. (3) E2E: extend editorial test to set a name and assert the peer sees it.","notes":"CORE SHIPPED (commit 531ebaa on branch editorial-ux-and-collab-sync; NOT yet pushed to main). Rust: DeviceIdentity.display_name + resolve_default_display_name (git user.name -\u003e macOS id -F -\u003e $USER -\u003e Anonymous) + set/load helpers + Participant.display_name uses effective name + review_set_display_name IPC + reviewProfile in init; 4 unit tests. Frontend: userProfile rune store, NamePrompt in-app modal pre-filled with default, one-time prompt intercepts first Share + fires once on first join, Edit affordance in connection-badge popover. Editorial E2E 13-\u003e17. FOLLOW-UP (not blocking): changing the name AFTER already joining/sharing does not retroactively update the published ParticipantJoined (needs a re-publish/profile-update event); mitigated since the auto-default already publishes the real git/OS name and the Share prompt fires BEFORE the owner's publish.","status":"in_progress","priority":1,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-23T22:29:21Z","created_by":"James Lal","updated_at":"2026-05-23T22:46:25Z","dependencies":[{"issue_id":"attn-07i.1","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-23T16:29:21Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"attn-j5m","title":"Connection badge shows Offline though WebRTC is connected (live_direct dropped when owner is on local doc)","description":"SYMPTOM: In a 2-party GUI share (owner + reviewer), the connection badge shows 'Offline' even though the WebRTC DataChannel is fully connected and carrying collab data. This is almost certainly the source of the user-reported 'sync feels broken / WebRTC isn't working' perception — WebRTC IS primary and connected; the badge just doesn't show it.\n\nEVIDENCE (test:webrtc:live + temporary tracing): the DataChannel connects in ~100ms on localhost (create_offer -\u003e badge-eval connected=1 = 100ms). The Rust manager correctly computes all_live (peers=1, transports=1, connected=1) and emits ReviewUpdate::ConnectionChanged{connection:'live_direct'}. Co-typing edits and a review comment both propagate. Only the badge is wrong (data-state stays 'offline'). So the bug is purely frontend.\n\nROOT CAUSE: web/src/lib/review/store.svelte.ts applyConnection() updates the active badge (this.connection) ONLY when currentRoomId === payload.roomId (early-returns otherwise). The OWNER, by design (attn-0wa), stays on its LOCAL doc rather than flipping to the shared-room view, so currentRoomId is not the shared room; the 'live_direct' update is recorded on the per-room record via upsertRoom but never reaches this.connection, so the global badge stays at its 'offline' default. (Reviewer also showed offline in test:webrtc:live — confirm whether its currentRoomId is set by the raw review_join IPC path vs the UI join used by the editorial E2E.)\n\nRECOMMENDED FIX: the connection badge should reflect the connection state of the room the user is actually participating in / sharing, independent of which doc the editor is showing. Options: (a) when the owner shares, treat the shared room as the badge's room even while viewing the local doc; (b) have the badge read the per-room connection (upsertRoom already stores it) for the active share rather than only this.connection gated on currentRoomId. Decide the desired UX for 'owner viewing local doc while sharing'. Add a badge assertion to scripts/test-editorial-e2e.sh and make scripts/test-webrtc-live-e2e.sh green.\n\nNOTE: functionally non-blocking (collab converges, WebRTC is primary), but high perceived-severity — it makes working WebRTC look broken.","status":"closed","priority":1,"issue_type":"bug","owner":"james@littlebearlabs.io","created_at":"2026-05-23T21:17:07Z","created_by":"James Lal","updated_at":"2026-05-23T22:08:49Z","closed_at":"2026-05-23T22:08:49Z","close_reason":"Fixed in d2fbe17 (on main). applyConnection now keeps status.connection in sync with this.connection, so the ConnectionBadge no longer reads a stale 'offline'. Verified: test:webrtc:live 7→9/9 (owner+reviewer badges flip to live_direct), editorial E2E 13/0/0, svelte-check 0 errors/0 warnings.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-ms7","title":"Faithful no-TURN partition: block peer UDP so ICE fails (validates attn-7qv)","description":"The attn-orf 'partition' scenario applies iptables DROP between agent container IPs, but the WebRTC DataChannel still forms (connected=2) — ICE finds a path the rule doesn't cover (likely srflx via host-gateway, or an iptables-nft backend mismatch in debian-slim). Result: relay-only fallback is never exercised, so attn-7qv's per-peer routing is unvalidated. Make the partition truly sever the peer-to-peer path: default-DROP all egress/ingress, ALLOW only the relay (host.docker.internal:8787 TCP for WS+outbox) and loopback; verify direct UDP between agents is dead (e.g. ICE never reaches Connected). Then assert collab + comments STILL converge across owner/rvB/rvC via relay only. That run is the real no-TURN symmetric-NAT repro and the validation gate for attn-7qv.","status":"closed","priority":1,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-23T19:20:19Z","created_by":"James Lal","updated_at":"2026-05-23T20:52:20Z","started_at":"2026-05-23T20:49:26Z","closed_at":"2026-05-23T20:52:20Z","close_reason":"Done: partition now blocks ALL UDP (path-independent), with a no-live_direct faithfulness assert. Mesh provably can't form; relay-only convergence confirmed.","dependencies":[{"issue_id":"attn-ms7","depends_on_id":"attn-k3v","type":"parent-child","created_at":"2026-05-23T13:20:30Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"attn-ms7","title":"Faithful no-TURN partition: block peer UDP so ICE fails (validates attn-7qv)","description":"The attn-orf 'partition' scenario applies iptables DROP between agent container IPs, but the WebRTC DataChannel still forms (connected=2) — ICE finds a path the rule doesn't cover (likely srflx via host-gateway, or an iptables-nft backend mismatch in debian-slim). Result: relay-only fallback is never exercised, so attn-7qv's per-peer routing is unvalidated. Make the partition truly sever the peer-to-peer path: default-DROP all egress/ingress, ALLOW only the relay (host.docker.internal:8787 TCP for WS+outbox) and loopback; verify direct UDP between agents is dead (e.g. ICE never reaches Connected). Then assert collab + comments STILL converge across owner/rvB/rvC via relay only. That run is the real no-TURN symmetric-NAT repro and the validation gate for attn-7qv.","status":"closed","priority":1,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-23T19:20:19Z","created_by":"James Lal","updated_at":"2026-05-23T20:52:20Z","closed_at":"2026-05-23T20:52:20Z","close_reason":"Done: partition now blocks ALL UDP (path-independent), with a no-live_direct faithfulness assert. Mesh provably can't form; relay-only convergence confirmed.","dependencies":[{"issue_id":"attn-ms7","depends_on_id":"attn-k3v","type":"parent-child","created_at":"2026-05-23T13:20:30Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} {"id":"attn-x06","title":"No TURN server: WebRTC mesh fails across symmetric NAT (cross-machine desync root cause)","description":"STUN-only, no TURN (webrtc.rs DEFAULT_STUN_SERVER only; security-review.md + relay-spec.md confirm 'no TURN server stood up / STUN only'). Symmetric-NAT peer-pairs can't form direct DataChannels -\u003e partial WebRTC mesh across real networks -\u003e asymmetric drops in both co-typing and comments. User confirms desync is across-machines for both. Fix: stand up a TURN server (coturn or a managed TURN), plumb TURN URLs+short-lived credentials into WebRtcConfig.ice_servers(), and add ice_transport_policy handling. This is the real NAT-traversal fix.","status":"closed","priority":1,"issue_type":"bug","owner":"james@littlebearlabs.io","created_at":"2026-05-23T02:18:05Z","created_by":"James Lal","updated_at":"2026-05-23T02:19:05Z","closed_at":"2026-05-23T02:19:05Z","close_reason":"User constraint: NO TURN, ever. NAT traversal for symmetric-NAT peers must be solved via a reliable per-peer relay data-fallback instead (attn-7qv), not TURN.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-orf","title":"Docker topology matrix harness (NAT / loss / partition / relay-only)","description":"Compose: relay container + N headless-agent containers + a coordinator. Use network namespaces + tc netem/iptables to exercise NAT traversal, packet loss/latency, partition+reconnect, and relay-only fallback (block UDP to force WS). Assert events.jsonl + doc convergence across all peers per topology. Depends on T3.","notes":"NOW CONVERGES 4/4 in baseline AND partition (commit f60cde1). Two fixes were needed, BOTH unrelated to the transport: (1) re-entrant mutex deadlock in the headless agent — handle_line held current_room across manager.submit (MutexGuard in match scrutinee lives the whole block), and submit's synchronous EventImported sink re-locks the same mutex -\u003e deadlock after the first comment, starving all later commands incl. collab; (2) control channel: docker -i stdin AND bind-mounted FIFO/file all drop rapid writes on Docker Desktop (FUSE attr-cache staleness / no streaming) — replaced with a CONTAINER-LOCAL file the agent polls (ATTN_AGENT_CMD_FILE) that the harness appends to via docker exec. Verified collab is WebRTC-primary: sender routes channels=true relay=false, peers receive Collab over the DataChannel. The earlier owner-Share-hang is gone with these fixes. REMAINING GAP: the 'partition' scenario does NOT actually sever the mesh (connected=2 even under iptables DROP of inter-agent IPs — ICE finds a path around it), so relay-only fallback is NOT yet exercised. See follow-up bead for a faithful UDP/peer-path block.","status":"closed","priority":1,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-23T02:03:11Z","created_by":"James Lal","updated_at":"2026-05-23T20:52:22Z","closed_at":"2026-05-23T20:52:22Z","close_reason":"Harness built, trustworthy, and converges: baseline (WebRTC mesh) + faithful partition (UDP blackout, relay-only) both 4/4. Reliable container-local control channel; deadlock root-caused+fixed. NAT/loss profiles remain as future netem work but the core partition/relay-only matrix is proven.","dependencies":[{"issue_id":"attn-orf","depends_on_id":"attn-8zd","type":"blocks","created_at":"2026-05-22T20:03:18Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-orf","depends_on_id":"attn-k3v","type":"parent-child","created_at":"2026-05-22T22:47:15Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"id":"attn-woc","title":"Fix: fan review events across the WebRTC mesh (parity with send_collab)","description":"FINDING (attn-sls): review events already converge to all peers via the relay in all modes on localhost, so this is NOT a drop-fix. It remains valid as the user's stated intent ('as much over WebRTC as possible; WS mostly for signaling'): optionally route review events over the live_webrtc mesh (parity with send_collab) to cut relay cost/latency, with relay as the fallback when the mesh is incomplete. Re-prioritize after the topology-layer repro (attn-orf) shows whether mesh peers actually drop events under partition/NAT.","status":"closed","priority":1,"issue_type":"bug","owner":"james@littlebearlabs.io","created_at":"2026-05-23T02:03:08Z","created_by":"James Lal","updated_at":"2026-05-23T19:20:43Z","closed_at":"2026-05-23T19:20:43Z","close_reason":"Landed in 32b2dd7; validated in the Docker harness — review-event Envelopes (kind=Event) are delivered over the WebRTC DataChannel to all connected peers (inbound Signal/Event confirmed), with the relay/outbox still covering un-meshed peers.","dependencies":[{"issue_id":"attn-woc","depends_on_id":"attn-k3v","type":"parent-child","created_at":"2026-05-22T22:47:14Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-woc","depends_on_id":"attn-sls","type":"blocks","created_at":"2026-05-22T20:03:18Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"id":"attn-sls","title":"Deterministic in-process mesh-convergence test (reproduce review-event drop)","description":"Build a CI-friendly, no-UDP harness: in-process mesh bus + injected mock WebRtcSender/MailboxSender (selector already supports handle injection) driving N ReviewManager peers. Assert that BOTH a collab step AND a review event from reviewerB converge to reviewerC. The review-event assertion FAILS today (red test reproducing the bug); collab passes. Lock the mock-relay deliverableTo contract: event-\u003eall subscribers, signal target=null-\u003eall, signal target=X-\u003eX only.","notes":"DONE: tests/review_sync_convergence.rs landed. 3-peer, real Miniflare relay (gated by ATTN_SKIP_CONFORMANCE/wrangler), no UDP. Asserts reviewerB comment converges to owner+reviewerC in live/hybrid/async. ALL PASS -\u003e rules out the relay-mediated review-event layer as the cause of asymmetric loss. Reusable as a regression guard and as the harness skeleton for topology fault-injection.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-23T02:03:06Z","created_by":"James Lal","updated_at":"2026-05-23T02:13:17Z","started_at":"2026-05-23T02:03:19Z","closed_at":"2026-05-23T02:13:17Z","close_reason":"Deterministic in-process convergence harness built and passing; relay-mediated review-event path proven sound on localhost in all modes. Bug localized away from this layer.","dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"attn-orf","title":"Docker topology matrix harness (NAT / loss / partition / relay-only)","description":"Compose: relay container + N headless-agent containers + a coordinator. Use network namespaces + tc netem/iptables to exercise NAT traversal, packet loss/latency, partition+reconnect, and relay-only fallback (block UDP to force WS). Assert events.jsonl + doc convergence across all peers per topology. Depends on T3.","notes":"NOW CONVERGES 4/4 in baseline AND partition (commit f60cde1). Two fixes were needed, BOTH unrelated to the transport: (1) re-entrant mutex deadlock in the headless agent — handle_line held current_room across manager.submit (MutexGuard in match scrutinee lives the whole block), and submit's synchronous EventImported sink re-locks the same mutex -\u003e deadlock after the first comment, starving all later commands incl. collab; (2) control channel: docker -i stdin AND bind-mounted FIFO/file all drop rapid writes on Docker Desktop (FUSE attr-cache staleness / no streaming) — replaced with a CONTAINER-LOCAL file the agent polls (ATTN_AGENT_CMD_FILE) that the harness appends to via docker exec. Verified collab is WebRTC-primary: sender routes channels=true relay=false, peers receive Collab over the DataChannel. The earlier owner-Share-hang is gone with these fixes. REMAINING GAP: the 'partition' scenario does NOT actually sever the mesh (connected=2 even under iptables DROP of inter-agent IPs — ICE finds a path around it), so relay-only fallback is NOT yet exercised. See follow-up bead for a faithful UDP/peer-path block.","status":"closed","priority":1,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-23T02:03:11Z","created_by":"James Lal","updated_at":"2026-05-23T20:52:22Z","closed_at":"2026-05-23T20:52:22Z","close_reason":"Harness built, trustworthy, and converges: baseline (WebRTC mesh) + faithful partition (UDP blackout, relay-only) both 4/4. Reliable container-local control channel; deadlock root-caused+fixed. NAT/loss profiles remain as future netem work but the core partition/relay-only matrix is proven.","dependencies":[{"issue_id":"attn-orf","depends_on_id":"attn-8zd","type":"blocks","created_at":"2026-05-22T20:03:18Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-orf","depends_on_id":"attn-k3v","type":"parent-child","created_at":"2026-05-22T22:47:15Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"attn-woc","title":"Fix: fan review events across the WebRTC mesh (parity with send_collab)","description":"FINDING (attn-sls): review events already converge to all peers via the relay in all modes on localhost, so this is NOT a drop-fix. It remains valid as the user's stated intent ('as much over WebRTC as possible; WS mostly for signaling'): optionally route review events over the live_webrtc mesh (parity with send_collab) to cut relay cost/latency, with relay as the fallback when the mesh is incomplete. Re-prioritize after the topology-layer repro (attn-orf) shows whether mesh peers actually drop events under partition/NAT.","status":"closed","priority":1,"issue_type":"bug","owner":"james@littlebearlabs.io","created_at":"2026-05-23T02:03:08Z","created_by":"James Lal","updated_at":"2026-05-23T19:20:43Z","closed_at":"2026-05-23T19:20:43Z","close_reason":"Landed in 32b2dd7; validated in the Docker harness — review-event Envelopes (kind=Event) are delivered over the WebRTC DataChannel to all connected peers (inbound Signal/Event confirmed), with the relay/outbox still covering un-meshed peers.","dependencies":[{"issue_id":"attn-woc","depends_on_id":"attn-k3v","type":"parent-child","created_at":"2026-05-22T22:47:14Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-woc","depends_on_id":"attn-sls","type":"blocks","created_at":"2026-05-22T20:03:18Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"attn-sls","title":"Deterministic in-process mesh-convergence test (reproduce review-event drop)","description":"Build a CI-friendly, no-UDP harness: in-process mesh bus + injected mock WebRtcSender/MailboxSender (selector already supports handle injection) driving N ReviewManager peers. Assert that BOTH a collab step AND a review event from reviewerB converge to reviewerC. The review-event assertion FAILS today (red test reproducing the bug); collab passes. Lock the mock-relay deliverableTo contract: event-\u003eall subscribers, signal target=null-\u003eall, signal target=X-\u003eX only.","notes":"DONE: tests/review_sync_convergence.rs landed. 3-peer, real Miniflare relay (gated by ATTN_SKIP_CONFORMANCE/wrangler), no UDP. Asserts reviewerB comment converges to owner+reviewerC in live/hybrid/async. ALL PASS -\u003e rules out the relay-mediated review-event layer as the cause of asymmetric loss. Reusable as a regression guard and as the harness skeleton for topology fault-injection.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-23T02:03:06Z","created_by":"James Lal","updated_at":"2026-05-23T02:13:17Z","closed_at":"2026-05-23T02:13:17Z","close_reason":"Deterministic in-process convergence harness built and passing; relay-mediated review-event path proven sound on localhost in all modes. Bug localized away from this layer.","dependency_count":0,"dependent_count":1,"comment_count":0} {"id":"attn-k3v","title":"Bulletproof collab sync testing + fix asymmetric review-event loss","description":"Multi-user (3+) collab drops review events (comments/suggestions) asymmetrically: a non-owner reviewer's comment reaches the owner but not other reviewers. Root cause: review events route through selector::send_envelopes over a SINGLE webrtc arm (no mesh fan-out), the owner never re-fans inbound events (manager.rs forward_transport_event:2543), and Live mode keeps no relay WS subscription (mailbox=None) so relay broadcast can't cover the gap. Co-typing is unaffected because send_collab fans across the live_webrtc mesh map. Intent: WebRTC carries data to all peers; WS is for signaling. Deliver a layered, deterministic test harness (in-process mesh-convergence first, then headless-agent + Docker topology matrix) and fix the fan-out.","notes":"ITEM 1 (top priority): cross-machine sync drop. Conflict-resolution LOGIC is solid (real OT co-typing, three-way suggestion merge, anchor remap — all tested on localhost), but on a real network the no-TURN partial mesh + send_collab's all-or-nothing 'skip relay when mesh complete' (manager.rs:1186) means OT steps AND suggestion/comment delivery can silently DROP for un-meshable peers. This is what makes the rest feel broken. Pieces: attn-7qv (P0 per-peer relay-fallback fix), attn-orf (Docker symmetric-NAT repro/guard), attn-woc (events over mesh).","status":"closed","priority":1,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-23T02:03:03Z","created_by":"James Lal","updated_at":"2026-05-23T20:52:45Z","closed_at":"2026-05-23T20:52:45Z","close_reason":"EPIC complete. All children closed: attn-8zd (headless agent), attn-orf (topology harness), attn-7qv (per-peer relay fallback, VALIDATED), attn-ms7 (faithful partition), attn-woc (mesh event fan-out). Collab is WebRTC-primary with a proven no-TURN relay fallback; the agent deadlock that masked it is fixed. Cross-topology convergence demonstrated in Docker (baseline mesh + UDP-blackout relay-only, both 4/4).","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-az7","title":"Preserve ATTN_HOME for npx-launched review daemons","status":"closed","priority":1,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-22T22:09:35Z","created_by":"James Lal","updated_at":"2026-05-23T00:24:25Z","started_at":"2026-05-22T22:21:38Z","closed_at":"2026-05-23T00:24:25Z","close_reason":"Implemented --attn-home routing, npm launcher env preservation, share target validation, and UI error surfacing; verified Rust/web gates plus relay-backed share/join and invalid-share smoke.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-az7","title":"Preserve ATTN_HOME for npx-launched review daemons","status":"closed","priority":1,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-22T22:09:35Z","created_by":"James Lal","updated_at":"2026-05-23T00:24:25Z","closed_at":"2026-05-23T00:24:25Z","close_reason":"Implemented --attn-home routing, npm launcher env preservation, share target validation, and UI error surfacing; verified Rust/web gates plus relay-backed share/join and invalid-share smoke.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"attn-4wv","title":"Sharing status implies sharing everything; show which file(s) are shared","description":"The 'Sharing' pill/status icon gives no indication of WHAT is being shared, implying the whole session/everything is shared. Surface the shared file name (and for folder shares, the file list/count) in or next to the share status.","status":"closed","priority":1,"issue_type":"bug","owner":"james@littlebearlabs.io","created_at":"2026-05-22T15:38:33Z","created_by":"James Lal","updated_at":"2026-05-22T16:07:16Z","closed_at":"2026-05-22T16:07:16Z","close_reason":"Added SharedFilesBadge.svelte next to the Share pill: shows the filename (single share) or 'N files' (folder share) with a popover listing relative paths. Verified E2E: owner shows '3 files'.","dependency_count":0,"dependent_count":1,"comment_count":0} {"id":"attn-deh","title":"Menu-bar floating items render under other UI (z-index)","description":"The review dock floating items (share, snapshot, reviewer avatar, camera) and their popovers appear UNDER other UI / clipped behind window chrome. Fix stacking context: review-bar z-index, right-rail overflow clipping, and popover layering so floating items and dropdowns sit above the document and chrome.","status":"closed","priority":1,"issue_type":"bug","owner":"james@littlebearlabs.io","created_at":"2026-05-22T15:38:33Z","created_by":"James Lal","updated_at":"2026-05-22T16:07:17Z","closed_at":"2026-05-22T16:07:17Z","close_reason":"Fixed popover clipping: PeerStrip identity card + presence tooltip now right-align (open inward) and sit at z-[60] above the dock; dock given a small top offset. Verified by screenshot — identity popover fully visible, was clipping off the right window edge. NOTE: the original 'dark bar over icons' facet may be environment-specific; asked user to confirm in their setup.","dependency_count":0,"dependent_count":1,"comment_count":0} {"id":"attn-fr8","title":"No UI navigation into shared files and subfolders","description":"A shared room can contain many files across subfolders, but there is no real navigation. Build BOTH: (1) a left-sidebar folder tree of the shared room's files when in a room, and (2) extend the top ReviewFileNav strip with folder grouping. Reviewer switches files via reviewStore.setCurrentFile(fileId).","status":"closed","priority":1,"issue_type":"bug","owner":"james@littlebearlabs.io","created_at":"2026-05-22T15:38:32Z","created_by":"James Lal","updated_at":"2026-05-22T16:07:15Z","closed_at":"2026-05-22T16:07:15Z","close_reason":"Added ReviewFileTree.svelte (sidebar folder tree built from snapshot ownerDisplayPaths via shared-tree.ts) + extended the ReviewFileNav strip with folder context. Both surfaces verified E2E (sidebar tree shows subfolder; strip lists files with 'deep/' prefix).","dependency_count":0,"dependent_count":1,"comment_count":0} {"id":"attn-xds","title":"Opening a shared link does not jump to the shared content","description":"When a reviewer joins a room, the editor stays on whatever local file was previously open instead of showing the shared document. Reviewer-side view must switch to the shared snapshot on join (role-aware: reviewer shows shared doc even with a local tab open; owner keeps local view). Show a clear 'waiting for shared content' state until the first snapshot arrives. Related to attn-0wa (owner incorrectly flips to shared-doc mode).","status":"closed","priority":1,"issue_type":"bug","owner":"james@littlebearlabs.io","created_at":"2026-05-22T15:38:32Z","created_by":"James Lal","updated_at":"2026-05-22T16:07:15Z","closed_at":"2026-05-22T16:07:15Z","close_reason":"Reviewer view is now role-based (isReviewerInRoom = in a room they didn't mint), so it shows the shared snapshot even with a local tab open, with a 'waiting for shared content' state until the first snapshot arrives. Verified E2E: reviewer renders the shared doc, not its local file.","dependency_count":0,"dependent_count":1,"comment_count":0} -{"id":"attn-jru","title":"Reviewer joins headless via --as-agent; should open a real window","description":"When sharing/joining for human review, the reviewer must NOT use 'attn review join \u003cinvite\u003e --as-agent \u003cname\u003e' which is a headless agent join (no window, no UI). The human-reviewer path should route through the windowed daemon join so a real UI opens. Fix scripts/dev-collab.sh (reviewer = windowed daemon, daemon-routed join) and ensure no human-facing flow emits --as-agent. --as-agent stays for bots/CI only.","status":"closed","priority":1,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-22T15:38:31Z","created_by":"James Lal","updated_at":"2026-05-22T16:07:14Z","started_at":"2026-05-22T15:41:31Z","closed_at":"2026-05-22T16:07:14Z","close_reason":"dev-collab.sh now routes the reviewer join to the windowed daemon (attn review join, no --as-agent); the daemon socket join was already wired to ReviewManager — removed the misleading 'stub, manager wiring pending' log. Verified: reviewer daemon joins + receives snapshots.","dependency_count":0,"dependent_count":1,"comment_count":0} -{"id":"attn-lms","title":"Sharing experience bug sweep","description":"Umbrella for the urgent sharing/collab UX bugs found 2026-05-22: headless --as-agent join, reviewer not auto-opening shared content, missing shared-file navigation, menu-bar z-index clipping, and ambiguous sharing status.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-22T15:38:30Z","created_by":"James Lal","updated_at":"2026-05-23T20:49:25Z","started_at":"2026-05-22T15:41:31Z","closed_at":"2026-05-23T20:49:25Z","close_reason":"All 5 child bugs closed (attn-4wv, attn-deh, attn-fr8, attn-jru, attn-xds). Sharing UX sweep complete.","dependencies":[{"issue_id":"attn-lms","depends_on_id":"attn-4wv","type":"blocks","created_at":"2026-05-22T09:38:35Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-lms","depends_on_id":"attn-deh","type":"blocks","created_at":"2026-05-22T09:38:35Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-lms","depends_on_id":"attn-fr8","type":"blocks","created_at":"2026-05-22T09:38:34Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-lms","depends_on_id":"attn-jru","type":"blocks","created_at":"2026-05-22T09:38:33Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-lms","depends_on_id":"attn-xds","type":"blocks","created_at":"2026-05-22T09:38:34Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":5,"dependent_count":0,"comment_count":0} +{"id":"attn-jru","title":"Reviewer joins headless via --as-agent; should open a real window","description":"When sharing/joining for human review, the reviewer must NOT use 'attn review join \u003cinvite\u003e --as-agent \u003cname\u003e' which is a headless agent join (no window, no UI). The human-reviewer path should route through the windowed daemon join so a real UI opens. Fix scripts/dev-collab.sh (reviewer = windowed daemon, daemon-routed join) and ensure no human-facing flow emits --as-agent. --as-agent stays for bots/CI only.","status":"closed","priority":1,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-22T15:38:31Z","created_by":"James Lal","updated_at":"2026-05-22T16:07:14Z","closed_at":"2026-05-22T16:07:14Z","close_reason":"dev-collab.sh now routes the reviewer join to the windowed daemon (attn review join, no --as-agent); the daemon socket join was already wired to ReviewManager — removed the misleading 'stub, manager wiring pending' log. Verified: reviewer daemon joins + receives snapshots.","dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"attn-lms","title":"Sharing experience bug sweep","description":"Umbrella for the urgent sharing/collab UX bugs found 2026-05-22: headless --as-agent join, reviewer not auto-opening shared content, missing shared-file navigation, menu-bar z-index clipping, and ambiguous sharing status.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-22T15:38:30Z","created_by":"James Lal","updated_at":"2026-05-23T20:49:25Z","closed_at":"2026-05-23T20:49:25Z","close_reason":"All 5 child bugs closed (attn-4wv, attn-deh, attn-fr8, attn-jru, attn-xds). Sharing UX sweep complete.","dependencies":[{"issue_id":"attn-lms","depends_on_id":"attn-4wv","type":"blocks","created_at":"2026-05-22T09:38:35Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-lms","depends_on_id":"attn-deh","type":"blocks","created_at":"2026-05-22T09:38:35Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-lms","depends_on_id":"attn-fr8","type":"blocks","created_at":"2026-05-22T09:38:34Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-lms","depends_on_id":"attn-jru","type":"blocks","created_at":"2026-05-22T09:38:33Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-lms","depends_on_id":"attn-xds","type":"blocks","created_at":"2026-05-22T09:38:34Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":5,"dependent_count":0,"comment_count":0} {"id":"attn-032","title":"Fix npm trusted publishing for attnmd 0.4.0","description":"The v0.4.0 GitHub release completed successfully, including macOS/Linux assets and Homebrew tap update, but the manual Publish npm workflow failed when publishing attnmd@0.4.0. npm accepted provenance generation and then returned E404: Not Found / no permission for PUT https://registry.npmjs.org/attnmd. npm view shows attnmd exists, is owned by lightsofapollo, and latest remains 0.3.5. Local npm is not authenticated, so 0.4.0 could not be published manually from this machine.","acceptance_criteria":"attnmd@0.4.0 is published to npm with latest tag. The Publish npm workflow can be rerun successfully for v0.4.0, either by configuring npm trusted publishing for .github/workflows/npm-publish.yml or by wiring an authorized npm token. npm view attnmd version returns 0.4.0.","notes":"Release run succeeded: https://github.com/lightsofapollo/attn/actions/runs/26273347424. Failed npm workflow run: https://github.com/lightsofapollo/attn/actions/runs/26273799254.","status":"closed","priority":1,"issue_type":"bug","owner":"james@littlebearlabs.io","created_at":"2026-05-22T07:19:38Z","created_by":"James Lal","updated_at":"2026-05-22T14:11:25Z","closed_at":"2026-05-22T14:11:25Z","close_reason":"attnmd@0.4.0 published successfully after fixing npm trusted publisher workflow configuration","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-0wa","title":"Owner view flips to shared-document mode after reviewer live edit","description":"During the marketing capture workflow, the owner window is correct through reviewer comment/suggestion, but after the reviewer inserts live text the owner DOM reports data-slot=shared-doc-banner and the capture shows the reviewer Shared document banner. Repro via scripts/capture-collab-screenshots.sh before removing the live text insert. Investigate why the owner loses its local active-tab surface after remote collab steps.","notes":"ITEM 2: real bug that hurts editorial flow. App.svelte (~line 133) flips the OWNER into shared-document mode after a reviewer's live edit (isReviewerInRoom gating). Owner should stay on their local doc; only true reviewers render the shared snapshot.","status":"closed","priority":1,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-22T01:19:42Z","created_by":"James Lal","updated_at":"2026-05-23T05:14:03Z","started_at":"2026-05-23T05:04:23Z","closed_at":"2026-05-23T05:14:03Z","close_reason":"Fixed: review-mode gating now requires a positive reviewer role (daemon 'Joined'-\u003erole reviewer), not just currentShare===null. currentShare is session-only and lost on reconnect/rehydrate, so an owner returning to a remembered room (role 'owner', no share) flipped into shared-doc view. Extracted isReviewerView/collabRoleFor pure helpers in room-ui.ts; App.svelte uses them; 6 regression cases added (incl. the reconnect case). svelte-check clean, all 28 web test files pass.","dependencies":[{"issue_id":"attn-0wa","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:15Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.7.9","title":"H2: AAD-bind signal envelope target.deviceId (anti-relay-redirect)","description":"Security review (11.5) flagged: signal envelope's target.deviceId is not AAD-bound, allowing the relay to redirect to a different device. Mitigation: enforce envelope.target.deviceId == self.device_id in the inbound signal dispatcher OR include target.deviceId in AAD. See planning/collab/security-review.md §H2.","status":"closed","priority":1,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-19T16:18:29Z","created_by":"James Lal","updated_at":"2026-05-19T17:36:29Z","started_at":"2026-05-19T17:02:47Z","closed_at":"2026-05-19T17:36:29Z","close_reason":"Round 21 (final push): implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.7.9","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-19T10:18:28Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.5.17","title":"H1: require Attn-Owner-Signature on first POST /v2/rooms/:roomId","description":"Security review (11.5) flagged: room-create POST is un-admitted by design, allowing race attacks. Mitigation: require Attn-Owner-Signature header carrying ownerSigningKey self-sig over the canonical request body on the first POST. See planning/collab/security-review.md §H1.","status":"closed","priority":1,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-19T16:18:28Z","created_by":"James Lal","updated_at":"2026-05-19T17:36:15Z","started_at":"2026-05-19T17:02:47Z","closed_at":"2026-05-19T17:36:15Z","close_reason":"Round 21 (final push): implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.5.17","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-19T10:18:27Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.11.10","title":"Nested-file nav + breadcrumb regression in directory mode","description":"scripts/test-e2e.sh reports 2 FAILs against the current collab HEAD (2d76b45):\n\n1. Clicking 'nested/child.md' in the sidebar does not change the rendered content. The body remains on the previously-selected basic.md ('Project Status'). Expected to load child.md ('Nested Document').\n\n2. The breadcrumb element is absent. Neither '[class*=\"breadcrumb\"]' nor 'nav[aria-label]' is present in the DOM.\n\nVisual proof: /tmp/attn-e2e-screenshots/06-nested-file.png — sidebar shows child.md selected (highlighted), but the body still shows basic.md content, and no breadcrumb is rendered above the h1.\n\nRepro:\n scripts/test-e2e.sh\n # See suite 2 'Navigate Between Files' — last two assertions FAIL.\n\nNOT caused by attn-nnj.11.2 (doc-only change). Likely surfaced by recent Round-13/14 sidebar/tab/breadcrumb refactors. Probably impacts users navigating directories in real use. Discovered during epic-level e2e verification of attn-nnj.11.","notes":"Discovered during attn-nnj.11 epic-level e2e verification. Test command: scripts/test-e2e.sh. Other harnesses (test-dual-instance-smoke 10/10 PASS, test-review-e2e 12 PASS + 1 PEND) are clean — this is isolated to single-instance directory nav.","status":"closed","priority":1,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-19T04:35:47Z","created_by":"James Lal","updated_at":"2026-05-19T15:06:29Z","started_at":"2026-05-19T13:53:28Z","closed_at":"2026-05-19T15:06:29Z","close_reason":"Implemented; merged; 412 Rust + 213 relay tests pass (6 conformance scenarios deferred to 5.16)","dependencies":[{"issue_id":"attn-nnj.11.10","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T22:35:46Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.2.11","title":"IpcMessage + SocketMessage review variants (additive)","description":"Pure additive enum extension. In src/ipc.rs:9-59 add IpcMessage variants: review_share, review_join, review_create_comment, review_create_suggestion, review_accept_suggestion, review_resolve_anchor. In src/daemon.rs:62-76 add SocketMessage variants: ReviewShare, ReviewJoin, ReviewPull, ReviewStop, ReviewInbox. Handlers stub to a TODO that the Manager (0b-8) will fill. Lets the frontend stubs (0c-5) wire end-to-end without waiting for ReviewManager.","acceptance_criteria":"- IpcMessage + SocketMessage variants added with serde tagged-enum discrimination\\n- handle_message / handle_client dispatch new variants to TODO!/log handlers without crashing\\n- Frontend can post a review_share message via mock and Rust receives it cleanly\\n- Existing variants untouched","notes":"Audits show both enums are already serde-tagged — this is purely additive. Decouples 0c frontend work from 0b-8 ReviewManager scaffolding.","status":"closed","priority":1,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-19T04:29:36Z","created_by":"James Lal","updated_at":"2026-05-19T17:00:01Z","closed_at":"2026-05-19T17:00:01Z","close_reason":"Duplicate of 12.6/12.7/12.8/12.9/12.10/2.9 — already closed","dependencies":[{"issue_id":"attn-nnj.2.11","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T22:29:36Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.12.15","title":"review/store.ts scaffold (Svelte 5 runes)","description":"Scaffold web/src/lib/review/store.ts as the global review state holder using Svelte 5 runes. Minimal API: for panelOpen, currentRoomId, peers[], threads[] (empty), pendingOutbox (empty). Subscribers to window.__attn__.reviewEvent / reviewStatus push into this. Phase 2 issue 4.2 layers derived selectors on top (comments-on-current-snapshot, ambiguous-list, outbox-count).","acceptance_criteria":"- web/src/lib/review/store.ts exists with -based shape and typed via 0c-4 interfaces\\n- IPC callbacks from 0c-3 push events into the store\\n- No reactivity bugs: subscribing components see updates via \\n- Empty initial state renders cleanly","notes":"Use Svelte 5 runes per project convention (svelte5-best-practices skill exists). Phase 2 4.2 extends this with derived selectors.","status":"closed","priority":1,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-19T04:29:35Z","created_by":"James Lal","updated_at":"2026-05-19T16:59:45Z","closed_at":"2026-05-19T16:59:45Z","close_reason":"Duplicate of 12.6/12.7/12.8/12.9/12.10/2.9 — already closed","dependencies":[{"issue_id":"attn-nnj.12.15","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T22:29:35Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.12.14","title":"keyboard.ts hooks: comment/suggestion/panel-toggle","description":"Extend KeyboardConfig in web/src/lib/keyboard.ts with optional onCommentComposer, onSuggestionComposer, onToggleReviewPanel handlers. Default bindings: Cmd+. (comment), Cmd+Shift+. (suggestion), Cmd+J (toggle panel). Existing shortcuts unaffected. Update KeyboardShortcutsDialog.svelte to surface the new bindings only when a review room is active.","acceptance_criteria":"- 3 new handler keys on KeyboardConfig (all optional)\\n- Default keybinds registered when handlers provided\\n- KeyboardShortcutsDialog conditionally shows the review section\\n- No collision with existing shortcuts (j/k scroll, g/G top-bottom, t theme, q quit, e edit, f sidebar, Cmd+W/[/], etc.)","notes":"CLAUDE.md keyboard table is authoritative for existing shortcuts.","status":"closed","priority":1,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-19T04:29:34Z","created_by":"James Lal","updated_at":"2026-05-19T16:59:28Z","closed_at":"2026-05-19T16:59:28Z","close_reason":"Duplicate of 12.6/12.7/12.8/12.9/12.10/2.9 — already closed","dependencies":[{"issue_id":"attn-nnj.12.14","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T22:29:34Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.12.13","title":"popover-anchor utility (selection → DOM rect → constrained pop)","description":"Shared utility web/src/lib/review/popover-anchor.ts: given a ProseMirror EditorView + selection range, return a DOMRect and a constrained position for a popover (clamped within viewport, flipped above/below as needed). Used by comment composer (2-4), suggestion composer (2-5), and the ambiguous anchor picker (2-7) so all three pop in the same spot.","acceptance_criteria":"- Pure TS function: (view, from, to) → { rect, recommendedAnchor: { top, left, side: 'above'|'below' } }\\n- Handles multi-line selections (uses leading rect)\\n- Unit test against a simulated EditorView\\n- No types","notes":"Reference impl: web/src/lib/CommandPalette.svelte may have positioning logic to reuse.","status":"closed","priority":1,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-19T04:29:33Z","created_by":"James Lal","updated_at":"2026-05-19T16:59:14Z","closed_at":"2026-05-19T16:59:14Z","close_reason":"Duplicate of 12.6/12.7/12.8/12.9/12.10/2.9 — already closed","dependencies":[{"issue_id":"attn-nnj.12.13","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T22:29:33Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.12.12","title":"Theme CSS variables for review surfaces","description":"app.css today has surfaces/accent/sidebar/code-block vars but no decoration/panel vars. Add light + dark mode vars: --comment-highlight, --suggestion-bg, --suggestion-deletion, --confidence-high, --confidence-med, --confidence-low, --moved-badge-bg, --moved-badge-fg, --panel-surface, --panel-border, --peer-avatar-bg-{owner,reviewer,agent}, --stale-anchor-fg. Coordinate values via the existing OKLCH ramp.","acceptance_criteria":"- All new vars defined for both :root (light) and .dark themes\\n- Toggle via existing theme system works without flicker\\n- Variables surface in CSS, ready to be referenced by Phase 2 decoration plugin and components\\n- Use rampa-colors / theme-foundation skills if generating new ramps","notes":"Use OKLCH per project convention. Available skills: rampa-colors, theme-foundation.","status":"closed","priority":1,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-19T04:29:32Z","created_by":"James Lal","updated_at":"2026-05-19T16:59:02Z","closed_at":"2026-05-19T16:59:02Z","close_reason":"Duplicate of 12.6/12.7/12.8/12.9/12.10/2.9 — already closed","dependencies":[{"issue_id":"attn-nnj.12.12","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T22:29:32Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.12.11","title":"mock-ipc.ts: review callback shims (no scenario data)","description":"Extend mock-ipc.ts so the mocked window.ipc.postMessage understands review_* commands (logs them, echoes a fake ack via the review callbacks). No scripted scenario data yet — that's Phase 2 issue 4.1, which builds on this surface. Without this, frontend dev breaks the moment any review_* call is made.","acceptance_criteria":"- Mock dispatches review_* commands to no-op handlers\\n- Mock invokes window.__attn__.reviewStatus/reviewEvent/etc handlers when called via a test helper\\n- No console errors on baseline Starting Vite dev server on http://127.0.0.1:5173\nWaiting for Vite to be ready...\n\n \u001b[32m\u001b[1mVITE\u001b[22m v6.4.1\u001b[39m \u001b[2mready in \u001b[0m\u001b[1m1065\u001b[22m\u001b[2m\u001b[0m ms\u001b[22m\n\n \u001b[32m➜\u001b[39m \u001b[1mLocal\u001b[22m: \u001b[36mhttp://127.0.0.1:\u001b[1m5173\u001b[22m/\u001b[39m\nLaunching attn with HMR enabled (path: .)\n\u001b[2m4:50:22 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32m✨ new dependencies optimized: \u001b[33mshiki/themes, shiki/langs\u001b[32m\u001b[39m\n\u001b[2m4:50:22 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32m✨ optimized dependencies changed. reloading\u001b[39m\n\u001b[2m5:10:53 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/app.css\u001b[22m\n\u001b[2m5:11:10 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/app.css\u001b[22m\n\u001b[2m5:11:21 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/app.css\u001b[22m\n\u001b[2m5:12:47 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/app.css\u001b[22m\n\u001b[2m5:14:39 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/app.css\u001b[22m\n\u001b[2m5:15:01 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/app.css\u001b[22m\n\u001b[2m5:16:23 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mpage reload \u001b[39m\u001b[2msrc/lib/types.ts\u001b[22m\n\u001b[2m5:16:23 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/App.svelte, /src/app.css\u001b[22m\n\u001b[2m5:16:23 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/lib/KeyboardShortcutsDialog.svelte, /src/app.css\u001b[22m\n\u001b[2m5:16:23 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/App.svelte, /src/app.css\u001b[22m\n\u001b[2m5:16:24 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/app.css\u001b[22m\n\u001b[2m5:16:24 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/lib/Editor.svelte, /src/app.css\u001b[22m\n\u001b[2m5:27:05 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mpage reload \u001b[39m\u001b[2msrc/lib/mock-ipc.ts\u001b[22m\n\u001b[2m5:27:05 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/App.svelte, /src/app.css\u001b[22m\n\u001b[2m5:42:43 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/App.svelte, /src/app.css\u001b[22m\n\u001b[2m6:13:05 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/App.svelte, /src/app.css, /src/lib/Sidebar.svelte, /src/lib/Editor.svelte, /src/lib/PathBreadcrumb.svelte, /src/lib/components/ui/sonner/sonner.svelte, /src/lib/FileTree.svelte\u001b[22m\n\u001b[2m6:13:05 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mpage reload \u001b[39m\u001b[2msrc/lib/types.ts\u001b[22m\n\u001b[2m6:32:31 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mpage reload \u001b[39m\u001b[2msrc/lib/mock-ipc.ts\u001b[22m","notes":"Phase 2 issue 4.1 layers scripted scenarios on top of this.","status":"closed","priority":1,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-19T04:29:31Z","created_by":"James Lal","updated_at":"2026-05-19T16:58:49Z","closed_at":"2026-05-19T16:58:49Z","close_reason":"Duplicate of 12.6/12.7/12.8/12.9/12.10/2.9 — already closed","dependencies":[{"issue_id":"attn-nnj.12.11","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T22:29:31Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.11.8","title":"Dual-instance E2E harness (owner + reviewer via ATTN_HOME)","description":"Package the multi-instance automation pattern as a reusable test harness. Boots two attn daemons under ATTN_HOME=/tmp/attn-collab-owner and ATTN_HOME=/tmp/attn-collab-reviewer (plus optionally local Miniflare), exposes shell helpers attn_owner(...) and attn_reviewer(...) that prefix the right ATTN_HOME, and demonstrates a baseline assertion script that pokes both daemons via --query / --eval / --click. All review surface E2E tests (Phase 2 demo 4.14, Phase 5 e2e 8.6, Phase 4 WebRTC e2e 7.7) reuse this harness rather than wiring two-daemon plumbing themselves.","acceptance_criteria":"- scripts/lib/dual-instance.sh: sourced library exposing attn_owner / attn_reviewer / start_dual / stop_dual / wait_for_dual\\n- scripts/test-dual-instance-smoke.sh: a smoke test that boots two daemons, confirms each --info reports the right ATTN_HOME, drives --query on each independently, tears down cleanly\\n- Trap-based cleanup so Ctrl+C/early-exit kills both daemons\\n- README section in CLAUDE.md documents the pattern with a copy-pasteable example\\n- The Phase 2 demo (4.14) and Phase 5 e2e (8.6) test scripts source this library — no duplicated start/stop boilerplate","notes":"Depends on 2.10 (ATTN_HOME, done) and 11.4 (e2e scaffolding shape — in flight). The harness should NOT depend on Miniflare being up — leave that as a separate optional flag so this can run before the relay lands. Adopters: 4.14 + 7.7 + 8.6 + 11.7 dev-collab.sh.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T23:26:31Z","created_by":"James Lal","updated_at":"2026-05-18T23:57:28Z","started_at":"2026-05-18T23:49:35Z","closed_at":"2026-05-18T23:57:28Z","close_reason":"Implemented; merged into collab","dependencies":[{"issue_id":"attn-nnj.11.8","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T17:26:31Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.11.7","title":"scripts/dev-collab.sh: one-command local collab harness","description":"Boot the whole local collab stack with one command. Starts: Miniflare/wrangler relay on :8787 (background), owner daemon with ATTN_HOME=/tmp/attn-collab-owner pointing at a fixture markdown file, reviewer daemon with ATTN_HOME=/tmp/attn-collab-reviewer joining the room via attn://review/... copied from owner share. Tails logs from both. Ctrl+C kills everything cleanly. Sibling to the existing scripts/test-e2e.sh — same conventions.","acceptance_criteria":"- scripts/dev-collab.sh runs and produces a working owner + reviewer pair connected via local relay\\n- Default fixture file: tests/fixtures/basic.md (or a new collab-specific one)\\n- Environment can be overridden via env vars (ATTN_RELAY_URL, FIXTURE_PATH)\\n- Ctrl+C tears down all 3 processes; no orphans\\n- README section 'Local collab testing' explains the workflow","notes":"Depends on ATTN_HOME (0b-10), the relay scaffold (5-1), and the Rust mailbox transport bootstrap flow (6-6) being usable. Useful from Phase 3 onward; can stub-deploy earlier with just the relay running.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:58:47Z","created_by":"James Lal","updated_at":"2026-05-19T17:36:01Z","started_at":"2026-05-19T17:02:46Z","closed_at":"2026-05-19T17:36:01Z","close_reason":"Round 21 (final push): implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.11.7","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T16:58:46Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.11.7","depends_on_id":"attn-nnj.2.10","type":"blocks","created_at":"2026-05-18T16:58:47Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.11.7","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:58:48Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.11.7","depends_on_id":"attn-nnj.6.6","type":"blocks","created_at":"2026-05-18T16:58:49Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.2.10","title":"ATTN_HOME env override for multi-instance dev","description":"src/daemon.rs:109-121 runtime_dir() picks /tmp/attn-\u003cexe-hash\u003e in debug and ~/.attn in release with no env override. Add ATTN_HOME (or ATTN_RUNTIME_DIR) env var that, when set, overrides both code paths. This unblocks running two attn daemons on one machine for local collab testing: ATTN_HOME=/tmp/attn-owner attn ... and ATTN_HOME=/tmp/attn-reviewer attn .... Also threads through to the future review store path so ~/.attn/reviews/ becomes $ATTN_HOME/reviews/.","acceptance_criteria":"- ATTN_HOME env var, when set, replaces the runtime_dir() default in BOTH debug and release\\n- Socket path, fingerprint, log, and (future) reviews/ all live under $ATTN_HOME\\n- Two daemons started with different ATTN_HOME values don't clobber each other's sockets/state\\n- README or CLAUDE.md updated with the multi-instance dev recipe\\n- Existing single-instance behavior unchanged when ATTN_HOME is unset","notes":"Touches src/daemon.rs runtime_dir(). Also update src/projects.rs:73 to fall back to ATTN_HOME before XDG_STATE_HOME so project registry shares the namespace. Tiny change, unblocks Phase 3+ local testing.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:58:45Z","created_by":"James Lal","updated_at":"2026-05-18T23:18:42Z","started_at":"2026-05-18T23:08:50Z","closed_at":"2026-05-18T23:18:42Z","close_reason":"Implemented and merged into collab via parallel worktree agents; pnpm build + cargo check both clean post-merge","dependencies":[{"issue_id":"attn-nnj.2.10","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:58:45Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":3,"comment_count":0} -{"id":"attn-nnj.2.9","title":"IpcMessage + SocketMessage review variants (additive)","description":"Pure additive enum extension. In src/ipc.rs:9-59 add IpcMessage variants: review_share, review_join, review_create_comment, review_create_suggestion, review_accept_suggestion, review_resolve_anchor. In src/daemon.rs:62-76 add SocketMessage variants: ReviewShare, ReviewJoin, ReviewPull, ReviewStop, ReviewInbox. Handlers stub to a TODO that the Manager (0b-8) will fill. Lets the frontend stubs (0c-5) wire end-to-end without waiting for ReviewManager.","acceptance_criteria":"- IpcMessage + SocketMessage variants added with serde tagged-enum discrimination\\n- handle_message / handle_client dispatch new variants to TODO!/log handlers without crashing\\n- Frontend can post a review_share message via mock and Rust receives it cleanly\\n- Existing variants untouched","notes":"Audits show both enums are already serde-tagged - this is purely additive. Decouples 0c frontend work from 0b-8 ReviewManager scaffolding.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:53:21Z","created_by":"James Lal","updated_at":"2026-05-18T23:56:36Z","started_at":"2026-05-18T23:49:34Z","closed_at":"2026-05-18T23:56:36Z","close_reason":"Implemented; merged into collab; 27 tests pass","dependencies":[{"issue_id":"attn-nnj.2.9","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:53:21Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.9","depends_on_id":"attn-nnj.2.1","type":"blocks","created_at":"2026-05-18T16:54:00Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.9","depends_on_id":"attn-nnj.2.2","type":"blocks","created_at":"2026-05-18T16:54:00Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} -{"id":"attn-nnj.12.10","title":"review/store.ts scaffold (Svelte 5 runes)","description":"Scaffold web/src/lib/review/store.ts as the global review state holder using Svelte 5 runes. Minimal API: state for panelOpen, currentRoomId, peers[], threads[] (empty), pendingOutbox (empty). Subscribers to window.__attn__.reviewEvent / reviewStatus push into this. Phase 2 issue 4.2 layers derived selectors on top (comments-on-current-snapshot, ambiguous-list, outbox-count).","acceptance_criteria":"- web/src/lib/review/store.ts exists with rune-based state and typed via 0c-4 interfaces\\n- IPC callbacks from 0c-3 push events into the store\\n- No reactivity bugs: subscribing components see updates via derived\\n- Empty initial state renders cleanly","notes":"Use Svelte 5 runes per project convention (svelte5-best-practices skill exists). Phase 2 4.2 extends this with derived selectors.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:53:20Z","created_by":"James Lal","updated_at":"2026-05-18T23:45:22Z","started_at":"2026-05-18T23:31:43Z","closed_at":"2026-05-18T23:45:22Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.12.10","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:53:20Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.12.10","depends_on_id":"attn-nnj.12.3","type":"blocks","created_at":"2026-05-18T16:53:46Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.12.10","depends_on_id":"attn-nnj.12.4","type":"blocks","created_at":"2026-05-18T16:53:44Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.12.9","title":"keyboard.ts hooks: comment/suggestion/panel-toggle","description":"Extend KeyboardConfig in web/src/lib/keyboard.ts with optional onCommentComposer, onSuggestionComposer, onToggleReviewPanel handlers. Default bindings: Cmd+. (comment), Cmd+Shift+. (suggestion), Cmd+J (toggle panel). Existing shortcuts unaffected. Update KeyboardShortcutsDialog.svelte to surface the new bindings only when a review room is active.","acceptance_criteria":"- 3 new handler keys on KeyboardConfig (all optional)\\n- Default keybinds registered when handlers provided\\n- KeyboardShortcutsDialog conditionally shows the review section\\n- No collision with existing shortcuts (j/k scroll, g/G top-bottom, t theme, q quit, e edit, f sidebar, Cmd+W/[/], etc.)","notes":"CLAUDE.md keyboard table is authoritative for existing shortcuts.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:53:20Z","created_by":"James Lal","updated_at":"2026-05-18T23:18:28Z","started_at":"2026-05-18T23:08:49Z","closed_at":"2026-05-18T23:18:28Z","close_reason":"Implemented and merged into collab via parallel worktree agents; pnpm build + cargo check both clean post-merge","dependencies":[{"issue_id":"attn-nnj.12.9","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:53:19Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0} -{"id":"attn-nnj.12.8","title":"popover-anchor utility (selection -\u003e DOM rect -\u003e constrained pop)","description":"Shared utility web/src/lib/review/popover-anchor.ts: given a ProseMirror EditorView + selection range, return a DOMRect and a constrained position for a popover (clamped within viewport, flipped above/below as needed). Used by comment composer (2-4), suggestion composer (2-5), and the ambiguous anchor picker (2-7) so all three pop in the same spot.","acceptance_criteria":"- Pure TS function: (view, from, to) returns { rect, recommendedAnchor: { top, left, side: above|below } }\\n- Handles multi-line selections (uses leading rect)\\n- Unit test against a simulated EditorView\\n- No any types","notes":"Reference impl: web/src/lib/CommandPalette.svelte may have positioning logic to reuse.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:53:19Z","created_by":"James Lal","updated_at":"2026-05-18T23:18:15Z","started_at":"2026-05-18T23:08:49Z","closed_at":"2026-05-18T23:18:15Z","close_reason":"Implemented and merged into collab via parallel worktree agents; pnpm build + cargo check both clean post-merge","dependencies":[{"issue_id":"attn-nnj.12.8","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:53:18Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":3,"comment_count":0} -{"id":"attn-nnj.12.7","title":"Theme CSS variables for review surfaces","description":"app.css today has surfaces/accent/sidebar/code-block vars but no decoration/panel vars. Add light + dark mode vars: --comment-highlight, --suggestion-bg, --suggestion-deletion, --confidence-high, --confidence-med, --confidence-low, --moved-badge-bg, --moved-badge-fg, --panel-surface, --panel-border, --peer-avatar-bg-{owner,reviewer,agent}, --stale-anchor-fg. Coordinate values via the existing OKLCH ramp.","acceptance_criteria":"- All new vars defined for both :root (light) and .dark themes\\n- Toggle via existing theme system works without flicker\\n- Variables surface in CSS, ready to be referenced by Phase 2 decoration plugin and components\\n- Use rampa-colors / theme-foundation skills if generating new ramps","notes":"Use OKLCH per project convention. Available skills: rampa-colors, theme-foundation.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:53:18Z","created_by":"James Lal","updated_at":"2026-05-18T23:18:02Z","started_at":"2026-05-18T23:08:48Z","closed_at":"2026-05-18T23:18:02Z","close_reason":"Implemented and merged into collab via parallel worktree agents; pnpm build + cargo check both clean post-merge","dependencies":[{"issue_id":"attn-nnj.12.7","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:53:17Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":3,"comment_count":0} -{"id":"attn-nnj.12.6","title":"mock-ipc.ts: review callback shims (no scenario data)","description":"Extend mock-ipc.ts so the mocked window.ipc.postMessage understands review_* commands (logs them, echoes a fake ack via the review callbacks). No scripted scenario data yet — that's Phase 2 issue 4.1, which builds on this surface. Without this, frontend dev breaks the moment any review_* call is made.","acceptance_criteria":"- Mock dispatches review_* commands to no-op handlers; no console errors\\n- Mock invokes window.__attn__.reviewStatus/reviewEvent/reviewSnapshot/reviewAnchorResolution handlers when called via a test helper\\n- No regression in existing mocked surface","notes":"Phase 2 issue 4.1 layers scripted scenarios on top of this.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:53:17Z","created_by":"James Lal","updated_at":"2026-05-19T00:40:12Z","started_at":"2026-05-19T00:26:06Z","closed_at":"2026-05-19T00:40:12Z","close_reason":"Implemented in parallel; merged into collab; 166 tests pass + relay 35 tests pass","dependencies":[{"issue_id":"attn-nnj.12.6","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:53:17Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.12.6","depends_on_id":"attn-nnj.12.3","type":"blocks","created_at":"2026-05-18T16:53:45Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.12.6","depends_on_id":"attn-nnj.12.4","type":"blocks","created_at":"2026-05-18T16:53:44Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.12.6","depends_on_id":"attn-nnj.12.5","type":"blocks","created_at":"2026-05-18T16:53:45Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.12.4","title":"types.ts: ReviewEvent / Anchor / ResolvedAnchor interfaces","description":"Add review domain TypeScript interfaces matching the Rust serde shapes (camelCase): ReviewEvent (with EventMeta/Body discriminated union), ReviewStatus, ReviewSnapshot, ReviewAnchorResolutionUpdate, Anchor (with PositionAnchor/QuoteAnchor/BlockAnchor/ContextAnchor/StructureAnchor sub-types), ResolvedAnchor (exact|remapped|ambiguous|stale variants), SuggestionOperation. Source of truth is data-model.md.","acceptance_criteria":"- No types (user instruction)\\n- All variants from data-model.md represented\\n- Includes JSDoc citing data-model.md section per type\\n- Roundtrips through JSON.parse(JSON.stringify(x)) without loss","notes":"data-model.md §Anchors, §Anchor Resolution, §Review Events.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:50:13Z","created_by":"James Lal","updated_at":"2026-05-18T23:17:47Z","started_at":"2026-05-18T23:08:48Z","closed_at":"2026-05-18T23:17:47Z","close_reason":"Implemented and merged into collab via parallel worktree agents; pnpm build + cargo check both clean post-merge","dependencies":[{"issue_id":"attn-nnj.12.4","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:50:12Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":4,"comment_count":0} -{"id":"attn-nnj.12.5","title":"ipc.ts: review_* outbound command stubs","description":"Extend web/src/lib/ipc.ts with typed outbound commands: review_share, review_join, review_create_comment, review_create_suggestion, review_accept_suggestion, review_resolve_anchor. Each is a thin wrapper around window.ipc.postMessage with the typed payload. No backend yet; calls land at the Rust IpcMessage handler stub (0b-9).","acceptance_criteria":"- 6 typed exported functions in web/src/lib/ipc.ts\\n- Payloads typed via types.ts (depends on 0c-4)\\n- Real ipc.ts uses postMessage; mock-ipc.ts (depends on 0c-6) routes to local handler\\n- Functions are async and return a typed Result/Promise","notes":"Spec: data-model.md §Webview IPC Changes.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:50:13Z","created_by":"James Lal","updated_at":"2026-05-19T00:23:43Z","started_at":"2026-05-19T00:04:12Z","closed_at":"2026-05-19T00:23:43Z","close_reason":"Implemented in parallel worktrees; merged into collab; 142 tests pass","dependencies":[{"issue_id":"attn-nnj.12.5","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:50:13Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.12.5","depends_on_id":"attn-nnj.12.4","type":"blocks","created_at":"2026-05-18T16:53:43Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.12.5","depends_on_id":"attn-nnj.2.9","type":"blocks","created_at":"2026-05-18T16:54:02Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":5,"comment_count":0} -{"id":"attn-nnj.12.2","title":"Editor.svelte: $props-injectable plugins + nodeViews","description":"Editor.svelte:137-186 buildPlugins() is monolithic; nodeViews dict at 475-484 is closed. Extend to accept optional plugins?: Plugin[] and nodeViews?: Record\u003cstring, NodeViewConstructor\u003e via Svelte 5 , appended after built-ins. Enables Phase 2 decorations plugin and any future collab plugins without re-hardcoding.","acceptance_criteria":"- extended with plugins?, nodeViews?\\n- Built-in plugins still loaded first; injected plugins appended\\n- Existing callers unaffected (props are optional)\\n- Stub test: passing an empty decoration plugin must not regress existing nodeViews (math, mermaid, tables, code-highlight)","notes":"Model after the existing prosemirror/code-highlight.ts factory shape.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:50:11Z","created_by":"James Lal","updated_at":"2026-05-18T23:17:33Z","started_at":"2026-05-18T23:08:48Z","closed_at":"2026-05-18T23:17:33Z","close_reason":"Implemented and merged into collab via parallel worktree agents; pnpm build + cargo check both clean post-merge","dependencies":[{"issue_id":"attn-nnj.12.2","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:50:10Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.12.3","title":"window.__attn__ bridge: review callback registration","description":"App.svelte:1003-1019 registers setContent/updateContent/font scale only. Extend with no-op stubs for reviewStatus(payload), reviewEvent(payload), reviewSnapshot(snapshot), reviewAnchorResolution(update). Real handlers wire to the review store later (Phase 0c-10 / Phase 2).","acceptance_criteria":"- window.__attn__ exposes the 4 new methods, each typed via types.ts (depends on 0c-4)\\n- Default impl: console.debug only, so Rust can already evaluate_script without errors\\n- Type definitions in web/src/vite-env.d.ts (or wherever Window augmentation lives) updated","notes":"Spec: data-model.md §Webview IPC Changes.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:50:11Z","created_by":"James Lal","updated_at":"2026-05-18T23:29:47Z","started_at":"2026-05-18T23:21:22Z","closed_at":"2026-05-18T23:29:47Z","close_reason":"Implemented via parallel worktree agents; merged into collab","dependencies":[{"issue_id":"attn-nnj.12.3","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:50:11Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.12.3","depends_on_id":"attn-nnj.12.4","type":"blocks","created_at":"2026-05-18T16:53:42Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} -{"id":"attn-nnj.12.1","title":"3-column layout: right-rail slot in App.svelte","description":"App.svelte:1352-1373 SidebarInset is 2-column today (sidebar + editor). Extend to 3-column with a named right-rail slot for the future ReviewPanel. Slot collapses when no review room is active (no chrome shift).","acceptance_criteria":"- App.svelte exposes a right-rail snippet/slot that mounts a placeholder div when no review session\\n- Layout uses CSS flex/grid, not absolute positioning\\n- Sidebar toggle (existing) still works; right-rail toggles independently via Cmd+J (placeholder shortcut)\\n- No visual regression with attn ./planning/ baseline","notes":"Touches App.svelte mainContent snippet. Don't render ReviewPanel yet — just the slot.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:50:09Z","created_by":"James Lal","updated_at":"2026-05-18T23:17:18Z","started_at":"2026-05-18T23:08:47Z","closed_at":"2026-05-18T23:17:18Z","close_reason":"Implemented and merged into collab via parallel worktree agents; pnpm build + cargo check both clean post-merge","dependencies":[{"issue_id":"attn-nnj.12.1","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:50:09Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":6,"comment_count":0} -{"id":"attn-nnj.12","title":"Phase 0c: UI/IPC plumbing","description":"Frontend + IPC infrastructure that has to land before Phase 2 features can drop in cleanly. Pure plumbing — no review UI rendered yet. Audit grounded: App.svelte:1352-1373 has only 2-column SidebarInset; Editor.svelte:137-186 hardcodes 8 plugins; App.svelte:1003-1019 window.__attn__ bridge registers no review callbacks; mock-ipc.ts has no review surface; ipc.ts has no review_* commands; types.ts has no ReviewEvent shapes; theme has no decoration vars.","notes":"Sequencing: blocks Phase 2 features. Per user direction, all crypto stays in Rust — frontend only handles plaintext ReviewEvent objects.","status":"closed","priority":1,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:50:08Z","created_by":"James Lal","updated_at":"2026-05-19T17:02:10Z","closed_at":"2026-05-19T17:02:10Z","close_reason":"All children closed","dependencies":[{"issue_id":"attn-nnj.12","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:50:07Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.11.5","title":"Security review pass","description":"After Phase 3a / 3b / 4 land and before any public release: full security review of crypto envelope handling. Specifically check: signature verification ordering (decrypt under AEAD FIRST, then verify the plaintext signature — never the reverse, which would let an attacker substitute a verified ciphertext); AAD binding completeness on every encrypt/decrypt call site (no naked AEAD calls); PoW token replay window (the 5-minute window from decision #6 actually enforced); ownerSigningKey TOFU correctness (first signature wins, subsequent different keys rejected); browser fragment-stripping race window (no observer can see the fragment between page load and replaceState). Use the security-review skill if available.","acceptance_criteria":"planning/collab/security-review.md created with findings + severity per checked area.\nEach of the five focus areas (decrypt-then-verify order, AAD binding, PoW replay window, owner TOFU, browser fragment race) has a section with: pass/fail, code references, evidence.\nAny HIGH or CRITICAL findings have follow-up bd issues created and linked.\nReview covers Rust client AND TS browser client AND relay worker — all three speak crypto.\nSign-off recorded in the doc with date and reviewer.","notes":"Specs: planning/collab/crypto-spec.md (entire), planning/collab/relay-spec.md §Anti-Abuse + §Admission, planning/collab/amendments.md §Decision #6 + §Decision #13 + §Owner identity. Files: planning/collab/security-review.md (new), src/review/crypto.rs, relay/src/*, web/src/lib/review/*. Skills: security-review skill is available — invoke it explicitly. Schedule AFTER Phase 4 lands (so all crypto codepaths exist) but BEFORE public release.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:35:24Z","created_by":"James Lal","updated_at":"2026-05-19T16:25:30Z","started_at":"2026-05-19T16:07:55Z","closed_at":"2026-05-19T16:25:30Z","close_reason":"Round 19: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.11.5","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T16:35:24Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.11.5","depends_on_id":"attn-nnj.5.15","type":"blocks","created_at":"2026-05-18T16:38:34Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.11.5","depends_on_id":"attn-nnj.6.7","type":"blocks","created_at":"2026-05-18T16:38:35Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.11.5","depends_on_id":"attn-nnj.7.7","type":"blocks","created_at":"2026-05-18T16:38:35Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.11.5","depends_on_id":"attn-nnj.8.6","type":"blocks","created_at":"2026-05-18T16:38:36Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.11.4","title":"E2E test scaffolding for review surfaces","description":"Extend scripts/test-e2e.sh with a review/ test suite that boots the daemon under task dev with a mock-IPC scenario file pre-loaded, then uses --eval / --query / --wait-for to assert review-panel state (comment count, anchor status, suggestion list, etc.). Lays groundwork for Phase 2's 'comment survives owner edits' demo AND Phase 5's apply integration test. Reuses the daemon's existing automation flags so we don't need a separate Playwright runner.","acceptance_criteria":"scripts/test-e2e.sh has a new review/ section that loads a mock-IPC fixture into web/src/lib/mock-ipc.ts via env var or query param.\nHelpers: wait_for_review_state, assert_comment_count, assert_anchor_status etc. (bash functions) that wrap --query and --wait-for.\nA scaffold test boots the daemon with a fixture containing 2 mock comments and asserts both render in the panel.\nTest passes locally on macOS without requiring a relay or webrtc-rs (mock-IPC drives the frontend in isolation).\nScreenshot captured to /tmp/attn-e2e-screenshots/review-*.png for visual review.\nPattern documented so Phase 2 + Phase 5 authors can extend without reinventing.","notes":"Specs: planning/collab/amendments.md §existing automation flags affect ReviewManager design, §Mock IPC must be extended. Files: scripts/test-e2e.sh, tests/fixtures/review/ (new), web/src/lib/mock-ipc.ts (extend). Mock-IPC extension is described in amendments.md §Mock IPC — coordinate so this scaffolding lands alongside or after that extension. window.__attn__.reviewState() should be a stable shape that this test depends on.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:35:09Z","created_by":"James Lal","updated_at":"2026-05-18T23:41:35Z","started_at":"2026-05-18T23:21:24Z","closed_at":"2026-05-18T23:41:35Z","close_reason":"Re-close after DB restore: implemented; merged into collab; 8 PASS / 5 PEND / 0 FAIL","dependencies":[{"issue_id":"attn-nnj.11.4","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T16:35:09Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0} -{"id":"attn-nnj.6.7","title":"Conformance integration tests against Miniflare","description":"Run the relay conformance corpus from Phase 3a issue 14 against the Rust client. CI script: boot 'wrangler dev --local' (Miniflare) from the relay/ package, then run cargo test --features mailbox-integration on the Rust crate. Covers happy path and every error code the relay returns so the Rust client stays in lock-step with the relay's wire contract.","acceptance_criteria":"- scripts/test-mailbox-integration.sh (or task target): starts wrangler dev --local in background, waits for /health, runs cargo test --features mailbox-integration -- --test-threads=1, tears down\n- Rust tests under src/review/transport/tests/ (or tests/ integration crate) load relay/test/conformance/cases.json via serde and execute each case via the real MailboxTransport\n- Coverage matches Phase 3a issue 14 corpus: room lifecycle, WS backfill (full / mid / 4005), all caps + batch=32, owner-only ops, PoW failures, hibernation roundtrip, rate limits, longSession clamping\n- CI integration: GitHub Actions job runs this script and fails on any case mismatch\n- Tests fail fast on any new relay-side error code missing from the Rust mapping (helps catch wire drift early)","notes":"Spec: planning/collab/relay-spec.md §Test Plan (687-705). Consumes conformance corpus from 3a-14 — make sure the JSON schema is serde-friendly. Blocked by 3b-6 since bootstrap is the precondition for every other case.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:58Z","created_by":"James Lal","updated_at":"2026-05-19T13:50:28Z","started_at":"2026-05-19T04:31:04Z","closed_at":"2026-05-19T13:50:28Z","close_reason":"Implemented (partially for 5.14 — scaffold + skip-on-empty, follow-up to fill cases.json); 372 Rust + 184 relay tests pass","dependencies":[{"issue_id":"attn-nnj.6.7","depends_on_id":"attn-nnj.2.10","type":"blocks","created_at":"2026-05-18T16:58:49Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.7","depends_on_id":"attn-nnj.5.14","type":"blocks","created_at":"2026-05-18T16:35:59Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.7","depends_on_id":"attn-nnj.6","type":"parent-child","created_at":"2026-05-18T16:34:57Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.7","depends_on_id":"attn-nnj.6.1","type":"blocks","created_at":"2026-05-18T16:36:09Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.7","depends_on_id":"attn-nnj.6.6","type":"blocks","created_at":"2026-05-18T16:36:12Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":4,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.6.6","title":"Device + room bootstrap flow (Share + Join commands)","description":"Wire ReviewManager into ReviewCommand::Share and ReviewCommand::Join. Share: derive room keys (event/snapshot/signal/admission per crypto-spec §Key Derivation), POST /v2/rooms/:roomId with ownerSigningKey + clamped policy, POST /devices with kind=owner and self-signature, populate the local /devices cache, emit ReviewUpdate::RoomCreated. Join: parse the invite URL (crypto-spec §Invite URLs), derive the same per-kind keys, POST /devices with kind=reviewer or agent, GET /devices to populate the verification cache, emit ReviewUpdate::ParticipantJoined.","acceptance_criteria":"- Share flow: generate room secret → derive event/snapshot/signal/admission keys + ownerSigningKey → POST /v2/rooms/:roomId (mint PoW from pool, send admissionKey, ownerSigningKey, clamped policy) → POST /devices (kind=owner, selfSignature over canonical device bytes) → store room state under ~/.attn/reviews/rooms/\u003croomId\u003e/ → emit ReviewUpdate::RoomCreated{invite_url}\n- Join flow: parse invite URL → derive keys identically → POST /devices (kind=reviewer|agent per CLI flag) → GET /devices → cache the device roster (publicSigningKey by deviceId) → emit ReviewUpdate::ParticipantJoined{room, peers}\n- Owner-key handling: ownerSigningKey is generated client-side at Share, stored locally as private key, public half sent in the create body; never written outside ~/.attn (file perms 600)\n- Both flows install the WS client (issue 3b-3) and outbox processor (3b-2) for the room after bootstrap completes\n- Bootstrap errors map: room create 409 ATTN_ROOM_EXISTS_DIFFERENT_POLICY → ReviewUpdate::ShareConflict; device 409 ATTN_DEVICE_KEY_CHANGED → ReviewUpdate::JoinKeyConflict\n- Tests: Share against Miniflare, Join against same Miniflare instance, key derivation determinism, owner-key mismatch on rejoin attempt, peer roster cached and refreshed","notes":"Spec: planning/collab/crypto-spec.md §Key Derivation (39-77), §Invite URLs (59-77), §Signing-Key Publication (344-403). planning/collab/relay-spec.md §POST /v2/rooms/:roomId (114-167), §POST /v2/rooms/:roomId/devices (169-217). Consumes issue 3b-3 (WS client) and 3b-2 (outbox) once bootstrap completes.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:57Z","created_by":"James Lal","updated_at":"2026-05-19T04:07:48Z","started_at":"2026-05-19T03:41:59Z","closed_at":"2026-05-19T04:07:48Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.6.6","depends_on_id":"attn-nnj.11.1","type":"blocks","created_at":"2026-05-18T16:38:27Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.6","depends_on_id":"attn-nnj.5.5","type":"blocks","created_at":"2026-05-18T16:38:27Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.6","depends_on_id":"attn-nnj.5.6","type":"blocks","created_at":"2026-05-18T16:38:26Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.6","depends_on_id":"attn-nnj.6","type":"parent-child","created_at":"2026-05-18T16:34:56Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.6","depends_on_id":"attn-nnj.6.1","type":"blocks","created_at":"2026-05-18T16:36:09Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.6","depends_on_id":"attn-nnj.6.3","type":"blocks","created_at":"2026-05-18T16:36:11Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":5,"dependent_count":2,"comment_count":0} -{"id":"attn-nnj.6.5","title":"Cursor management + 4005 cursor-too-old recovery","description":"Persist last_seen_seq per room in ~/.attn/reviews/rooms/\u003croomId\u003e/cursors.json. Update after every successfully imported envelope (issue 3b-4 wires the actual write). On WS error{ATTN_CURSOR_TOO_OLD, resyncFromSeq}: discard the current cursor, attempt a P2P snapshot request from a peer device (defer this branch to Phase 4 if WebRTC transport isn't yet wired; in the meantime fall back to re-subscribe from resyncFromSeq, accepting the pre-resync history loss explicitly via ReviewUpdate::HistoryGap).","acceptance_criteria":"- ~/.attn/reviews/rooms/\u003croomId\u003e/cursors.json holds {last_seen_seq, oldest_retained_seq, updated_at}; atomic write via temp+rename\n- Read on startup; passed to issue 3b-3 WS client for the initial subscribe.after\n- On TransportError::CursorTooOld{resync_from_seq}: log warning, emit ReviewUpdate::HistoryGap{lost_from, lost_to}, set last_seen_seq=resync_from_seq, write cursors.json, reconnect with subscribe{after: resync_from_seq}\n- Stub for P2P snapshot recovery: a clear TODO('phase-4 webrtc') with the signature of a future async fn request_snapshot_from_peer(peer_device_id) so Phase 4 can plug in\n- Tests: cursor persistence across restart, 4005 fallback to resync (no P2P), atomic write under crash simulation, HistoryGap emitted exactly once per 4005","notes":"Spec: planning/collab/relay-spec.md §WebSocket Protocol (372-422, error frame) and §Close Codes (4005). amendments.md decision #5 (no GET /envelopes backfill means 4005 is the only way history is exposed). Snapshot-from-peer plumbing depends on Phase 4 WebRTC and is intentionally stubbed here.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:56Z","created_by":"James Lal","updated_at":"2026-05-19T04:30:01Z","started_at":"2026-05-19T04:09:04Z","closed_at":"2026-05-19T04:30:01Z","close_reason":"Implemented; merged","dependencies":[{"issue_id":"attn-nnj.6.5","depends_on_id":"attn-nnj.6","type":"parent-child","created_at":"2026-05-18T16:34:55Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.5","depends_on_id":"attn-nnj.6.1","type":"blocks","created_at":"2026-05-18T16:36:08Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.5","depends_on_id":"attn-nnj.6.3","type":"blocks","created_at":"2026-05-18T16:36:11Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.6.4","title":"Inbound envelope pipeline (decrypt + verify + import)","description":"Per ServerFrame::Envelope from the WS stream: look up the per-kind key (eventKey | snapshotKey | signalKey) from room state, AES-256-GCM decrypt, verify Ed25519 signature against the deviceId's publicSigningKey from the cached /devices roster, dedupe by EventId (against ~/.attn/reviews/rooms/\u003croomId\u003e/events.jsonl ledger), append to events.jsonl, update the persistent sync cursor. On success emit ReviewUpdate::EventImported into the ReviewManager event loop.","acceptance_criteria":"- For each ServerFrame::Envelope: select key by envelope.kind; decrypt (AES-256-GCM, nonce per crypto-spec §Nonce Discipline) — failure → ReviewUpdate::DecryptFailed and continue (do not crash, do not advance cursor)\n- Ed25519 verify plaintext signature against device.publicSigningKey from cached /devices snapshot; signature failure → drop + ReviewUpdate::SignatureInvalid (do not advance cursor for that envelope)\n- Dedupe by EventId: if already present in events.jsonl skip the append (but still advance the seq cursor)\n- Append the canonical event bytes to events.jsonl (append-only, fsync per batch)\n- Update last_seen_seq AFTER successful append (so a crash mid-write replays cleanly)\n- Emit ReviewUpdate::EventImported{event} to the consumer (Phase 4 UI / ReviewManager)\n- Cached /devices roster auto-refreshed when an envelope arrives signed by an unknown deviceId (issue GET /devices and retry verify once)\n- Tests: happy path, wrong key, wrong sig, duplicate EventId, unknown device triggers refresh + verify","notes":"Spec: planning/collab/crypto-spec.md §Envelope Encryption (79-115), §Nonce Discipline (108-115), §Signatures (199-258). planning/collab/data-model.md §Transport Model. /devices cache is populated by issue 3b-6 bootstrap and refreshed lazily.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:55Z","created_by":"James Lal","updated_at":"2026-05-19T04:06:29Z","started_at":"2026-05-19T03:23:02Z","closed_at":"2026-05-19T04:06:29Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.6.4","depends_on_id":"attn-nnj.6","type":"parent-child","created_at":"2026-05-18T16:34:55Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.4","depends_on_id":"attn-nnj.6.1","type":"blocks","created_at":"2026-05-18T16:36:08Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.11.3","title":"Binary-size verification CI gate","description":"Add scripts/check-binary-size.sh that runs after cargo build --release and asserts the .app bundle (or stripped binary if not bundled) is under 25 MiB. Wire into CI so Phase 4 (webrtc-rs) cannot regress past the budget without an explicit waiver. This is the safety net that prevents future PRs from silently breaking the decision #1 tradeoff.","acceptance_criteria":"scripts/check-binary-size.sh exists; runs du on the appropriate artifact (.app bundle preferred, falls back to stripped binary).\nExits non-zero with a clear message if size \u003e 25 MiB; prints the size + the budget either way.\nCI workflow (.github/workflows/*.yml) invokes it on every PR that touches Cargo.toml or src/**.\nFailure can be waived only by setting ATTN_SIZE_BUDGET_WAIVER=1 in CI env (or equivalent) with a comment in the PR.\nDocumented in CLAUDE.md or RELEASE_SETUP.md so future contributors know the rule.","notes":"Specs: planning/collab/amendments.md §Decision #1, §Phase 4. Files: scripts/check-binary-size.sh (new), .github/workflows/*.yml. Use the same release/bundle output the existing scripts/build.sh produces. The 25 MiB number comes directly from Decision #1's tradeoff statement. Blocks Phase 4 work in the sense that Phase 4 issue 1 should consume this gate.","status":"closed","priority":1,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:54Z","created_by":"James Lal","updated_at":"2026-05-18T23:45:09Z","closed_at":"2026-05-18T23:45:09Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.11.3","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T16:34:54Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.11.1","title":"attn://review/... custom-scheme handler","description":"In src/main.rs custom protocol handler, route attn://review/\u003croomId\u003e#key=... to a new SocketMessage::ReviewJoin{invite} BEFORE falling through to the existing attn://localhost/... file-serving path. Reject attn://localhost/review/... explicitly (it's reserved per amendments.md). Without this, clicking an invite URL on macOS won't work and Phase 4/6 invite handling is blocked.","acceptance_criteria":"src/main.rs custom protocol handler matches attn://review/\u003c...\u003e before the existing file-serving route.\nMatched invites become a SocketMessage::ReviewJoin{invite: String} dispatched to the daemon.\nattn://localhost/review/... returns a 400-equivalent (refused with a clear log line) — reserved path collision.\nNon-review attn://localhost/* paths continue to serve files as today (existing behavior unchanged).\nUnit test or integration test (via --eval) confirms a synthetic attn://review/abc#key=xyz hits the ReviewJoin handler.\nSocketMessage::ReviewJoin variant defined and wired through daemon.rs.","notes":"Specs: planning/collab/amendments.md §Custom attn:// scheme handler. Files: src/main.rs (~1207 lines — locate the existing custom_protocol registration), src/daemon.rs (new SocketMessage variant), src/ipc.rs (potentially). Critical detail: the route match must come BEFORE the fallthrough, not after. Blocks Phase 4 (issue 7 e2e test needs to click an invite) and Phase 6 (browser invite parse logic on the native side too).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:30Z","created_by":"James Lal","updated_at":"2026-05-18T23:33:30Z","started_at":"2026-05-18T23:21:23Z","closed_at":"2026-05-18T23:33:30Z","close_reason":"Implemented; merged into collab; cargo check + cargo test clean","dependencies":[{"issue_id":"attn-nnj.11.1","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T16:34:30Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.9.7","title":"CLI subcommands for local agents","description":"Full set of attn review subcommands so a local agent (e.g., a coding assistant running on the owner's machine) can drive the daemon: share \u003cpath\u003e [--mode live|async|hybrid] [--ttl 7d]; join \u003cinvite\u003e; inbox [--json]; submit-comment \u003cfile\u003e; submit-suggestion \u003cfile\u003e; pull; stop. Each maps to a SocketMessage variant per data-model.md §Daemon Socket Commands. Local CLI uses owner's daemon identity by default (per amendments.md §Agent CLI key handling) — distinct from remote agents in Phase 6 issue 6.","acceptance_criteria":"Each subcommand parses via clap and dispatches a typed SocketMessage to the running daemon.\nshare creates a new room with the given policy fields, prints the invite URL (attn://review/...) for the owner to share.\njoin takes either an attn://review/... URL or a raw invite string; daemon joins the room and starts receiving envelopes.\ninbox lists pending review actions on the owner's open rooms; --json emits structured output for agent consumption.\nsubmit-comment / submit-suggestion take a JSON file (or stdin) describing the anchor + content; daemon adds to outbox with the owner's identity by default.\npull manually triggers a relay catchup. stop ends a room (owner only — requires owner signature).\nCLI help (attn review --help) lists all subcommands with clear examples.","notes":"Specs: planning/collab/amendments.md §Agent CLI key handling, planning/collab/data-model.md §Daemon Socket Commands. Files: src/cli/review.rs (new), src/daemon.rs (new SocketMessage variants). Owner-identity default is the load-bearing detail vs Phase 6 issue 6 (remote agent has its own key). --as-agent \u003cname\u003e override comes from Phase 6 issue 6's implementation but the flag itself can be wired here.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:15Z","created_by":"James Lal","updated_at":"2026-05-19T15:06:14Z","started_at":"2026-05-19T04:31:06Z","closed_at":"2026-05-19T15:06:14Z","close_reason":"Implemented; merged; 412 Rust + 213 relay tests pass (6 conformance scenarios deferred to 5.16)","dependencies":[{"issue_id":"attn-nnj.9.7","depends_on_id":"attn-nnj.2.8","type":"blocks","created_at":"2026-05-18T16:39:10Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.9.7","depends_on_id":"attn-nnj.9","type":"parent-child","created_at":"2026-05-18T16:34:14Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.6.3","title":"WebSocket client + reconnect with backoff","description":"Tokio-tungstenite (or async-tungstenite) WebSocket client. Subprotocol header: 'attn.v2, \u003cbase64url admission HMAC\u003e'. Handles server ping by responding with pong inside 60s. On disconnect: exponential backoff reconnect (200ms → 30s cap with jitter), replay subscribe{after: last_seen_seq} on each reconnect. Maps close codes 4000/4001/4002/4005 to typed transport errors so the ReviewManager can surface them as ReviewUpdate variants.","acceptance_criteria":"- Connects with subprotocol 'attn.v2' and admission HMAC piggybacked (matches issue 3a-11 handshake)\n- After connect, sends subscribe{after: last_seen_seq} and starts receiving ServerFrame items as a Stream\n- Ping handler: replies with pong within 60s; if no server ping in 90s, force reconnect (defensive)\n- Disconnect handler: exponential backoff (200ms, 400ms, 800ms, ... cap 30s) with ±25% jitter\n- Close codes mapped: 4000→TransportError::AdmissionInvalid (non-retryable), 4001→RoomDeleted (terminal), 4002→RoomExpired (terminal), 4005→CursorTooOld{resync_from_seq} (handled by issue 3b-5), 1001→Timeout (retryable)\n- Replays subscribe{after: last_seen_seq} on every reconnect so the stream resumes from the persistent cursor\n- Tests: happy-path frame roundtrip (against Miniflare from 3a-11), ping/pong, reconnect after server-initiated close, 4005 surfacing, 4001/4002 terminal","notes":"Spec: planning/collab/relay-spec.md §WebSocket Protocol (362-459) and §Close Codes (423-431). Implements the client side of issue 3a-11. Cursor persistence lives in 3b-5.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:51Z","created_by":"James Lal","updated_at":"2026-05-19T04:07:35Z","started_at":"2026-05-19T03:41:59Z","closed_at":"2026-05-19T04:07:35Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.6.3","depends_on_id":"attn-nnj.5.11","type":"blocks","created_at":"2026-05-18T16:38:25Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.3","depends_on_id":"attn-nnj.6","type":"parent-child","created_at":"2026-05-18T16:33:50Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.3","depends_on_id":"attn-nnj.6.1","type":"blocks","created_at":"2026-05-18T16:36:07Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} -{"id":"attn-nnj.6.2","title":"Outbox processor (~/.attn/reviews/rooms/\u003croomId\u003e/outbox.jsonl)","description":"Persistent outbox that survives crashes. Each line in outbox.jsonl is a queued MailboxEnvelope plus state metadata (attempts, last_error, status: pending|in_flight|sent). Processor reads pending entries, mints a PoW token from the Phase 0a token pool, batches up to 32, POSTs to /v2/rooms/:roomId/envelopes, and on success marks them sent (compaction sweeps sent lines out periodically). Backoff on 429/507/ATTN_POW_*. EnvelopeId is deterministic (see crypto-spec) so retries are server-side idempotent.","acceptance_criteria":"- ~/.attn/reviews/rooms/\u003croomId\u003e/outbox.jsonl created on first send; append-only with periodic compaction of sent entries\n- Each line: serde JSON {envelope, attempts, last_error?, status, queued_at}\n- Processor task per room: read pending → mint PoW (use Phase 0a token pool) → batch up to 32 → POST → on 2xx mark sent\n- Backoff: 429 honors Retry-After header; 507 ATTN_ROOM_EVENT_CAP/STORAGE_FULL → exponential backoff with cap (e.g., 1s, 2s, 4s, 8s, 30s); ATTN_POW_* → mint fresh token immediately (token reuse failure)\n- Crash safety: status:in_flight entries are reset to pending on startup so they re-send; deterministic envelopeId guarantees server dedupe\n- Emits ReviewUpdate::EnvelopeSent on success and ReviewUpdate::SendFailed{retryable} on terminal failure\n- Tests: happy path, batch=32 boundary, retry on 429, retry on 507, PoW-replay recovery, crash mid-send dedupe","notes":"Spec: planning/collab/relay-spec.md §POST /v2/rooms/:roomId/envelopes (218-269). amendments.md decision #7 (batch cap 32, single PoW per request). crypto-spec.md §EnvelopeId (283-301) for deterministic ID. Phase 0a token pool is a prerequisite reference (assume exists or stub if not yet implemented).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:50Z","created_by":"James Lal","updated_at":"2026-05-19T02:29:35Z","started_at":"2026-05-19T01:57:30Z","closed_at":"2026-05-19T02:29:35Z","close_reason":"Implemented (3.8+round-10 retries); merged into collab; 267 Rust + 144 relay tests pass","dependencies":[{"issue_id":"attn-nnj.6.2","depends_on_id":"attn-nnj.1.7","type":"blocks","created_at":"2026-05-18T16:38:24Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.2","depends_on_id":"attn-nnj.5.7","type":"blocks","created_at":"2026-05-18T16:38:25Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.2","depends_on_id":"attn-nnj.6","type":"parent-child","created_at":"2026-05-18T16:33:49Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.2","depends_on_id":"attn-nnj.6.1","type":"blocks","created_at":"2026-05-18T16:36:06Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.6.1","title":"src/review/transport.rs scaffold + Transport trait","description":"Define the transport abstraction that all relay variants implement. src/review/ does not yet exist — create the module tree. Trait: Transport { async connect(roomId, deviceId) -\u003e Result\u003c()\u003e; async send_envelopes(Vec\u003cMailboxEnvelope\u003e) -\u003e Result\u003cSendReceipt\u003e; subscribe(after_seq: u64) -\u003e impl Stream\u003cItem=Result\u003cServerFrame\u003e\u003e }. Two implementations are planned: MailboxTransport (this phase, issues 3b-2..3b-6) and WebRTCTransport (Phase 4). The frontend never sees raw transport — only typed ReviewUpdate variants emitted by ReviewManager after decrypt+verify+import.","acceptance_criteria":"- src/review/mod.rs added to src/lib.rs (or main module tree)\n- src/review/transport.rs defines Transport trait with the signatures above\n- Concrete types: MailboxEnvelope (canonical bytes + metadata), SendReceipt {envelope_id, server_seq}, ServerFrame enum (Hello, Envelope, Presence, PolicyChanged, Ping, Error) matching the WS protocol from relay-spec.md\n- TransportError enum maps relay error codes (ATTN_ADMISSION_INVALID, ATTN_POW_*, ATTN_CURSOR_TOO_OLD, ATTN_ROOM_EXPIRED, ATTN_ROOM_DELETED, ATTN_RATE_LIMITED, ...) to typed Rust errors with retryable/non-retryable classification\n- A NoopTransport test impl for use in unit tests of ReviewManager\n- cargo check + cargo clippy clean (no any-equivalent — use proper types per repo conventions)\n- No frontend exposure: the trait lives behind ReviewManager, which is what emits ReviewUpdate","notes":"Spec: planning/collab/relay-spec.md §WebSocket Protocol (362-459) defines the frame shapes. planning/collab/data-model.md §Transport Model. Code conventions: TypeScript repo for the relay, but this Rust crate avoids 'any'/dyn-without-bounds equivalents. No backwards-compat shim with any prior transport — this is the new module from scratch.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:49Z","created_by":"James Lal","updated_at":"2026-05-19T01:56:27Z","started_at":"2026-05-19T01:36:54Z","closed_at":"2026-05-19T01:56:27Z","close_reason":"Implemented; merged into collab; 254 Rust tests + 130 relay tests pass; corpus replay green","dependencies":[{"issue_id":"attn-nnj.6.1","depends_on_id":"attn-nnj.1.9","type":"blocks","created_at":"2026-05-18T16:38:24Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.6.1","depends_on_id":"attn-nnj.6","type":"parent-child","created_at":"2026-05-18T16:33:48Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":7,"comment_count":0} -{"id":"attn-nnj.9.5","title":"CORS + browser allowlist on the relay","description":"Confirm relay-spec.md §Browser Considerations is implemented in the worker: Origin allowlist on WS upgrade requests (reject non-allowlisted with 403), Access-Control-Allow-Origin headers on HTTP responses (POST /envelopes etc.) pulled from ALLOWED_BROWSER_ORIGINS env var. Test from a non-allowlisted origin and confirm 403. Without this, the browser client cannot function due to same-origin policy.","acceptance_criteria":"WS upgrade handler reads ALLOWED_BROWSER_ORIGINS env var and rejects upgrades with non-allowlisted Origin via HTTP 403.\nHTTP endpoints (POST /envelopes, POST /devices, POST /acks, POST /blobs) emit Access-Control-Allow-Origin matching the request Origin if in allowlist.\nOPTIONS preflight is handled with the right Access-Control-Allow-Methods + Access-Control-Allow-Headers (including Attn-PoW, Attn-Owner-Signature).\nIntegration test in the conformance corpus: request from https://evil.example → 403; request from https://attn.dev → 200/204.\nALLOWED_BROWSER_ORIGINS documented in relay deployment notes.","notes":"Specs: planning/collab/relay-spec.md §Browser Considerations. Files: relay/src/cors.ts (or wherever the worker entry middleware lives). Origin must be a strict match, not a prefix or wildcard. Note: the Rust client doesn't send an Origin header, so a missing Origin should be permitted (Rust path) but a present-but-not-allowlisted Origin should be denied.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:45Z","created_by":"James Lal","updated_at":"2026-05-19T16:56:25Z","started_at":"2026-05-19T16:26:12Z","closed_at":"2026-05-19T16:56:25Z","close_reason":"Round 20: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.9.5","depends_on_id":"attn-nnj.5.7","type":"blocks","created_at":"2026-05-18T16:39:11Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.9.5","depends_on_id":"attn-nnj.9","type":"parent-child","created_at":"2026-05-18T16:33:44Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.9.4","title":"Browser review UI (subset of native UI)","description":"Reuse Phase 2 Svelte review components where possible — the snapshot viewer, comment threads, suggestion decorations. Reviewer-only surface: NO share button (browser cannot own a room), NO apply UI (browser cannot mutate the owner's working copy). Browser CAN add comments and suggestions; those get added to an in-memory outbox that uploads via POST /envelopes from the browser context (with admission HMAC + hashcash PoW).","acceptance_criteria":"Browser /review/:roomId page renders the latest snapshot and existing comment/suggestion threads.\nReviewer can add a comment anchored to a selection; comment uploads via POST /envelopes (hashcash mined off-thread in a Web Worker per amendments.md).\nReviewer can add a suggestion (text replacement) anchored to a range; same upload path.\nNO share button, NO apply button, NO 'create room' affordance in the browser UI.\nShared Svelte components from Phase 2 render identically to native (within visual-diff tolerance).\nBrowser-specific empty/error states for: invalid invite, expired room (close 4001), stale cursor (close 4005), failed admission (403).","notes":"Specs: planning/collab/amendments.md §Phase 6, planning/collab/relay-spec.md §Browser Considerations. Files: web/src/routes/review/[roomId]/ (new) + reuse web/src/lib/review/* components from Phase 2. Depends on: invite parsing (Phase 6 issue 2), WS client (Phase 6 issue 3), CORS configured (Phase 6 issue 5). PoW miner from Phase 0a should already exist as a Web Worker — reuse it.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:31Z","created_by":"James Lal","updated_at":"2026-05-19T17:57:56Z","started_at":"2026-05-19T17:37:11Z","closed_at":"2026-05-19T17:57:56Z","close_reason":"Round 22 (final): implemented; merged","dependencies":[{"issue_id":"attn-nnj.9.4","depends_on_id":"attn-nnj.9","type":"parent-child","created_at":"2026-05-18T16:33:31Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.9.4","depends_on_id":"attn-nnj.9.2","type":"blocks","created_at":"2026-05-18T16:36:14Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.9.4","depends_on_id":"attn-nnj.9.3","type":"blocks","created_at":"2026-05-18T16:36:15Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.9.4","depends_on_id":"attn-nnj.9.5","type":"blocks","created_at":"2026-05-18T16:36:16Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.9.3","title":"Browser WebSocket client + envelope import","description":"TS counterpart of the Phase 3b WebSocket client: connect to the relay's wss://.../v2/rooms/:roomId/ws, attach admission HMAC piggyback per relay-spec, receive hello + envelope frames, decrypt under eventKey/snapshotKey, signature-verify, import into an in-memory replica of the event log and snapshot graph. Uses whichever crypto path won the Phase 6 issue 1 decision (WASM or TS). Browser is reviewer-only — no owner-signing-key flows here.","acceptance_criteria":"WebSocket client connects with admission HMAC (matches the Rust client's behavior bit-for-bit on the test corpus).\nBackfill via hello + envelope frames per relay-spec.md §WebSocket (no GET /envelopes per decision #5).\nStale cursor → close code 4005 handled with a re-bootstrap path (re-fetch snapshot, replay from there).\nDecrypt → verify ORDER is correct (decrypt under eventKey FIRST, then signature-verify the plaintext per crypto-spec.md).\nIn-memory store survives WS disconnect+reconnect without duplicate events (EventId dedupe).\nEnd-to-end test: a comment added on the Rust client appears in the browser within 1s.","notes":"Specs: planning/collab/relay-spec.md §WebSocket + §Signaling, planning/collab/crypto-spec.md §Envelope Format, planning/collab/amendments.md §Decision #5 (WebSocket-only). Files: web/src/lib/review/transport.ts (new). Depends on browser crypto path from Phase 6 issue 1 and on invite/key derivation from Phase 6 issue 2. NO PERSISTENCE — everything in-memory (decision #13).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:17Z","created_by":"James Lal","updated_at":"2026-05-19T17:35:47Z","started_at":"2026-05-19T17:02:46Z","closed_at":"2026-05-19T17:35:47Z","close_reason":"Round 21 (final push): implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.9.3","depends_on_id":"attn-nnj.5.11","type":"blocks","created_at":"2026-05-18T16:38:32Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.9.3","depends_on_id":"attn-nnj.5.6","type":"blocks","created_at":"2026-05-18T16:38:33Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.9.3","depends_on_id":"attn-nnj.5.7","type":"blocks","created_at":"2026-05-18T16:38:33Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.9.3","depends_on_id":"attn-nnj.9","type":"parent-child","created_at":"2026-05-18T16:33:16Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.9.3","depends_on_id":"attn-nnj.9.1","type":"blocks","created_at":"2026-05-18T16:36:13Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.9.3","depends_on_id":"attn-nnj.9.2","type":"blocks","created_at":"2026-05-18T16:36:14Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":5,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.5.15","title":"Test plan acceptance suite (Miniflare integration tests)","description":"Implement the 14 numbered test scenarios from relay-spec.md §Test Plan as vitest integration tests under relay/test/integration/. Each test boots Miniflare (programmatic API), exercises the endpoints, and asserts on response codes, error codes, DO storage state (via wrapped get/list), and R2 contents. Consumes the conformance corpus from issue 3a-14 where applicable but adds in-depth assertions Miniflare can introspect.","acceptance_criteria":"- relay/test/integration/*.test.ts mirrors the 14 scenarios from §Test Plan\n- Each test boots a fresh Miniflare instance per case (or per file) so state is isolated\n- Asserts on: HTTP status, error code in body, DO storage keys via Miniflare's getDurableObjectStorage, R2 keys via getR2Bucket\n- Uses fake timers / Miniflare's setCurrentTime to test TTL alarms deterministically\n- Runs in CI via npm test (relay package) — green required before merge\n- Covers: room create+idempotency, device register+conflict, envelope ingest+caps, batch cap=32, WS backfill happy path, WS backfill 4005 path, hibernation roundtrip, owner-ack+delete, anonymous-ack no-delete, DELETE room, idle expiry, hard-max expiry, longSession 7d, rate-limit per-IP+per-device+anti-enum","notes":"Spec: planning/collab/relay-spec.md §Test Plan (687-705). Uses conformance corpus from 3a-14 as the source of request/response pairs but is the canonical pass/fail gate for the relay.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:08Z","created_by":"James Lal","updated_at":"2026-05-19T16:55:51Z","started_at":"2026-05-19T16:26:11Z","closed_at":"2026-05-19T16:55:51Z","close_reason":"Round 20: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.5.15","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:33:07Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.15","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:31Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.15","depends_on_id":"attn-nnj.5.14","type":"blocks","created_at":"2026-05-18T16:35:59Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.5.14","title":"Conformance corpus (relay/test/conformance/cases.json)","description":"Build a shared conformance corpus consumed by BOTH the Miniflare integration suite (Phase 3a issue 15) AND the Rust transport tests (Phase 3b issue 7). Each case is a full {request, expectedResponse, expectedSideEffects} record covering the entire HTTP+WS surface and every error code in the spec.","acceptance_criteria":"- relay/test/conformance/cases.json (or one file per category) with deterministic fixtures (fixed keys, fixed timestamps in mock clock)\n- Coverage: room lifecycle (create idempotent, create policy-conflict, delete), WS backfill (after=0 full replay, after=last no replay, after=deleted→4005 with resyncFromSeq), all caps (32 batch, maxEvents, maxRoomBytes, maxEventBytes, maxSnapshotBytes, signal sub-cap eviction), owner-only ops (ack+delete with/without sig, DELETE room), PoW failures (insufficient bits, expired, resource mismatch, replayed), hibernation roundtrip (write → eject DO → reconnect → backfill), rate limits (per-IP, per-device, anti-enum), longSession clamping\n- A loader/runner abstraction in TypeScript that interprets cases and executes them against any HTTP+WS target (Miniflare or live wrangler dev)\n- Same JSON loadable from Rust via serde (matching schema documented at the top of the file)\n- README explaining how to add a new case","notes":"Spec source of truth: planning/collab/relay-spec.md §Test Plan (687-705) lists 14 scenarios — these are the minimum coverage. crypto-spec.md §Test Vectors (421-433) for PoW vectors. Phase 3b issue 7 will run this same corpus from Rust against wrangler dev --local. Plan the file format to be serde-deserializable from the start.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:07Z","created_by":"James Lal","updated_at":"2026-05-19T15:05:43Z","started_at":"2026-05-19T04:31:02Z","closed_at":"2026-05-19T15:05:43Z","close_reason":"Implemented; merged; 412 Rust + 213 relay tests pass (6 conformance scenarios deferred to 5.16)","dependencies":[{"issue_id":"attn-nnj.5.14","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:33:07Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.14","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:30Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} -{"id":"attn-nnj.5.13","title":"Rate limiting (per-IP, per-device, anti-enumeration)","description":"Two-tier rate limiting. Worker edge (before DO): per-IP 600/min sliding window using Cloudflare's rate-limiting binding or a KV-backed sliding window, and an anti-enumeration counter that returns 429 after 30 unknown rooms in 5 minutes from one IP. Per-device 120/min in DO storage (sliding window over rate:\u003cdeviceId\u003e:\u003cbucket\u003e) checked after admission. All 429 responses include retryAfterMs in body and Retry-After header.","acceptance_criteria":"- Edge per-IP: 600 requests/min sliding window across all rooms; over → 429 ATTN_RATE_LIMITED with retryAfterMs\n- Edge anti-enum: 30 unknown rooms (404 ATTN_ROOM_NOT_FOUND triggers) from one IP in 5 min → 429 ATTN_ANTI_ENUMERATION; the unknown-room counter is keyed by IP, not roomId, so the attacker cannot tell which roomId tripped it\n- DO per-device: 120 requests/min sliding window over rate:\u003cdeviceId\u003e:\u003cminute-bucket\u003e entries; over → 429 ATTN_RATE_LIMITED\n- 429 responses set Retry-After header (seconds, rounded up) AND body {error:{code, retryAfterMs}}\n- Edge limits checked before the request crosses to the DO (cost protection)\n- Per-device limits checked after admission so they're attributable\n- Tests: per-IP cap, per-device cap, anti-enum trip with mixed unknown rooms, Retry-After format","notes":"Spec: planning/collab/relay-spec.md §Anti-Abuse (565-571). Numbers come from amendments and user's brief (600/min per IP, 120/min per device, 30 unknown/5min). Implement edge limits in src/index.ts before the DO fetch, device limits inside the DO.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:06Z","created_by":"James Lal","updated_at":"2026-05-19T15:27:48Z","started_at":"2026-05-19T15:07:28Z","closed_at":"2026-05-19T15:27:48Z","close_reason":"Round 17: implemented; merged; 409 Rust + 237 relay tests pass","dependencies":[{"issue_id":"attn-nnj.5.13","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:33:06Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.13","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:30Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.13","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:49Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.13","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:49Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.9.2","title":"Browser invite URL parsing + memory-only secret handling","description":"At https://attn.dev/review/:roomId#key=... — on load, parse the fragment, IMMEDIATELY strip it from the visible URL via history.replaceState(null, '', location.pathname + location.search), and hold roomSecret only in JS heap memory. Derive rootKey then derive the subkeys (eventKey, snapshotKey, signalingKey, admissionKey). Zero/overwrite the original fragment string where possible. Reload requires re-paste — no sessionStorage, no IndexedDB, no cookies (decision #13). This is the entire trust-on-browser story.","acceptance_criteria":"On page load, location.hash is parsed exactly once and immediately stripped via history.replaceState.\nroomSecret never written to sessionStorage, localStorage, IndexedDB, cookies, or service-worker caches — verify via grep + manual audit.\nrootKey + subkeys derived synchronously after fragment parse; derivation matches crypto-spec.md.\nAfter fragment strip, location.href shows the bare URL with no #key= visible (e.g., to the page title, devtools history list, or any other observer).\nReload reproduces the 'paste invite to join' UX rather than silently rejoining.\nUnit test (jsdom or playwright) asserts replaceState fires before any other code accesses location.hash a second time.","notes":"Specs: planning/collab/amendments.md §Decision #13, planning/collab/crypto-spec.md §Invite URLs + §Key Derivation. Files: web/src/lib/review/invite.ts (new), web/src/routes/review/[roomId]/+page.svelte (or similar). The strip-fragment-before-anything-else ordering matters — see Phase 6 issue in cross-cutting security review for the race-window concern. Zeroize the fragment string in JS is best-effort (string immutability in JS limits us); the goal is no PERSISTENT trace.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:01Z","created_by":"James Lal","updated_at":"2026-05-19T16:06:19Z","started_at":"2026-05-19T15:31:08Z","closed_at":"2026-05-19T16:06:19Z","close_reason":"Round 18: implemented; merged; 414+ Rust tests pass","dependencies":[{"issue_id":"attn-nnj.9.2","depends_on_id":"attn-nnj.9","type":"parent-child","created_at":"2026-05-18T16:33:00Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0} -{"id":"attn-nnj.9.1","title":"Browser crypto sourcing decision (WASM vs TS)","description":"Real architectural fork: compile attn-collab-crypto Rust crate to WASM (one source of truth, larger bundle, identical behavior with Rust client) vs hand-write TS crypto against the shared test-vector corpus (smaller bundle, two implementations with drift risk). Bundle-size budget is the hard constraint — target the hosted JS bundle under 500 KiB gzipped. Output a decision doc with the recommendation and the measurements that back it. This issue blocks all browser WS/crypto work in Phase 6, so resolve it early.","acceptance_criteria":"planning/collab/ui/browser-crypto-decision.md created with both options spelled out and bundle-size measurements.\nWASM path: build attn-collab-crypto with wasm-pack (or wasm-bindgen), measure brotli/gzip size, note startup cost.\nTS path: scoped libraries identified (e.g., @noble/ciphers for XChaCha20-Poly1305, @noble/ed25519, @noble/hashes for HKDF-SHA256), estimate gzipped bundle size for the subset used.\nRecommendation includes a concrete number for the resulting bundle in both cases and a winner.\nFlagged with bd human — owner sign-off needed before downstream Phase 6 issues unblock.","notes":"Specs: planning/collab/crypto-spec.md §Primitives (XChaCha20-Poly1305 + Ed25519 + HKDF-SHA256 + canonical JSON RFC 8785 + base64url-no-pad), planning/collab/amendments.md (Decision #4 cipher locked, Decision #13 browser memory-only). Test-vector corpus must validate whichever path is chosen — the corpus is shared, not Rust-specific. If TS path: @noble/* libs are audited and tree-shake well.","status":"closed","priority":1,"issue_type":"decision","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:32:30Z","created_by":"James Lal","updated_at":"2026-05-19T16:25:13Z","started_at":"2026-05-19T16:07:54Z","closed_at":"2026-05-19T16:25:13Z","close_reason":"Round 19: implemented; merged; build clean","labels":["human"],"dependencies":[{"issue_id":"attn-nnj.9.1","depends_on_id":"attn-nnj.9","type":"parent-child","created_at":"2026-05-18T16:32:30Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.5.12","title":"Alarms: idle + hard-max TTL + pow-prune","description":"DO alarms drive room TTL cleanup. Cloudflare DO supports only one pending alarm at a time, so always schedule at min(hard_max_at, last_event_at + idleTimeoutMs, next_pow_prune_at). On fire, determine which deadline tripped and act: idle/hard-max → close all WS with 4002 (room expired), wipe DO storage, schedule R2 cleanup; pow-prune → delete pow_seen entries past expiresAt+10min. Every WS connect calls cleanup_check() if now is within 1h of expires_at (decision #9). Hard-max defaults: 24h, or 7d when policy.longSession=true (decision #8). Idle default: 1h.","acceptance_criteria":"- alarm() handler reads all candidate deadlines (hard_max_at, last_event_at+idleTimeoutMs, pow_prune_at) and reschedules at the next earliest after acting on any that have fired\n- Idle/hard-max fire: broadcast close 4002 with reason 'room_expired', deleteAll() DO storage, enqueue R2 prefix delete, set tombstone so further requests 410 ATTN_ROOM_EXPIRED for 24h\n- pow-prune fire: scan meta:pow_seen:* and delete entries where extracted expiresAt+10min \u003c now\n- POST /envelopes updates last_event_at and reschedules the alarm\n- Every WS upgrade calls cleanup_check() — if now within 1h of expires_at, run the same scan and reschedule\n- hard_max_at computed at room creation: created + (longSession ? 7d : 24h), clamped by policy.expiresAt\n- Tests: idle expiry path, hard-max expiry path, longSession 7-day cap, single-alarm scheduling correctness when multiple deadlines compete, cleanup_check on WS connect, pow_seen prune","notes":"Spec: planning/collab/relay-spec.md §Alarms (514-529), §Close Codes (423-431). amendments.md decisions #8 (TTL defaults + longSession) and #9 (R2 lifecycle as safety net, DO alarm primary, cleanup_check on WS connect). Coordinates with PoW replay protection from 3a-3 and WS close from 3a-11.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:32:29Z","created_by":"James Lal","updated_at":"2026-05-19T13:49:59Z","started_at":"2026-05-19T04:31:02Z","closed_at":"2026-05-19T13:49:59Z","close_reason":"Implemented (partially for 5.14 — scaffold + skip-on-empty, follow-up to fill cases.json); 372 Rust + 184 relay tests pass","dependencies":[{"issue_id":"attn-nnj.5.12","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:32:28Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.12","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:29Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.12","depends_on_id":"attn-nnj.5.11","type":"blocks","created_at":"2026-05-18T16:35:58Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.12","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:47Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.12","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:48Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.5.11","title":"WebSocket protocol + DO hibernation","description":"Implement the WebSocket transport with subprotocol 'attn.v2' and admission HMAC piggybacked as the second protocol value. Use state.acceptWebSocket() for hibernation so idle sessions don't burn DO CPU; tag attached sockets with [deviceId, participantId]. Server frames: hello, envelope, presence, policy_changed, ping, error. Client frames: subscribe, pong. On subscribe.after \u003c meta:oldest_retained_seq emit error{code:ATTN_CURSOR_TOO_OLD, resyncFromSeq} and close with code 4005. 30s ping interval, 60s pong timeout → close 1001.","acceptance_criteria":"- Upgrade handshake: subprotocol header must include 'attn.v2' and a second value carrying the admission HMAC; missing/invalid → 401 ATTN_ADMISSION_INVALID\n- Uses state.acceptWebSocket(ws, [deviceId, participantId]) for hibernation; webSocketMessage/webSocketClose handlers route by tag\n- Backfill on subscribe: replay envelope frames from storage where seq \u003e subscribe.after (decision #5 — no GET /envelopes endpoint)\n- On subscribe.after \u003c meta:oldest_retained_seq → send error{code:ATTN_CURSOR_TOO_OLD, resyncFromSeq:meta:oldest_retained_seq} then close 4005\n- Server frames implemented: hello{serverSeq, oldestRetainedSeq, peers}, envelope{seq, envelope}, presence{deviceId, state}, policy_changed{policy}, ping, error\n- Client frames handled: subscribe{after}, pong\n- 30s server ping interval (per-session scheduled via alarm or setTimeout-substitute); no pong within 60s → close 1001 ATTN_TIMEOUT\n- Broadcast helper used by POST /envelopes routes through getSession-by-tag lookup\n- Tests: handshake reject, backfill from 0, backfill from mid, 4005 on too-old cursor, hibernation roundtrip (eject + re-deliver after restart), ping/pong, presence broadcast","notes":"Spec: planning/collab/relay-spec.md §WebSocket Protocol (362-459), §Hibernation Tags (531-533). amendments.md decision #5 (WS-only delivery, GET /envelopes REMOVED). Uses admission middleware from 3a-2.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:32:28Z","created_by":"James Lal","updated_at":"2026-05-19T04:06:15Z","started_at":"2026-05-19T03:23:01Z","closed_at":"2026-05-19T04:06:15Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.5.11","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:32:28Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.11","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:29Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.11","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:46Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.11","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:47Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":3,"comment_count":0} -{"id":"attn-nnj.5.10","title":"DELETE /v2/rooms/:roomId — owner room wipe","description":"Owner-only endpoint to nuke the room. Requires both Attn-PoW and Attn-Owner-Signature. Disconnects all WebSocket sessions with close code 4001 (room deleted), wipes all DO storage for the room, and schedules R2 cleanup by listing and deleting all objects under rooms/\u003croomId\u003e/.","acceptance_criteria":"- Requires valid Attn-Admission, Attn-PoW, AND Attn-Owner-Signature; missing/invalid owner sig → 403 ATTN_OWNER_SIG_REQUIRED / ATTN_OWNER_KEY_MISMATCH\n- Closes every active WebSocket with close code 4001, reason 'room_deleted'\n- Wipes all DO storage keys (envelope:*, acks:*, devices:*, meta:*, pow_seen:*) via storage.deleteAll() or scoped delete\n- Schedules R2 cleanup: list objects with prefix rooms/\u003croomId\u003e/ and delete in batches (paginate)\n- Returns 200 {deleted:true} synchronously even if R2 cleanup is still draining\n- Idempotent: re-DELETE on already-gone room returns 404 ATTN_ROOM_NOT_FOUND\n- Test: WS sessions observe 4001 close before storage wipe; subsequent admission attempts return 404","notes":"Spec: planning/collab/relay-spec.md §DELETE /v2/rooms/:roomId (305-311), §Close Codes (423-431). amendments.md decision #3 (owner-only). Owner-sig from issue 3a-4.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:32:27Z","created_by":"James Lal","updated_at":"2026-05-19T04:29:44Z","started_at":"2026-05-19T04:09:04Z","closed_at":"2026-05-19T04:29:44Z","close_reason":"Implemented; merged","dependencies":[{"issue_id":"attn-nnj.5.10","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:32:26Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.10","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:28Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.10","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:45Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.10","depends_on_id":"attn-nnj.5.3","type":"blocks","created_at":"2026-05-18T16:35:58Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.10","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:46Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.7.7","title":"WebRTC e2e integration test","description":"Boot two local attn daemons in a test harness, share a doc from one, join from the other, exchange comments over the DataChannel. Confirms decision #1's Rust-owned WebRTC path actually works in wry/tao on macOS. Use the daemon's existing automation flags (--eval, --query, --wait-for) to drive both sides without needing a separate test runner. This is the integration test that proves Phase 4 has actually shipped.","acceptance_criteria":"scripts/test-e2e.sh gains a review/webrtc.sh (or equivalent) that boots two daemon instances on isolated state dirs.\nOwner shares a markdown fixture; reviewer joins via the invite URL (attn://review/...).\nA comment from the reviewer arrives on the owner side within 2s, asserted via --query on the owner's review panel.\nDataChannel connection state is asserted Connected on both sides via --eval against window.__attn__.reviewState().\nTest passes in CI on macOS (and Linux if relay tests already run there).\nCaptures connection logs to /tmp/attn-e2e-screenshots/ on failure.","notes":"Specs: planning/collab/amendments.md §existing automation flags affect ReviewManager design + §Phase 4. Files: scripts/test-e2e.sh + new tests/fixtures/review/*.md. Two daemons on the same machine need different ATTN_HOME (or equivalent) and different socket paths — see existing single-instance protocol in src/daemon.rs. Window.__attn__.reviewState() should be exposed earlier in Phase 2/3 — verify it exists before writing this test.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:32:15Z","created_by":"James Lal","updated_at":"2026-05-19T16:07:08Z","started_at":"2026-05-19T15:31:09Z","closed_at":"2026-05-19T16:07:08Z","close_reason":"Round 18: implemented; merged; 414+ Rust tests pass","dependencies":[{"issue_id":"attn-nnj.7.7","depends_on_id":"attn-nnj.2.10","type":"blocks","created_at":"2026-05-18T16:58:50Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.7.7","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-18T16:32:15Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.7.7","depends_on_id":"attn-nnj.7.1","type":"blocks","created_at":"2026-05-18T16:35:53Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.7.7","depends_on_id":"attn-nnj.7.3","type":"blocks","created_at":"2026-05-18T16:36:03Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.7.5","title":"Mode-aware transport selector in ReviewManager","description":"In ReviewManager, route outbound events based on policy.mode: hybrid sends through BOTH transports (DataChannel when connected, mailbox always-on); live uses WebRTC only; async uses mailbox only. Inbound dedupe is handled by the existing EventId-keyed import so the receiver doesn't double-process when both transports deliver. This is the integration point where mailbox + WebRTC become 'one transport with two wires' from the manager's perspective.","acceptance_criteria":"ReviewManager has a transport selector that consults policy.mode at send time.\nhybrid: outbound envelope is enqueued to BOTH transports; receiver dedupes by EventId on import.\nlive: outbound goes only to WebRTC; if WebRTC is Failed, send returns an error that surfaces as RoomStatusChanged(DirectFailed) (per issue 4).\nasync: outbound goes only to mailbox; WebRTC never initialized.\nInbound import is idempotent — receiving the same EventId twice (once from each transport) is a no-op the second time.\nTested with all three mode values.","notes":"Specs: planning/collab/amendments.md §Phase 4 (mode semantics + dedupe). Files: src/review/manager.rs. Idempotent import probably already exists from Phase 0b/3b store work — verify and reuse. Don't add EventId-tracking state here; the store layer owns dedupe.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:49Z","created_by":"James Lal","updated_at":"2026-05-19T14:20:07Z","started_at":"2026-05-19T13:53:28Z","closed_at":"2026-05-19T14:20:07Z","close_reason":"Implemented; 400 Rust + 193 relay tests pass","dependencies":[{"issue_id":"attn-nnj.7.5","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-18T16:31:48Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.7.5","depends_on_id":"attn-nnj.7.1","type":"blocks","created_at":"2026-05-18T16:35:52Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.7.5","depends_on_id":"attn-nnj.7.3","type":"blocks","created_at":"2026-05-18T16:36:02Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.7.5","depends_on_id":"attn-nnj.7.4","type":"blocks","created_at":"2026-05-18T16:36:03Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.5.9","title":"POST /v2/rooms/:roomId/acks — acknowledgment + optional delete","description":"Mark envelopes as ACKed by deviceId. Body: {acks: [{envelopeId, deviceId}], delete?:boolean}. Idempotent. If delete=true AND policy.deleteEventsAfterOwnerAck==true AND the request carries a valid Attn-Owner-Signature, delete envelopes that have been ACKed by ANY owner-kind device. Otherwise just record the ACK without deletion (multi-device safety per decision #12).","acceptance_criteria":"- Validates body schema; deviceId must be registered in the room\n- Records ACK in DO storage under acks:\u003cenvelopeId\u003e:\u003cdeviceId\u003e\n- delete=true is conditional on: policy.deleteEventsAfterOwnerAck===true (default false per decision #12) AND owner signature verifies AND envelope has at least one owner-device ACK\n- Deletion is by-envelope: removes envelope:\u003cseq\u003e entries (and blob R2 keys for snapshot_blob), decrements meta:envelope_count and meta:bytes_used\n- Updates meta:oldest_retained_seq if a leading run of envelopes is deleted\n- Idempotent: re-ACK is a no-op; re-delete of already-deleted envelope returns 200 (count: 0)\n- Returns {acked:[envelopeId...], deleted:[envelopeId...]}\n- Tests: ack-only, ack+delete with owner sig, ack+delete without owner sig (no-op deletion), ack+delete with policy.deleteEventsAfterOwnerAck=false (no-op deletion)","notes":"Spec: planning/collab/relay-spec.md §POST /v2/rooms/:roomId/acks (277-303). amendments.md decisions #3 (owner-sig gating) and #12 (deleteEventsAfterOwnerAck default false). Owner-sig verification reused from issue 3a-4.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:48Z","created_by":"James Lal","updated_at":"2026-05-19T04:08:01Z","started_at":"2026-05-19T03:42:00Z","closed_at":"2026-05-19T04:08:01Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.5.9","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:31:47Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.9","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:27Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.9","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:44Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.9","depends_on_id":"attn-nnj.5.3","type":"blocks","created_at":"2026-05-18T16:35:57Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.9","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:44Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.5.8","title":"POST + GET /v2/rooms/:roomId/blobs — R2 spillover","description":"When kind==snapshot_blob and ciphertextBytes \u003e 1 MiB, /envelopes is rejected and the client must use the blob flow. POST /v2/rooms/:roomId/blobs returns a presigned PUT URL (15-min TTL) at R2 key rooms/\u003croomId\u003e/blobs/\u003cenvelopeId\u003e. Client uploads ciphertext directly, then re-POSTs /envelopes with the same envelopeId and a small BlobRef payload referencing the R2 key. GET /v2/rooms/:roomId/blobs/:envelopeId returns a 5-min presigned GET URL.","acceptance_criteria":"- POST /blobs body validates {envelopeId, kind:'snapshot_blob', ciphertextBytes, hash}; rejects when ciphertextBytes ≤ 1 MiB (use /envelopes path instead)\n- Returns {uploadUrl, expiresAt} — presigned PUT, 15-min TTL, key=rooms/\u003croomId\u003e/blobs/\u003cenvelopeId\u003e\n- After upload, client re-POSTs /envelopes with kind=snapshot_blob and ciphertext = canonical BlobRef payload ({r2Key, ciphertextBytes, hash})\n- GET /blobs/:envelopeId returns {downloadUrl, expiresAt} — presigned GET, 5-min TTL\n- Validates envelopeId belongs to this room (check DO state) before issuing GET URL\n- Counts blob bytes against meta:bytes_used (against policy.maxRoomBytes)\n- Tests: upload roundtrip, undersized rejection, replay (same envelopeId returns same key), unauthorized GET","notes":"Spec: planning/collab/relay-spec.md §POST /v2/rooms/:roomId/blobs (313-360), §R2 Integration (557-563). Lifecycle TTL = 7 days as safety net (decision #9); primary cleanup is the DO alarm in issue 3a-12.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:47Z","created_by":"James Lal","updated_at":"2026-05-19T14:19:52Z","started_at":"2026-05-19T13:53:27Z","closed_at":"2026-05-19T14:19:52Z","close_reason":"Implemented; 400 Rust + 193 relay tests pass","dependencies":[{"issue_id":"attn-nnj.5.8","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:31:47Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.8","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:27Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.8","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:42Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.8","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:43Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.5.7","title":"POST /v2/rooms/:roomId/envelopes — batched ingest","description":"Accept up to 32 envelopes per HTTP request (decision #7). Each envelope is validated end-to-end: ciphertextBytes equals base64url-decoded ciphertext length; size against policy.max{Event|Snapshot}Bytes by kind; deviceId and authorId are registered in the room. Single PoW token covers the whole batch. On any cap overflow (envelope_count \u003e policy.maxEvents OR bytes_used + delta \u003e policy.maxRoomBytes) the whole batch fails with 507. Idempotent on envelopeId. Signal envelopes use sub-caps with FIFO eviction. On success, allocates serverSeq, updates counters, reschedules idle alarm.","acceptance_criteria":"- Batch cap 32: 33+ envelopes → 400 ATTN_BATCH_TOO_LARGE before any work\n- Single Attn-PoW token verified once for the whole HTTP request (resource binds METHOD + PATH, not per-envelope)\n- Per envelope: ciphertextBytes === base64url_decoded.length else 400 ATTN_ENVELOPE_SIZE_MISMATCH; kind-specific size cap against policy.max{Event|Snapshot}Bytes else 413 ATTN_ENVELOPE_TOO_LARGE; deviceId + authorId registered else 403 ATTN_UNKNOWN_DEVICE\n- serverSeq allocated atomically per envelope (monotonic, per relay-spec §serverSeq Allocation)\n- Idempotency: existing envelopeId returns its stored serverSeq with no state change\n- Whole-batch overflow: 507 ATTN_ROOM_EVENT_CAP or ATTN_ROOM_STORAGE_FULL (no partial commit)\n- Signal envelopes: maxSignalEnvelopes=64 per (authorId, target.deviceId), FIFO-evict oldest in DO storage\n- Updates meta:envelope_count, meta:bytes_used, meta:last_event_at; reschedules idle alarm\n- Broadcasts envelope frames to subscribed WS sessions (hibernation-safe)\n- Response: {accepted:[{envelopeId, serverSeq}], serverSeq:\u003cmax\u003e}\n- Tests: batch over cap, mixed kinds, idempotent retry, room-full, signal eviction, PoW reuse across batch","notes":"Spec: planning/collab/relay-spec.md §POST /v2/rooms/:roomId/envelopes (218-269), §serverSeq Allocation (499-512), §Caps (535-555). amendments.md decisions #7 (batch cap + single PoW) and #5 (WS-only delivery: this endpoint feeds the WS broadcast, no HTTP GET pull).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:46Z","created_by":"James Lal","updated_at":"2026-05-19T02:29:21Z","started_at":"2026-05-19T01:57:29Z","closed_at":"2026-05-19T02:29:21Z","close_reason":"Implemented (3.8+round-10 retries); merged into collab; 267 Rust + 144 relay tests pass","dependencies":[{"issue_id":"attn-nnj.5.7","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:31:46Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.7","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:26Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.7","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:41Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.7","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:42Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":3,"comment_count":0} -{"id":"attn-nnj.7.4","title":"Connection state machine + ICE handling","description":"Trickle ICE per relay-spec.md §Signaling. Track the peer-connection lifecycle through Connecting / Connected / Reconnecting / Failed states. Behavior on Failed is mode-dependent: in live mode, surface direct-connection failure explicitly via ReviewUpdate::RoomStatusChanged(DirectFailed) so the UI can tell the user the live channel is gone; in hybrid mode, silently rely on mailbox (no user-visible disruption — that's the whole point of hybrid). Reconnecting handles transient NAT rebinds without bouncing the user.","acceptance_criteria":"Connection state enum {Connecting, Connected, Reconnecting, Failed} drives WebRTC peer lifecycle.\nTrickle ICE: candidates emit as individual kind=signal envelopes as they're gathered (not batched at end-of-gathering).\nOn Failed in live mode → emit ReviewUpdate::RoomStatusChanged(DirectFailed). Frontend can surface 'live connection lost' UI.\nOn Failed in hybrid mode → no user-visible event; mailbox continues serving traffic.\nReconnecting attempts ICE restart before transitioning to Failed.\nState transitions covered by unit tests against a mock PeerConnection.","notes":"Specs: planning/collab/relay-spec.md §Signaling (trickle ICE protocol), planning/collab/amendments.md §Phase 4 (mode semantics). Files: src/review/transport/webrtc.rs (state machine), src/review/manager.rs (ReviewUpdate emission). Mode comes from policy.mode on the room. RoomStatusChanged is a new ReviewUpdate variant — add to the enum.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:35Z","created_by":"James Lal","updated_at":"2026-05-19T13:50:43Z","started_at":"2026-05-19T04:31:04Z","closed_at":"2026-05-19T13:50:43Z","close_reason":"Implemented (partially for 5.14 — scaffold + skip-on-empty, follow-up to fill cases.json); 372 Rust + 184 relay tests pass","dependencies":[{"issue_id":"attn-nnj.7.4","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-18T16:31:35Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.7.4","depends_on_id":"attn-nnj.7.1","type":"blocks","created_at":"2026-05-18T16:35:51Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.7.4","depends_on_id":"attn-nnj.7.2","type":"blocks","created_at":"2026-05-18T16:36:01Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.7.4","depends_on_id":"attn-nnj.7.3","type":"blocks","created_at":"2026-05-18T16:36:01Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.7.3","title":"DataChannel arm of Transport trait","description":"Implement WebRTCTransport against the Transport trait defined in Phase 3b. The crucial property (decision #14, amendments.md §Phase 4): the DataChannel envelope FORMAT is identical to mailbox envelopes — same AEAD under eventKey/snapshotKey, same routing, same import path. Only the wire differs. Frontend never sees raw transport; ReviewManager decrypts, signature-verifies, then emits typed ReviewUpdate. This is what lets hybrid mode dedupe by EventId across both transports for free.","acceptance_criteria":"WebRTCTransport struct implements the same Transport trait as MailboxTransport from Phase 3b.\nOutbound: events go through the existing outbox; when DataChannel is connected, send_envelope writes the same AEAD-encrypted envelope bytes to the channel.\nInbound: DataChannel on_message decrypts under eventKey/snapshotKey and feeds into the same envelope-import pipeline.\nNo plaintext on the wire (decision #14) — snapshot bytes are application-encrypted, never relying on DTLS for confidentiality.\nTrait abstraction allows a single ReviewManager codepath to consume both transports interchangeably.","notes":"Specs: planning/collab/amendments.md §Phase 4 + Decision #14. Files: src/review/transport/webrtc.rs (new), src/review/transport.rs (trait). Reuses the import pipeline built in Phase 3b — do NOT duplicate decrypt/verify logic. The wire is different but the envelope is the same — this is load-bearing for hybrid mode.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:21Z","created_by":"James Lal","updated_at":"2026-05-19T04:30:20Z","started_at":"2026-05-19T04:09:05Z","closed_at":"2026-05-19T04:30:20Z","close_reason":"Implemented; merged","dependencies":[{"issue_id":"attn-nnj.7.3","depends_on_id":"attn-nnj.6.1","type":"blocks","created_at":"2026-05-18T16:38:28Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.7.3","depends_on_id":"attn-nnj.6.4","type":"blocks","created_at":"2026-05-18T16:38:29Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.7.3","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-18T16:31:20Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.7.3","depends_on_id":"attn-nnj.7.1","type":"blocks","created_at":"2026-05-18T16:35:51Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.7.3","depends_on_id":"attn-nnj.7.2","type":"blocks","created_at":"2026-05-18T16:36:00Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":4,"dependent_count":3,"comment_count":0} -{"id":"attn-nnj.5.6","title":"POST + GET /v2/rooms/:roomId/devices — device registration","description":"POST /devices: upsert device record. Verifies selfSignature (Ed25519 over canonical device bytes) against publicSigningKey. If kind==owner, publicSigningKey MUST equal the room's ownerSigningKey, else 403 ATTN_OWNER_KEY_MISMATCH. Upsert is keyed by (participantId, deviceId); attempting to change publicSigningKey for an existing entry returns 409 ATTN_DEVICE_KEY_CHANGED. GET /devices returns the peer list in original registration order.","acceptance_criteria":"- POST validates schema: {participantId, deviceId, kind in [owner|reviewer|agent], publicSigningKey, selfSignature, capabilities?}\n- selfSignature verifies against publicSigningKey over the canonical device bytes from crypto-spec.md §Signing-Key Publication\n- kind==owner: publicSigningKey === room.ownerSigningKey else 403 ATTN_OWNER_KEY_MISMATCH\n- Upsert by (participantId, deviceId): if exists and key matches, return 200 with stored record; if key differs, 409 ATTN_DEVICE_KEY_CHANGED\n- GET /devices returns array in registration order; includes a server-stable 'registeredAt' timestamp\n- Updates meta:peer_count, enforces policy.maxPeers (8 cap) — 403 ATTN_ROOM_FULL when exceeded\n- Tests cover: fresh register, idempotent re-register, key-change rejection, owner-key mismatch, peer cap","notes":"Spec: planning/collab/relay-spec.md §POST /v2/rooms/:roomId/devices (169-217). crypto-spec.md §Signing-Key Publication (344-403). The devices list is the source of truth for sig verification on inbound envelopes (Phase 3b consumes this).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:08Z","created_by":"James Lal","updated_at":"2026-05-19T01:56:14Z","started_at":"2026-05-19T01:36:53Z","closed_at":"2026-05-19T01:56:14Z","close_reason":"Implemented; merged into collab; 254 Rust tests + 130 relay tests pass; corpus replay green","dependencies":[{"issue_id":"attn-nnj.5.6","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:31:07Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.6","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:26Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.6","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:40Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.6","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:40Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":2,"comment_count":0} -{"id":"attn-nnj.5.5","title":"POST /v2/rooms/:roomId — idempotent room creation","description":"Implement POST /v2/rooms/:roomId. Idempotent: first call creates the room with the supplied policy + ownerSigningKey, subsequent calls with the same body return the stored policy unchanged (no mutation). Body is validated against the zod schema from the scaffold issue. Policy values are clamped to spec maxima before storage. Computes and returns ownerSigningKeyId = base64url(SHA-256(ownerSigningKey)).","acceptance_criteria":"- Body validates against zod RoomCreateRequest schema (admissionKey, ownerSigningKey, policy, ...)\n- Policy clamping: maxPeers ≤ 8; expiresAt ≤ created+24h, or +7d when longSession=true; idleTimeoutMs ≥ 60_000 and ≤ wall-clock TTL; powBits ∈ [12,24]; max{Event,Snapshot,RoomBytes} ≤ HARD_MAX_*\n- Stores ownerSigningKey, computes ownerSigningKeyId = base64url(SHA-256(key))\n- First create: 201 with {policy, ownerSigningKeyId, serverSeq:0, oldestRetainedSeq:0}\n- Replay of same body: 200 with the stored values (no mutation, idempotent)\n- Different body for an existing room: 409 ATTN_ROOM_EXISTS_DIFFERENT_POLICY\n- Initial bytes_used=0, envelope_count=0, hard_max_at = created + min(policy.expiresAt - created, 24h|7d cap)\n- Test: clamping behavior, idempotency, conflict on policy diff","notes":"Spec: planning/collab/relay-spec.md §POST /v2/rooms/:roomId (lines 114-167). §Caps (535-555). amendments.md decision #8 (TTLs, longSession). Note: this endpoint does NOT require pre-existing admission since it establishes the admissionKey. PoW IS still required (decision #6).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:07Z","created_by":"James Lal","updated_at":"2026-05-19T01:33:37Z","started_at":"2026-05-19T00:47:52Z","closed_at":"2026-05-19T01:33:37Z","close_reason":"Implemented; merged into collab; 212 Rust tests + 98 relay tests pass","dependencies":[{"issue_id":"attn-nnj.5.5","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:31:06Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.5","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:25Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.5","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:39Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.5","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:39Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.7.2","title":"Encrypted signaling envelopes (signalingKey + AAD)","description":"Build the signaling layer for WebRTC negotiation. SDP offers/answers and ICE candidates ride the same mailbox transport as regular envelopes — same POST /envelopes upload, same WS delivery, just kind=signal. Cleartext payload is canonical({kind: offer|answer|ice, sdp|ice[], from: deviceId}), encrypted with signalingKey under standard envelope AAD. On the receive side, WS envelopes with kind=signal decrypt and dispatch into webrtc-rs as SDP/ICE inputs. This is what makes the data-channel handshake survive NAT without a separate signaling channel.","acceptance_criteria":"src/review/transport/signaling.rs (new) builds offer/answer/ice envelopes with kind=signal, encrypted under signalingKey with AAD binding.\nOutbound signaling envelopes upload via the existing POST /envelopes pipeline from Phase 3b — no new HTTP path.\nInbound WS envelope dispatcher routes kind=signal frames into the signaling decoder, decrypts, dispatches to webrtc-rs callbacks.\nUnit test: round-trip an offer/answer pair through encrypt → decrypt and assert canonical equality.\nsignalingKey derivation matches crypto-spec.md (HKDF subkey under rootKey).","notes":"Specs: planning/collab/relay-spec.md §Signaling, planning/collab/crypto-spec.md §Key Derivation, planning/collab/amendments.md §Phase 4. Files: src/review/transport/signaling.rs (new), src/review/transport.rs (dispatcher integration). Reuses outbox + WS envelope plumbing from Phase 3b. AAD must bind (roomId, envelopeId, kind=signal, from).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:02Z","created_by":"James Lal","updated_at":"2026-05-19T04:08:15Z","started_at":"2026-05-19T03:42:00Z","closed_at":"2026-05-19T04:08:15Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.7.2","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-18T16:31:02Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.7.2","depends_on_id":"attn-nnj.7.1","type":"blocks","created_at":"2026-05-18T16:35:50Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} -{"id":"attn-nnj.7.1","title":"webrtc-rs dependency + binary-size gate","description":"Add webrtc-rs to Cargo.toml as the foundation for Phase 4. Pre-merge gate: confirm release binary stays under 25 MiB target (decision #1 tradeoff). webrtc-rs transitively pulls tokio, rcgen, sctp, dtls, openssl-sys or rustls — this issue is the gate where we discover whether to feature-flag aggressively or escalate. Blocks all downstream Phase 4 work; if the gate fails we re-scope before sinking effort into signaling/datachannel code.","acceptance_criteria":"webrtc-rs added to Cargo.toml with chosen feature set documented inline.\ncargo build --release succeeds on macOS aarch64.\ncargo tree -e features --no-default-features --no-dev-dependencies output captured and committed under planning/collab/ or attached to issue notes.\ndu -h on the release .app bundle (or stripped binary if not bundled) is recorded; total under 25 MiB.\nIf gate exceeded: feature flags to evaluate are listed in a comment, OR issue is escalated via bd human and downstream work is held.","notes":"Spec: planning/collab/amendments.md §Phase 4 WebRTC + Decision #1. Files: Cargo.toml (root). Use rustls backend (not openssl-sys) by default to keep the binary smaller and avoid system-OpenSSL coupling. Verify gate via the existing scripts/build.sh release path so it matches what ships. Blocks: every other Phase 4 issue.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:46Z","created_by":"James Lal","updated_at":"2026-05-19T04:06:55Z","started_at":"2026-05-19T03:23:03Z","closed_at":"2026-05-19T04:06:55Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.7.1","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-18T16:30:45Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":6,"comment_count":0} -{"id":"attn-nnj.4.14","title":"'Comment survives owner edits' demo + e2e check","description":"End-to-end scripted scenario using the mock-ipc scenario stream: owner edits a paragraph, the reviewer's previously-attached comment remaps via the anchor engine, decoration stays attached, and the 'moved' badge state shows in the panel. Repeatable via existing automation flags (--query, --eval).","acceptance_criteria":"- Scenario JSON in web/src/lib/mock-ipc-scenarios/ drives the demo\n- Repeatable test invocation documented (uses attn --query / --eval)\n- Asserts: decoration present after edit, status is remapped (0.70-0.89) or exact (\u003e=0.90)\n- Asserts: 'moved' badge shown in panel when remapped\n- Wired into scripts/test-e2e.sh or a sibling script","notes":"Spec refs: amendments.md Decision #15 cutoffs; data-model.md §Anchor engine. Uses attn --query and --eval automation flags (debug builds only). Depends on 2-1 mock-ipc, 2-6 decorations. Aligns with project rule: prefer in-app UI assertions; no 'any' types.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:43Z","created_by":"James Lal","updated_at":"2026-05-19T17:35:33Z","started_at":"2026-05-19T17:02:45Z","closed_at":"2026-05-19T17:35:33Z","close_reason":"Round 21 (final push): implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.4.14","depends_on_id":"attn-nnj.11.4","type":"blocks","created_at":"2026-05-18T16:38:31Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.14","depends_on_id":"attn-nnj.3.5","type":"blocks","created_at":"2026-05-18T16:38:22Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.14","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:42Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.14","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:28Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.14","depends_on_id":"attn-nnj.4.6","type":"blocks","created_at":"2026-05-18T16:31:34Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.4.11","title":"Connection badge","description":"Header connection indicator with four states from data-model.md: Live direct / Mailbox / Offline / Direct failed. Subscribes to reviewStatus updates from the store. Direct-failed state surfaces a retry affordance.","acceptance_criteria":"- Connection badge present in header at location decided in UX-3\n- All four states render with distinct visual treatment\n- Subscribes to reviewStatus via review store\n- Direct-failed state includes retry affordance (button or click action)\n- Retry action calls appropriate IPC (mocked)","notes":"Spec refs: data-model.md §UI/UX Changes (owner connection badge: Live direct / Mailbox / Offline / Direct failed). Depends on UX-3, 2-1, 2-2. No 'any' types.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:41Z","created_by":"James Lal","updated_at":"2026-05-19T16:24:57Z","started_at":"2026-05-19T16:07:54Z","closed_at":"2026-05-19T16:24:57Z","close_reason":"Round 19: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.4.11","depends_on_id":"attn-nnj.10.1","type":"blocks","created_at":"2026-05-18T16:30:59Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.11","depends_on_id":"attn-nnj.10.2","type":"blocks","created_at":"2026-05-18T16:31:04Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.11","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:11Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.11","depends_on_id":"attn-nnj.12.1","type":"blocks","created_at":"2026-05-18T16:53:49Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.11","depends_on_id":"attn-nnj.12.7","type":"blocks","created_at":"2026-05-18T16:53:53Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.11","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:40Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.11","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:26Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.11","depends_on_id":"attn-nnj.4.2","type":"blocks","created_at":"2026-05-18T16:31:31Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":7,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.4.10","title":"Share button + room mode selector","description":"Toolbar share affordance opens a dialog with room mode (Live / Async 24h / Async 7d / Hybrid), displays the generated attn://review/... URL, and provides a copy button. Wires to IPC review_share (mocked in this phase via 2-1).","acceptance_criteria":"- Share button present in toolbar at location decided in UX-3\n- Dialog opens with mode selector: Live / Async 24h / Async 7d / Hybrid\n- Generated attn://review/... URL displayed\n- Copy-to-clipboard button works (uses in-app feedback, not alert())\n- Calls IPC review_share; mock-ipc returns a fake URL for now\n- Dialog is in-app modal (no window.confirm/prompt)","notes":"Spec refs: data-model.md §UI/UX Changes (owner share + room mode); UX-3 for placement. Depends on UX-3, 2-1 mock-ipc. No 'any' types. No window.confirm/alert/prompt — use in-app modal.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:39Z","created_by":"James Lal","updated_at":"2026-05-19T16:06:04Z","started_at":"2026-05-19T15:31:08Z","closed_at":"2026-05-19T16:06:04Z","close_reason":"Round 18: implemented; merged; 414+ Rust tests pass","dependencies":[{"issue_id":"attn-nnj.4.10","depends_on_id":"attn-nnj.10.1","type":"blocks","created_at":"2026-05-18T16:30:59Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.10","depends_on_id":"attn-nnj.10.2","type":"blocks","created_at":"2026-05-18T16:31:04Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.10","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:10Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.10","depends_on_id":"attn-nnj.12.1","type":"blocks","created_at":"2026-05-18T16:53:48Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.10","depends_on_id":"attn-nnj.12.5","type":"blocks","created_at":"2026-05-18T16:53:59Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.10","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:39Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.10","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:25Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":6,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.4.9","title":"Snapshot badge + status row in editor header","description":"Editor header status row showing snapshot state. Owner-side: 'Snapshot current' / 'Snapshot superseded' / 'Reviewer on older snapshot'. Reviewer-side: snapshot age (e.g., '3 min ago') + 'owner is on a newer snapshot' notice. Subscribes to reviewSnapshot updates from the store.","acceptance_criteria":"- Editor header includes snapshot badge row\n- Owner states implemented: current / superseded / reviewer-on-older\n- Reviewer states implemented: age display + newer-snapshot notice\n- Subscribes to reviewSnapshot updates from review store\n- Visual treatment matches planning/collab/ui/presence-identity.md and connection-share.md","notes":"Spec refs: data-model.md §UI/UX Changes (snapshot badge owner/reviewer); UX-5 (presence-identity) for reviewer-on-older treatment. Depends on UX-1, UX-3, 2-2 store. No 'any' types.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:39Z","created_by":"James Lal","updated_at":"2026-05-19T16:55:34Z","started_at":"2026-05-19T16:26:11Z","closed_at":"2026-05-19T16:55:34Z","close_reason":"Round 20: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.4.9","depends_on_id":"attn-nnj.10.1","type":"blocks","created_at":"2026-05-18T16:30:58Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.9","depends_on_id":"attn-nnj.10.2","type":"blocks","created_at":"2026-05-18T16:31:03Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.9","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:10Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.9","depends_on_id":"attn-nnj.10.5","type":"blocks","created_at":"2026-05-18T16:31:45Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.9","depends_on_id":"attn-nnj.12.1","type":"blocks","created_at":"2026-05-18T16:53:47Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.9","depends_on_id":"attn-nnj.12.7","type":"blocks","created_at":"2026-05-18T16:53:52Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.9","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:38Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.9","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:24Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":7,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.4.8","title":"Stale comment panel state","description":"When a ResolvedAnchor has status 'stale', the panel renders a status pill 'could not find this text anymore' and a 'Re-anchor manually' affordance that switches the editor into a select-text-in-editor mode; the next text selection re-anchors the comment.","acceptance_criteria":"- Stale-state card shows clear status pill and original anchor preview\n- 'Re-anchor manually' button enters editor select mode\n- Next editor selection re-anchors and exits select mode\n- Cancel/escape exits select mode without re-anchoring\n- Store updates resolution to exact after re-anchor","notes":"Spec refs: data-model.md §ResolvedAnchor status 'stale'; amendments.md Decision #15 ('stale → panel-only, requires manual re-anchor'). Depends on UX-1, 2-2, 2-3 panel. No 'any' types.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:38Z","created_by":"James Lal","updated_at":"2026-05-19T16:57:52Z","started_at":"2026-05-19T16:26:10Z","closed_at":"2026-05-19T16:57:52Z","close_reason":"Round 20: implemented (force; design-doc dep)","dependencies":[{"issue_id":"attn-nnj.4.8","depends_on_id":"attn-nnj.10.1","type":"blocks","created_at":"2026-05-18T16:30:58Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.8","depends_on_id":"attn-nnj.10.2","type":"blocks","created_at":"2026-05-18T16:31:03Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.8","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:09Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.8","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:37Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.8","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:24Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.8","depends_on_id":"attn-nnj.4.2","type":"blocks","created_at":"2026-05-18T16:31:30Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":5,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.4.7","title":"Ambiguous anchor picker","description":"When a ResolvedAnchor has status 'ambiguous', the review panel surfaces a picker listing each candidate with its preview text and confidence score. Owner clicks one candidate → emits AnchorManuallyResolved event via IPC, store updates, decoration moves to inline.","acceptance_criteria":"- Ambiguous-state card in panel shows candidate list with preview + confidence\n- Picker UI is keyboard-navigable (arrow keys + enter)\n- Selecting a candidate emits AnchorManuallyResolved via IPC\n- Store transitions the anchor to a non-ambiguous resolution after pick\n- Decoration moves from panel-only to inline after pick","notes":"Spec refs: data-model.md §ResolvedAnchor status 'ambiguous'; amendments.md Decision #15 ('ambiguous → panel-only with picker'). Depends on UX-1, 2-1, 2-2, 2-3 panel. No 'any' types.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:37Z","created_by":"James Lal","updated_at":"2026-05-19T16:24:40Z","started_at":"2026-05-19T16:07:54Z","closed_at":"2026-05-19T16:24:40Z","close_reason":"Round 19: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.4.7","depends_on_id":"attn-nnj.10.1","type":"blocks","created_at":"2026-05-18T16:30:57Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.7","depends_on_id":"attn-nnj.10.2","type":"blocks","created_at":"2026-05-18T16:31:01Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.7","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:09Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.7","depends_on_id":"attn-nnj.12.5","type":"blocks","created_at":"2026-05-18T16:53:58Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.7","depends_on_id":"attn-nnj.12.8","type":"blocks","created_at":"2026-05-18T16:53:55Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.7","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:37Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.7","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:23Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.7","depends_on_id":"attn-nnj.4.2","type":"blocks","created_at":"2026-05-18T16:31:30Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":7,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.4.6","title":"Inline decoration system (ProseMirror plugin)","description":"New ProseMirror plugin at web/src/lib/prosemirror/review-decorations.ts. Reads ResolvedAnchor entries from the review store. Emits decorations per amendments.md Decision #15 cutoffs: ≥0.90 inline highlight no badge; 0.70-0.89 inline highlight + 'moved' badge in panel; ambiguous panel-only; stale panel-only. Handles overlap. Hover focuses the corresponding panel entry; clicking a panel entry scrolls editor to the decoration.","acceptance_criteria":"- web/src/lib/prosemirror/review-decorations.ts plugin exists, mirroring existing PM plugin pattern (math/tables/etc)\n- Reads from review store; updates as ResolvedAnchor entries change\n- Cutoffs implemented exactly per amendments.md Decision #15\n- Overlap handling implemented (stacked / layered)\n- Hover decoration ↔ focus panel entry wired both ways\n- Click panel entry → editor scrolls to decoration\n- No 'any' types","notes":"Spec refs: amendments.md Decision #15 (verbatim cutoffs); planning/collab/ui/inline-decorations.md (from UX-2). Existing PM plugin pattern: web/src/lib/prosemirror/{math,tables,code-highlight,code-block-nodeview,mermaid-nodeview}.ts. Depends on UX-2, 2-1 mock-ipc, 2-2 store.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:36Z","created_by":"James Lal","updated_at":"2026-05-19T15:29:39Z","started_at":"2026-05-19T15:07:27Z","closed_at":"2026-05-19T15:29:39Z","close_reason":"Implemented; merged; 409 Rust + 237 relay tests pass; store reassembled from 4.6 + 8.3 merge race","dependencies":[{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.10.1","type":"blocks","created_at":"2026-05-18T16:30:57Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.10.2","type":"blocks","created_at":"2026-05-18T16:31:01Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:05Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.12.2","type":"blocks","created_at":"2026-05-18T16:53:50Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.12.7","type":"blocks","created_at":"2026-05-18T16:53:52Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.3.5","type":"blocks","created_at":"2026-05-18T16:38:22Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:35Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:22Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.4.2","type":"blocks","created_at":"2026-05-18T16:31:29Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":8,"dependent_count":2,"comment_count":0} -{"id":"attn-nnj.4.5","title":"Suggestion composer from selection","description":"ProseMirror selection → 'Suggest' UI offering replace / delete / insert-before / insert-after operations. Captures expectedText automatically for replace and delete from the current selection. Validates non-empty replacement before allowing submit. Submits via IPC review_create_suggestion.","acceptance_criteria":"- Selection surfaces a 'Suggest' affordance distinct from 'Comment'\n- Four operation modes: replace, delete, insert-before, insert-after\n- expectedText auto-captured from selection for replace/delete\n- Empty replacement blocked from submission (with inline error, not alert())\n- Submits via IPC review_create_suggestion; mock-ipc echoes back\n- Cancel/escape closes cleanly","notes":"Spec ref: data-model.md §Suggestion + §UI/UX Changes (suggestion card). Pairs with 2-4 comment composer. Depends on 2-1 mock-ipc, 2-2 store. No window.confirm/alert. No 'any' types.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:35Z","created_by":"James Lal","updated_at":"2026-05-19T16:24:24Z","started_at":"2026-05-19T16:07:53Z","closed_at":"2026-05-19T16:24:24Z","close_reason":"Round 19: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.4.5","depends_on_id":"attn-nnj.12.5","type":"blocks","created_at":"2026-05-18T16:53:58Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.5","depends_on_id":"attn-nnj.12.8","type":"blocks","created_at":"2026-05-18T16:53:54Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.5","depends_on_id":"attn-nnj.12.9","type":"blocks","created_at":"2026-05-18T16:53:56Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.5","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:34Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.5","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:22Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.4.4","title":"Comment composer from selection","description":"ProseMirror selection → 'Comment' popover near selection → text body input → submit. Constructs an Anchor from the selection range and calls IPC review_create_comment. For mock IPC, fakes the event back via reviewEvent so the panel updates immediately.","acceptance_criteria":"- Selection in ProseMirror surfaces a 'Comment' affordance (popover or floating button)\n- Body input supports multi-line text\n- Submit builds a valid Anchor (per data-model.md) from the selection\n- Calls IPC review_create_comment; mock-ipc returns and echoes the event\n- Cancel/escape closes without submitting\n- Empty body blocked from submission","notes":"Spec ref: data-model.md §Comment composer from selection and Anchor structure. Wire alongside web/src/lib/Editor.svelte (ProseMirror view). Depends on 2-1 mock-ipc, 2-2 store. No 'any' types.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:34Z","created_by":"James Lal","updated_at":"2026-05-19T16:24:08Z","started_at":"2026-05-19T16:07:53Z","closed_at":"2026-05-19T16:24:08Z","close_reason":"Round 19: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.4.4","depends_on_id":"attn-nnj.12.5","type":"blocks","created_at":"2026-05-18T16:53:57Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.4","depends_on_id":"attn-nnj.12.8","type":"blocks","created_at":"2026-05-18T16:53:54Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.4","depends_on_id":"attn-nnj.12.9","type":"blocks","created_at":"2026-05-18T16:53:55Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.4","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:33Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.4","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:21Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.4.3","title":"ReviewMargin.svelte: Google-Docs-style margin sticky cards","description":"Right-margin overlay rendering review cards positioned to vertically align with their anchor in the editor. Replaces the earlier panel-river design after the user pivoted to Google Docs spatial model (cards live next to their anchored text, not in a separate triage list). Includes an 'orphan tray' at the top of the margin for ambiguous/stale cards that have no valid anchor position. Reuses Phase 0c plumbing (review store, types, theme vars, popover helper); only the layout slot from 12.1 needs an overlay adjustment (already on disk, not yet a rewrite).","acceptance_criteria":"- web/src/lib/ReviewPanel.svelte exists and renders from review store\n- Grouping matches planning/collab/ui/review-panel-design.md\n- Comment and suggestion card variants implemented with author/anchor/body/status/actions\n- Empty and loading states implemented\n- Keyboard shortcut registered (consistent with KeyboardShortcutsDialog.svelte)\n- No window.confirm/alert — uses in-app UI only","notes":"Depends on: UX-1 (panel design), 2-2 (store). Existing patterns: Sidebar.svelte for rail, KeyboardShortcutsDialog.svelte for shortcut registration. Svelte 5 runes throughout. No 'any' types.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:33Z","created_by":"James Lal","updated_at":"2026-05-19T16:05:48Z","started_at":"2026-05-19T15:31:07Z","closed_at":"2026-05-19T16:05:48Z","close_reason":"Round 18: implemented; merged; 414+ Rust tests pass","dependencies":[{"issue_id":"attn-nnj.4.3","depends_on_id":"attn-nnj.10.1","type":"blocks","created_at":"2026-05-18T16:30:56Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.3","depends_on_id":"attn-nnj.10.2","type":"blocks","created_at":"2026-05-18T16:31:00Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.3","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:05Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.3","depends_on_id":"attn-nnj.12.1","type":"blocks","created_at":"2026-05-18T16:53:47Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.3","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:33Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.3","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:21Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.3","depends_on_id":"attn-nnj.4.2","type":"blocks","created_at":"2026-05-18T16:31:28Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":6,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.4.1","title":"Mock-IPC extension: review event stream","description":"Extend web/src/lib/mock-ipc.ts to emit window.__attn__.reviewStatus(payload), reviewEvent(payload), reviewSnapshot(snapshot), and reviewAnchorResolution(update). Add a replayable scripted scenario JSON in web/src/lib/mock-ipc-scenarios/ that demonstrates: owner edits paragraph → reviewer comments → owner edits more → reviewer's anchor remaps → owner accepts. Enables Phase 2 frontend work to proceed without any Rust crates landing.","acceptance_criteria":"- mock-ipc.ts exposes reviewStatus, reviewEvent, reviewSnapshot, reviewAnchorResolution on window.__attn__\n- web/src/lib/mock-ipc-scenarios/ contains at least one JSON scenario file\n- Replay runs deterministically via dev-tools trigger (button or window helper)\n- Scenario covers owner-edits → reviewer-comments → remap → accept flow\n- Documented in a short README in mock-ipc-scenarios/","notes":"Spec refs: amendments.md §Mock IPC must be extended for parallel frontend dev (line ~74); data-model.md lines 1088-1091 callback list. Existing file to extend: web/src/lib/mock-ipc.ts (100 lines). Use Svelte 5 runes patterns in any new helpers. This unblocks all subsequent Phase 2 issues.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:32Z","created_by":"James Lal","updated_at":"2026-05-19T15:30:09Z","started_at":"2026-05-19T15:07:26Z","closed_at":"2026-05-19T15:30:09Z","close_reason":"Implemented; merged; 409 Rust + 237 relay tests pass; store reassembled from 4.6 + 8.3 merge race","dependencies":[{"issue_id":"attn-nnj.4.1","depends_on_id":"attn-nnj.12.6","type":"blocks","created_at":"2026-05-18T16:53:51Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.1","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:31Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":13,"comment_count":0} -{"id":"attn-nnj.4.2","title":"Frontend review store (Svelte 5 runes)","description":"Create web/src/lib/review/store.ts: a $state-based store holding rooms, threads, snapshots, anchorResolutions, and outbox. Subscribes to window.__attn__.reviewStatus/reviewEvent/reviewSnapshot/reviewAnchorResolution callbacks. Provides $derived selectors for 'comments on current file/snapshot', 'ambiguous anchors', and 'outbox count'. No 'any' types — proper TypeScript throughout.","acceptance_criteria":"- web/src/lib/review/store.ts exists with $state-based store\n- Subscribes to all four window.__attn__ review callbacks\n- Exposes derived selectors: commentsOnCurrent, ambiguousAnchors, outboxCount\n- Fully typed (no 'any'); types align with data-model.md ReviewEvent/Anchor/Snapshot shapes\n- Unit-testable shape (pure functions for selectors where possible)","notes":"Spec refs: data-model.md §Frontend Bridge (lines ~1080-1100) for callback contract; §UI/UX Changes for what the store must surface. Use Svelte 5 runes ($state, $derived, $effect). Follow project rule: no 'any' types. Depends on 2-1 mock-ipc extension for runtime emissions.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:32Z","created_by":"James Lal","updated_at":"2026-05-19T15:30:23Z","started_at":"2026-05-19T15:07:27Z","closed_at":"2026-05-19T15:30:23Z","close_reason":"Implemented; merged; 409 Rust + 237 relay tests pass; store reassembled from 4.6 + 8.3 merge race","dependencies":[{"issue_id":"attn-nnj.4.2","depends_on_id":"attn-nnj.12.10","type":"blocks","created_at":"2026-05-18T16:53:57Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.2","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:32Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.2","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:19Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":7,"comment_count":0} -{"id":"attn-nnj.2.8","title":"ReviewManager scaffold + UserEvent::Review event-loop wiring","description":"Scaffold src/review/manager.rs as a struct holding store + working_copy + rooms map. Wire it into the EXISTING tao event loop in src/main.rs by adding a UserEvent::Review(ReviewUpdate) arm — per amendments.md §Phase 0b, do NOT factor out a new event loop. Forwards ReviewUpdates to the webview via window.__attn__.reviewEvent(...).","acceptance_criteria":"- src/review/manager.rs defines `pub struct ReviewManager { store, working_copy, rooms: HashMap\u003cRoomId, RoomRuntime\u003e }` + new() + a tokio mpsc channel for ReviewUpdate\n- src/main.rs UserEvent enum gains a Review(ReviewUpdate) variant\n- The existing event_loop.run match adds an arm: UserEvent::Review(update) =\u003e { webview.evaluate_script(\u0026format!(\"window.__attn__.reviewEvent({})\", serde_json::to_string(\u0026update)?))?; }\n- Manager constructed during daemon startup; channel sender stashed in AppState (or accessible globally)\n- No real room/document logic yet — just lifecycle: manager starts, channel works, a smoke test sends a stub ReviewUpdate and the webview receives it (verified via --eval window.__attn__.lastReviewEvent)\n- ReviewUpdate is a typed enum (not serde_json::Value) — initial variants can be small but explicit","notes":"Spec: planning/collab/amendments.md §Codebase Corrections (main.rs is 1207 lines, not thin) + §Phase 0b (integrates into EXISTING event loop). data-model.md §Review Manager + §Webview IPC Changes for the JS bridge shape. Critical: do NOT introduce a second event loop or factor out main.rs's. Add to the existing match arms only.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:14Z","created_by":"James Lal","updated_at":"2026-05-19T01:33:11Z","started_at":"2026-05-19T00:47:50Z","closed_at":"2026-05-19T01:33:11Z","close_reason":"Implemented; merged into collab; 212 Rust tests + 98 relay tests pass","dependencies":[{"issue_id":"attn-nnj.2.8","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:30:14Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.8","depends_on_id":"attn-nnj.2.1","type":"blocks","created_at":"2026-05-18T16:30:26Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.8","depends_on_id":"attn-nnj.2.2","type":"blocks","created_at":"2026-05-18T16:30:28Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.8","depends_on_id":"attn-nnj.2.3","type":"blocks","created_at":"2026-05-18T16:30:29Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.8","depends_on_id":"attn-nnj.2.7","type":"blocks","created_at":"2026-05-18T16:30:30Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.8","depends_on_id":"attn-nnj.2.9","type":"blocks","created_at":"2026-05-18T16:54:01Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":5,"dependent_count":2,"comment_count":0} -{"id":"attn-nnj.2.7","title":"AppState refactor: review_rooms HashMap + file_to_room routing","description":"Refactor AppState in src/ipc.rs per amendments.md §Codebase Corrections: AppState becomes routing context holding `review_rooms: HashMap\u003cRoomId, RoomRuntimeHandle\u003e` and `file_to_room: HashMap\u003cPathBuf, RoomId\u003e`. Heavy state lives in ReviewManager — AppState just looks up which room a file belongs to.","acceptance_criteria":"- src/ipc.rs AppState fields: review_rooms: HashMap\u003cRoomId, RoomRuntimeHandle\u003e, file_to_room: HashMap\u003cPathBuf, RoomId\u003e (in addition to existing tab/project state)\n- RoomRuntimeHandle is a lightweight Arc/channel-sender to the manager (NOT the full ReviewRoom struct)\n- All AppState construction sites + call sites updated\n- Lookup helper: AppState::room_for_path(\u0026Path) -\u003e Option\u003cRoomRuntimeHandle\u003e\n- Existing IPC handlers compile and still pass tests\n- No flat list of rooms anywhere — file_to_room is the only path→room oracle","notes":"Spec: planning/collab/amendments.md §Codebase Corrections (Tabs and projects are first-class; the plan's AppState is wrong). This DIVERGES from the original data-model.md AppState design — amendments wins. RoomRuntimeHandle is defined here as a thin handle (clonable, Send+Sync); the real ReviewManager fills in the actual struct in issue 8.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:59Z","created_by":"James Lal","updated_at":"2026-05-19T00:23:59Z","started_at":"2026-05-19T00:04:13Z","closed_at":"2026-05-19T00:23:59Z","close_reason":"Implemented in parallel worktrees; merged into collab; 142 tests pass","dependencies":[{"issue_id":"attn-nnj.2.7","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:29:58Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.7","depends_on_id":"attn-nnj.2.1","type":"blocks","created_at":"2026-05-18T16:30:25Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.5.4","title":"Hashcash PoW verification + replay protection","description":"relay/src/pow.ts: verify the Attn-PoW header on every write endpoint (POST /devices, POST /envelopes, POST /acks, POST /blobs, DELETE). Token is 8 colon-separated fields, v2 format. Validation order per crypto-spec.md §Server Validation. Replay protection via meta:pow_seen:\u003cexpiresAt\u003e:\u003chash\u003e DO storage entries; a scheduled alarm prunes entries past expiresAt+10min. No client type is exempt (decision #6); default difficulty 16 bits, hard floor 12.","acceptance_criteria":"- Parse the 8-field v2 token; reject unknown version\n- Validation in order: parse → v==v2 → difficulty\u003e=max(policy.powBits,12) → expiresAt within 5-min window (not past, not \u003e5min future) → resource matches (roomId, deviceId, base64url(SHA-256(METHOD + space + PATH))[:8]) → leading-zero bits in SHA-256(token bytes) → not in pow_seen replay set\n- On success: insert meta:pow_seen:\u003cexpiresAt\u003e:\u003chash\u003e with TTL\n- Errors map to ATTN_POW_INVALID (parse/format), ATTN_POW_INSUFFICIENT_DIFFICULTY, ATTN_POW_EXPIRED, ATTN_POW_RESOURCE_MISMATCH, ATTN_POW_REPLAYED — all 403\n- Single PoW token per HTTP request — for batch /envelopes the same token covers the whole batch (decision #7)\n- Alarm-driven prune loop removes pow_seen entries with expiresAt+10min \u003c now\n- Unit tests use vectors from crypto-spec.md §Test Vectors","notes":"Spec: planning/collab/crypto-spec.md §Hashcash Proof-of-Work (lines 117-197), §Server Validation (152-165), §Replay Protection (166-169). amendments.md decisions #6 (universal PoW) and #7 (single token per batch). Used by all write endpoints — implement as composable middleware.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:51Z","created_by":"James Lal","updated_at":"2026-05-19T01:16:27Z","started_at":"2026-05-19T00:47:51Z","closed_at":"2026-05-19T01:16:27Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.5.4","depends_on_id":"attn-nnj.1.2","type":"blocks","created_at":"2026-05-18T16:38:23Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.4","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:29:51Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.4","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:24Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":9,"comment_count":0} -{"id":"attn-nnj.5.3","title":"Owner-signature verification","description":"relay/src/owner-sig.ts: verify the Attn-Owner-Signature header (base64url Ed25519 signature over the same canonicalRequest used for admission HMAC: METHOD\\nPATH\\nCANONICAL_QUERY\\nSHA256(body)). Verifies against ownerSigningKey stored at room creation; mismatching key returns 403 ATTN_OWNER_KEY_MISMATCH. Required for DELETE /v2/rooms/:roomId at all times, and required for POST /v2/rooms/:roomId/acks when the request asks for deletion AND policy.deleteEventsAfterOwnerAck==true. Missing header on a path that requires it returns 403 ATTN_OWNER_SIG_REQUIRED.","acceptance_criteria":"- Canonical bytes match the admission canonicalRequest construction (single shared helper)\n- Verify Ed25519 sig with @noble/ed25519 or Web Crypto subtle\n- Required: DELETE /v2/rooms/:roomId (always)\n- Required: POST /acks when delete=true requested AND policy.deleteEventsAfterOwnerAck==true\n- 403 ATTN_OWNER_SIG_REQUIRED when header missing on a required path\n- 403 ATTN_OWNER_KEY_MISMATCH when signature does not verify against the stored ownerSigningKey\n- Stored ownerSigningKeyId is base64url(SHA-256(ownerSigningKey)) — exposed in room policy responses\n- Unit tests cover valid sig, wrong key, tampered body, missing header, non-owner action attempt","notes":"Spec: planning/collab/relay-spec.md §Identity, Keys, and Admission \u003e Owner Distinction (lines 69-74), §POST /v2/rooms/:roomId (114-167), §POST /v2/rooms/:roomId/acks (277-303), §DELETE /v2/rooms/:roomId (305-311). amendments.md decision #3 (owner-only ops). Shares canonicalRequest helper with admission middleware.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:50Z","created_by":"James Lal","updated_at":"2026-05-19T01:16:13Z","started_at":"2026-05-19T00:47:51Z","closed_at":"2026-05-19T01:16:13Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.5.3","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:29:50Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.3","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:23Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} -{"id":"attn-nnj.5.2","title":"Admission HMAC verification module","description":"relay/src/admission.ts: middleware that verifies the Attn-Admission header on every authenticated endpoint. HMAC-SHA256 covers METHOD\\nPATH\\nCANONICAL_QUERY\\nSHA256(body). Trust model is URL-as-bearer (decision #2): the relay derives the admissionKey from material the client supplies at room creation and stores it on the room record; subsequent requests must present a matching HMAC. Wrong HMAC returns 401 with error code ATTN_ADMISSION_INVALID.","acceptance_criteria":"- Canonical string per spec: METHOD\\nPATH\\nCANONICAL_QUERY (sorted, percent-encoded)\\nSHA256(body) hex\n- HMAC constant-time compared; failure returns 401 {error:{code:'ATTN_ADMISSION_INVALID'}}\n- Admission key is loaded from room storage; missing room → 404 ATTN_ROOM_NOT_FOUND (before admission check to avoid timing oracle? — actually per spec admission failure must NOT leak room existence: return 401 for missing-room too)\n- Unit tests cover: wrong HMAC, missing header, body tampering, query reordering, missing room (uniform 401)\n- Exported as a Hono/itty middleware reused by all authenticated routes","notes":"Spec: planning/collab/relay-spec.md §Identity, Keys, and Admission (lines 37-67) and §Wire Conventions (89-99). Decision #2 in amendments.md (URL-as-bearer trust model). This middleware is the gatekeeper for every endpoint except POST /v2/rooms/:roomId (room creation, which establishes the admission key) and GET /health.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:49Z","created_by":"James Lal","updated_at":"2026-05-19T00:40:28Z","started_at":"2026-05-19T00:26:07Z","closed_at":"2026-05-19T00:40:28Z","close_reason":"Implemented in parallel; merged into collab; 166 tests pass + relay 35 tests pass","dependencies":[{"issue_id":"attn-nnj.5.2","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:29:49Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.5.2","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:22Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":9,"comment_count":0} -{"id":"attn-nnj.2.6","title":"Watcher self-write distinction with TTL-tracked hashes","description":"Teach src/watcher.rs to distinguish between our own writes (via WorkingCopyService) and external file changes (editor, git checkout). Tracks recent self-writes by (path, ContentHash) with a short TTL; matching events attach to the existing LocalRevision, others emit a new ExternalFileChange revision.","acceptance_criteria":"- src/watcher.rs maintains a HashMap\u003c(PathBuf, ContentHash), Instant\u003e of recent self-writes\n- WorkingCopyService::save inserts into this map immediately after a successful write\n- TTL configurable, default 5s; expired entries pruned on access\n- On notify event: compute new ContentHash; if (path, hash) hit → attach to existing LocalRevision (no new journal entry); else emit a new LocalRevision{source: ExternalFileChange} via the store + existing reload signal\n- Existing reload-the-webview behavior is preserved end-to-end\n- Unit tests cover: self-write skipped, external write journaled, TTL expiry causes external-classification, repeated identical external content still journaled once","notes":"Spec: planning/collab/data-model.md §File Watcher Integration. The watcher already debounces; reuse that. Don't journal from inside the watcher directly — call store.append_revision via a channel to the ReviewManager so single-writer ordering holds.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:46Z","created_by":"James Lal","updated_at":"2026-05-19T01:33:24Z","started_at":"2026-05-19T01:17:14Z","closed_at":"2026-05-19T01:33:24Z","close_reason":"Implemented; merged into collab; 212 Rust tests + 98 relay tests pass","dependencies":[{"issue_id":"attn-nnj.2.6","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:29:45Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.6","depends_on_id":"attn-nnj.2.1","type":"blocks","created_at":"2026-05-18T16:30:25Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.6","depends_on_id":"attn-nnj.2.5","type":"blocks","created_at":"2026-05-18T16:30:30Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.2.5","title":"Revision journal appended on every WorkingCopyService save","description":"On every WorkingCopyService::save, append a LocalRevision to ~/.attn/reviews/rooms/\u003croomId\u003e/revisions/\u003cfileId\u003e.jsonl. Captures parentHash → nextHash + optional pmSteps/patchText so future anchor code can replay the document history.","acceptance_criteria":"- WorkingCopyService::save appends a LocalRevision JSONL line per call\n- LocalRevision fields: revisionId, fileId, parentHash, nextHash, source (SaveSource), timestamp, optional pmSteps, optional patchText\n- Source variants wired: UserEdit, CheckboxToggle, ExternalFileChange, SnapshotLoaded, ManualReanchor (AcceptedSuggestion stub is fine — Phase 5 wires it)\n- File path: ~/.attn/reviews/rooms/\u003croomId\u003e/revisions/\u003cfileId\u003e.jsonl\n- Append is atomic per line (single write syscall + fsync)\n- Unit tests cover: single save → single revision, sequential saves produce parentHash chain, replay reads back identical sequence","notes":"Spec: planning/collab/data-model.md §Local Replicas (LocalRevision struct) + §Working Copy Service. pmSteps/patchText are optional now — Phase 1 anchor engine fills them in. RoomId routing comes from AppState (Phase 0b issue 7); for save calls outside any room, use a sentinel \"orphan\" or skip journaling.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:33Z","created_by":"James Lal","updated_at":"2026-05-19T01:15:47Z","started_at":"2026-05-19T00:47:49Z","closed_at":"2026-05-19T01:15:47Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.2.5","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:29:33Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.5","depends_on_id":"attn-nnj.2.1","type":"blocks","created_at":"2026-05-18T16:30:24Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.5","depends_on_id":"attn-nnj.2.2","type":"blocks","created_at":"2026-05-18T16:30:27Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.5","depends_on_id":"attn-nnj.2.4","type":"blocks","created_at":"2026-05-18T16:30:29Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.8.5","title":"Emit SuggestionAccepted and SuggestionRejected","description":"After a successful apply (clean or three-way accepted), ReviewManager constructs a SuggestionAccepted ReviewEvent referencing the LocalRevision id and the SaveResult.resultingHash, signs+encrypts it, and enqueues it on the outbox for relay/DataChannel delivery. The reject path emits SuggestionRejected with an optional reason. Both flow through the same outbox machinery as comment events.","acceptance_criteria":"- src/review/manager.rs on apply success builds SuggestionAccepted { suggestionId, appliedRevisionId, resultingHash } matching data-model.md §Suggestion Events.\n- On reject (owner picked 'Keep current'), builds SuggestionRejected { suggestionId, reason } where reason is optional and may come from the UI.\n- Events are signed with the owner's signing key, encrypted under eventKey, and appended to outbox.jsonl exactly like comment events (single code path).\n- meta.parentEventIds includes the original SuggestionCreated event id so receivers can reconstruct the thread.\n- Unit test: simulate full apply path, assert one SuggestionAccepted envelope sits in outbox with the expected fields; simulate reject path, assert one SuggestionRejected envelope.","notes":"Spec: planning/collab/data-model.md §Suggestion Events (lines 628-640). Files: src/review/manager.rs, src/review/outbox.rs. Reuse the existing outbox enqueue path — do not introduce a parallel writer. Outbox mutability rule from amendments.md §Outbox mutability and freezing applies (these events are immutable once first-send-attempted).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:25Z","created_by":"James Lal","updated_at":"2026-05-19T15:06:00Z","started_at":"2026-05-19T04:31:05Z","closed_at":"2026-05-19T15:06:00Z","close_reason":"Implemented; merged; 412 Rust + 213 relay tests pass (6 conformance scenarios deferred to 5.16)","dependencies":[{"issue_id":"attn-nnj.8.5","depends_on_id":"attn-nnj.8","type":"parent-child","created_at":"2026-05-18T16:29:24Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.8.5","depends_on_id":"attn-nnj.8.4","type":"blocks","created_at":"2026-05-18T16:29:53Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.8.4","title":"Apply through WorkingCopyService","description":"When the owner accepts a suggestion (clean apply or three-way 'accept'/'accept_edited'), the resulting bytes must be written to the file via WorkingCopyService::save with SaveSource::AcceptedSuggestion { room_id, suggestion_id }. This records a LocalRevision in the journal so the watcher distinguishes the apply from an external edit and so the SaveResult.resultingHash can feed the SuggestionAccepted event.","acceptance_criteria":"- src/review/apply.rs apply_accepted(verdict_or_edited_bytes, source: SaveSource::AcceptedSuggestion { room_id, suggestion_id }) calls WorkingCopyService::save and returns a SaveResult with resultingHash.\n- The LocalRevision entry created has source=AcceptedSuggestion and references both room_id and suggestion_id so the revision journal can be queried by room.\n- File watcher sees its own write and suppresses the reload bounce (existing self-write distinction in src/watcher.rs).\n- After write, the editor's in-memory document updates without losing the user's cursor (PM transaction rather than a full reload where possible).\n- Integration test in src/review/apply.rs writes a fixture file, applies a suggestion, asserts both file contents and a new LocalRevision entry.","notes":"Spec: planning/collab/data-model.md §Suggestion Events apply flow steps 5-6 (lines 648-649) + amendments.md §watcher.rs does more than reload (line ~21). Files: src/review/apply.rs, src/working_copy.rs (Phase 0b). WorkingCopyService::save is the only write path — never std::fs::write directly.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:22Z","created_by":"James Lal","updated_at":"2026-05-19T04:28:54Z","started_at":"2026-05-19T04:09:05Z","closed_at":"2026-05-19T04:28:54Z","close_reason":"Round 13: implemented; merged; all tests pass","dependencies":[{"issue_id":"attn-nnj.8.4","depends_on_id":"attn-nnj.2.4","type":"blocks","created_at":"2026-05-18T16:38:30Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.8.4","depends_on_id":"attn-nnj.8","type":"parent-child","created_at":"2026-05-18T16:29:21Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.8.4","depends_on_id":"attn-nnj.8.1","type":"blocks","created_at":"2026-05-18T16:29:52Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.5.1","title":"Relay package scaffold + Wrangler config","description":"Create the relay/ workspace package that hosts the Cloudflare Worker: package.json, tsconfig.json, wrangler.toml, src/index.ts (router stub), src/room-do.ts (Durable Object stub), src/schema.ts (zod request/response validators), and test/ folder layout. wrangler dev --local must boot Miniflare with the RELAY_ROOMS Durable Object binding and RELAY_BLOBS R2 bucket binding so subsequent issues can integration-test against it.","acceptance_criteria":"- relay/ directory exists with package.json, tsconfig.json, wrangler.toml, src/{index,room-do,schema}.ts, test/{integration,conformance}/ scaffolding\n- wrangler.toml: compatibility_date=2026-01-01, RELAY_ROOMS Durable Object class binding, RELAY_BLOBS R2 binding, HARD_MAX_ROOM_BYTES / HARD_MAX_EVENT_BYTES / HARD_MAX_SNAPSHOT_BYTES env vars set per spec\n- zod schemas typecheck against the request/response shapes in relay-spec.md\n- 'wrangler dev --local' boots Miniflare cleanly with the DO and R2 stub mounted; GET /health returns 200\n- npm test wires through to a vitest runner pointed at test/","notes":"Spec: planning/collab/relay-spec.md §Deployment (lines 614-685) for wrangler.toml sketch and repo layout. §Caps (Server Hard Maxima) for HARD_MAX_* values. This issue blocks every other 3a issue — keep it strictly to scaffolding (no business logic). Repo currently has no relay/ folder.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:21Z","created_by":"James Lal","updated_at":"2026-05-18T23:57:41Z","started_at":"2026-05-18T23:49:36Z","closed_at":"2026-05-18T23:57:41Z","close_reason":"Implemented; merged into collab","dependencies":[{"issue_id":"attn-nnj.5.1","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:29:21Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":15,"comment_count":0} -{"id":"attn-nnj.2.4","title":"WorkingCopyService replacing direct fs::write","description":"Introduce src/review/working_copy.rs as the single chokepoint for every markdown-file save. Replaces direct std::fs::write calls in src/ipc.rs (EditSave + checkbox toggle). Hashes content on every save so downstream revision/anchor code has a stable identity.","acceptance_criteria":"- src/review/working_copy.rs exposes `save(req: SaveRequest) -\u003e Result\u003cSaveResult\u003e`\n- SaveRequest { path: PathBuf, content: String, expected_hash: Option\u003cContentHash\u003e, source: SaveSource }\n- SaveResult { previous_hash: ContentHash, next_hash: ContentHash, revision_id: RevisionId }\n- SaveSource enum: UserEdit, CheckboxToggle, AcceptedSuggestion, ExternalFileChange, SnapshotLoaded, ManualReanchor\n- ContentHash computed per crypto-spec.md §ContentHash (canonical UTF-8, no BOM, LF line endings, preserve trailing-newline as authored)\n- expected_hash mismatch → returns ConflictError without writing\n- src/ipc.rs EditSave and checkbox-toggle paths use WorkingCopyService::save instead of std::fs::write\n- Unit tests cover: happy save, expected_hash mismatch, hash determinism across line-ending normalization","notes":"Spec: planning/collab/data-model.md §Working Copy Service + crypto-spec.md §ContentHash. The revision_id returned here gets persisted by the revision-journal issue. Keep this synchronous for now (single-writer).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:20Z","created_by":"James Lal","updated_at":"2026-05-19T00:39:55Z","started_at":"2026-05-19T00:26:06Z","closed_at":"2026-05-19T00:39:55Z","close_reason":"Implemented in parallel; merged into collab; 166 tests pass + relay 35 tests pass","dependencies":[{"issue_id":"attn-nnj.2.4","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:29:20Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.4","depends_on_id":"attn-nnj.2.1","type":"blocks","created_at":"2026-05-18T16:30:23Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.4","depends_on_id":"attn-nnj.2.2","type":"blocks","created_at":"2026-05-18T16:30:27Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} -{"id":"attn-nnj.8.3","title":"Three-way apply UI dialog (Svelte)","description":"Svelte 5 component that surfaces ApplyVerdict::RequiresThreeWay to the owner. Shows the snapshot text (what the suggester saw), the current owner text (what's there now), and the proposed replacement, side-by-side. Owner picks: accept proposed, keep current, or edit manually. The component returns the owner's choice via IPC back to ReviewManager.","acceptance_criteria":"- web/src/lib/ReviewApplyDialog.svelte renders three panes (snapshot | current | proposed) with monospace diff styling.\n- Props: { suggestionId, snapshotText, currentText, proposedText, anchorContext }.\n- Actions: 'Accept proposed' (returns 'accept'), 'Keep current' (returns 'reject'), 'Edit manually' (opens an inline editor with the proposed text as the starting buffer, returns 'accept_edited' with the edited string).\n- Built with Svelte 5 runes (, , ); no Svelte 4 patterns. No window.confirm / alert per project conventions — fully in-app UI.\n- Result flows to ReviewManager via window.__attn__.reviewSubmitApplyChoice(suggestionId, choice, editedText?).\n- Storybook-style demo route or mock-ipc fixture so the dialog renders standalone.","notes":"Spec: planning/collab/data-model.md §Suggestion Events apply flow step 3 (line 646). Follow svelte5-best-practices skill conventions. Files: web/src/lib/ReviewApplyDialog.svelte. Existing component patterns: look at the review panel pieces from Phase 2 work. Project rule: no window.confirm/alert — use proper in-app UI.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:06Z","created_by":"James Lal","updated_at":"2026-05-19T15:29:54Z","started_at":"2026-05-19T15:07:28Z","closed_at":"2026-05-19T15:29:54Z","close_reason":"Implemented; merged; 409 Rust + 237 relay tests pass; store reassembled from 4.6 + 8.3 merge race","dependencies":[{"issue_id":"attn-nnj.8.3","depends_on_id":"attn-nnj.10.4","type":"blocks","created_at":"2026-05-18T16:38:30Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.8.3","depends_on_id":"attn-nnj.8","type":"parent-child","created_at":"2026-05-18T16:29:05Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.8.3","depends_on_id":"attn-nnj.8.1","type":"blocks","created_at":"2026-05-18T16:29:52Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.2.3","title":"Local JSON/JSONL store at ~/.attn/reviews/","description":"Implement src/review/store.rs as the on-disk persistence layer for rooms, events, snapshots, outbox, and revisions. Atomic writes via temp-file+rename for JSON; append-only JSONL for event/outbox/revision logs. Idempotent on EventId and EnvelopeId so repeated imports are safe.","acceptance_criteria":"- Directory layout matches data-model.md §Local Review Store exactly (rooms/\u003croomId\u003e/{room.json, devices/, snapshots/, events.jsonl, outbox.jsonl, revisions/\u003cfileId\u003e.jsonl, replicas/\u003creplicaId\u003e/...})\n- All JSON writes are atomic: write to .tmp then rename\n- JSONL writes are append-only and fsync'd per append\n- import_event(EventId, ...) is idempotent — duplicate EventId is a no-op\n- enqueue_outbox(EnvelopeId, ...) is idempotent on EnvelopeId\n- Every top-level JSON file includes a `schemaVersion` field\n- Unit tests cover: fresh-init, repeat-import, partial-write recovery, concurrent appender safety (single writer)","notes":"Spec: planning/collab/data-model.md §Local Replicas + §Local Review Store layout (search §Local Review Store in data-model.md). Use std::fs + serde_json. No async yet; called from a single ReviewManager task. ~/.attn/ already exists for the daemon socket — extend with reviews/ subtree.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:01Z","created_by":"James Lal","updated_at":"2026-05-19T00:00:28Z","started_at":"2026-05-18T23:49:34Z","closed_at":"2026-05-19T00:00:28Z","close_reason":"Implemented; merged into collab; 9 store tests pass","dependencies":[{"issue_id":"attn-nnj.2.3","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:29:01Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.3","depends_on_id":"attn-nnj.2.1","type":"blocks","created_at":"2026-05-18T16:30:23Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.3","depends_on_id":"attn-nnj.2.2","type":"blocks","created_at":"2026-05-18T16:30:26Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.8.2","title":"Expected-text verification","description":"Tight unit tests around SuggestionOperation.expectedText vs current owner text. A false positive (verifying equal when the bytes differ) corrupts the user's file silently. expectedText was captured against the snapshot's exact bytes, so verification must be byte-identical with no Unicode normalization, no whitespace folding, and explicit line-ending handling.","acceptance_criteria":"- src/review/apply.rs has a private verify_expected_text(current: \u0026str, expected: \u0026str) -\u003e bool helper used by the suggestion resolver.\n- Unit tests cover: identical bytes -\u003e true; trailing-whitespace diff -\u003e false; CRLF vs LF diff -\u003e false (ContentHash normalizes to LF on write, but expectedText was captured exactly as the snapshot bytes — they must match exactly); NFC vs NFD unicode -\u003e false (no normalization); BOM present in one only -\u003e false; empty-string vs empty-string -\u003e true.\n- Property-style test: for random inputs s, verify_expected_text(s, s) == true and verify_expected_text(s, s + 'x') == false.\n- Documented in a module-level comment that this function intentionally does no normalization.","notes":"Spec: planning/collab/data-model.md §Suggestion Events apply flow step 2 (line 645). Critical correctness path — please err on the side of more tests. See also crypto-spec.md ContentHash normalization (writes LF; expectedText captured from snapshot bytes pre-normalization).","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:57Z","created_by":"James Lal","updated_at":"2026-05-19T04:08:29Z","started_at":"2026-05-19T03:42:01Z","closed_at":"2026-05-19T04:08:29Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.8.2","depends_on_id":"attn-nnj.8","type":"parent-child","created_at":"2026-05-18T16:28:57Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.8.2","depends_on_id":"attn-nnj.8.1","type":"blocks","created_at":"2026-05-18T16:29:51Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.8.1","title":"Suggestion resolver (Rust)","description":"Given a SuggestionCreated event and the current owner DocumentReplica, resolve the suggestion's Anchor through the Phase 1 anchor engine, then evaluate the SuggestionOperation. Produces an ApplyVerdict that drives the apply UI: clean apply, three-way merge, ambiguous picker, or stale. This is the core decision point of the Phase 5 apply flow.","acceptance_criteria":"- src/review/apply.rs exposes resolve_suggestion(event: \u0026SuggestionCreated, replica: \u0026DocumentReplica) -\u003e ApplyVerdict.\n- ApplyVerdict variants: Ready { range: PositionAnchor, replacement: String, op_kind }, RequiresThreeWay { range, snapshot_text, current_text, replacement }, Stale { reason }, Ambiguous { candidates }.\n- For SuggestionOperation::Replace/Delete: anchor resolves with status in {exact, remapped} AND current text at the resolved range equals expectedText -\u003e Ready; if anchor resolves but current text differs -\u003e RequiresThreeWay; if anchor is ambiguous -\u003e Ambiguous; if stale -\u003e Stale.\n- For SuggestionOperation::InsertBefore/InsertAfter: anchor must be unambiguous (exact or remapped); ambiguous/stale flow to Ambiguous/Stale.\n- Unit tests cover each verdict path with realistic fixtures.","notes":"Spec: planning/collab/data-model.md §Suggestion Events apply flow (lines 642-650). Files: src/review/apply.rs. Depends on the Phase 1 Rust resolver (attn-nnj.3.4) — added as cross-phase dep by parent agent. Pure function, no I/O, no UI — UI lives in the three-way dialog issue.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:49Z","created_by":"James Lal","updated_at":"2026-05-19T04:06:42Z","started_at":"2026-05-19T03:23:02Z","closed_at":"2026-05-19T04:06:42Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.8.1","depends_on_id":"attn-nnj.3.4","type":"blocks","created_at":"2026-05-18T16:38:29Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.8.1","depends_on_id":"attn-nnj.8","type":"parent-child","created_at":"2026-05-18T16:28:49Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} -{"id":"attn-nnj.2.2","title":"Serde model types for review domain","description":"Define all serde structs in src/review/model.rs covering rooms, participants, devices, documents, snapshots, replicas, revisions, events, envelopes, and sync cursors. JSON field names match data-model.md camelCase exactly so any future browser/CLI can read the same files.","acceptance_criteria":"- src/review/model.rs defines: ReviewRoom, RoomPolicy, Participant, Device, SharedDocument, SnapshotNode, BlobRef, DocumentReplica, ReplicaRelation, LocalRevision, ReviewEvent (with EventMeta + Body + Auth submodels), MailboxEnvelope, SyncCursor, DeliveryAck\n- ReviewEventBody is a tagged enum with variants: RoomCreated, ParticipantJoined, SnapshotCreated, SnapshotSuperseded, CommentCreated, CommentResolved, SuggestionCreated, SuggestionAccepted, SuggestionRejected, AnchorManuallyResolved, PresenceUpdated, SessionEnded\n- All structs use #[serde(rename_all = \"camelCase\")] (or per-field rename) so JSON matches data-model.md\n- Roundtrip test: every variant serializes → deserializes byte-identical\n- Zero use of `any` / `serde_json::Value` except where the spec explicitly says opaque payload","notes":"Spec: planning/collab/data-model.md §Terms, §Review Events (all subsections), §Encrypted Envelopes, §Sync Cursors And ACKs. Use #[serde(tag = \"kind\")] for the event body enum. Use the typed ID newtypes from Phase 0a issue 8 (RoomId, FileId, EventId, etc.) — they're already serde-transparent.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:48Z","created_by":"James Lal","updated_at":"2026-05-18T23:45:35Z","started_at":"2026-05-18T23:31:44Z","closed_at":"2026-05-18T23:45:35Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.2.2","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:28:48Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.2.2","depends_on_id":"attn-nnj.2.1","type":"blocks","created_at":"2026-05-18T16:30:22Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":6,"comment_count":0} -{"id":"attn-nnj.10.3","title":"Design: connection + share affordances","description":"Decide where the share button, connection badge, and peer strip live in the toolbar/header. Toolbar real estate is already contended by theme toggle, edit toggle, and command palette. Determine owner-only vs reviewer-only affordances and overflow behavior. Output planning/collab/ui/connection-share.md with proposed layout.","acceptance_criteria":"- planning/collab/ui/connection-share.md exists with annotated layout\n- Resolves placement of: share button, connection badge (Live direct / Mailbox / Offline / Direct failed), peer strip\n- Distinguishes owner-only vs reviewer-only affordances\n- Notes interaction with existing toolbar (theme toggle, edit toggle, command palette)\n- Flagged for human review before share/connection coding begins","notes":"Spec refs: data-model.md §UI/UX Changes (owner: share + room mode + connection + peer strip; reviewer: outbox + owner-offline state). Existing toolbar: search web/src/lib/ for theme toggle and edit toggle to inventory current real estate. Output path: planning/collab/ui/connection-share.md. Blocks Phase 2 share, connection-badge, peer-strip issues.","status":"closed","priority":1,"issue_type":"decision","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:45Z","created_by":"James Lal","updated_at":"2026-05-19T17:58:58Z","started_at":"2026-05-18T23:58:12Z","closed_at":"2026-05-19T17:58:58Z","close_reason":"Design docs landed in planning/collab/ui/ for ongoing reference","labels":["human"],"dependencies":[{"issue_id":"attn-nnj.10.3","depends_on_id":"attn-nnj.10","type":"parent-child","created_at":"2026-05-18T16:28:44Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":9,"comment_count":0} -{"id":"attn-nnj.10.2","title":"Design: inline decoration system in ProseMirror","description":"Design how the four anchor states render in the editor surface: exact (\u003e=0.90), remapped+moved (0.70-0.89), ambiguous (panel-only), and stale (panel-only). Decide between underline / highlight / margin marker treatments, hover affordance, and click-to-focus-panel interaction. Output planning/collab/ui/inline-decorations.md with concrete CSS and PM Decoration sketches.","acceptance_criteria":"- planning/collab/ui/inline-decorations.md exists with concrete CSS + PM Decoration sketches per state\n- Confidence cutoffs from amendments.md Decision #15 quoted verbatim\n- Hover and click-to-focus behaviors specified\n- Overlap handling addressed (multiple decorations covering same range)\n- Flagged for human review before Phase 2 decoration plugin coding","notes":"Spec refs: amendments.md Decision #15 (UI cutoffs: \u003e=0.90 inline no badge, 0.70-0.89 inline + 'moved' badge, ambiguous panel-only, stale panel-only). Existing PM plugins: web/src/lib/prosemirror/{math,tables,code-highlight,code-block-nodeview,mermaid-nodeview}.ts — mirror their decoration plugin pattern. Output path: planning/collab/ui/inline-decorations.md. Blocks Phase 2 decoration plugin issue.","status":"closed","priority":1,"issue_type":"decision","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:44Z","created_by":"James Lal","updated_at":"2026-05-19T17:58:44Z","started_at":"2026-05-18T23:49:36Z","closed_at":"2026-05-19T17:58:44Z","close_reason":"Design docs landed in planning/collab/ui/ for ongoing reference","labels":["human"],"dependencies":[{"issue_id":"attn-nnj.10.2","depends_on_id":"attn-nnj.10","type":"parent-child","created_at":"2026-05-18T16:28:44Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":7,"comment_count":0} -{"id":"attn-nnj.10.1","title":"Design: review margin layout \u0026 sticky-card model (was: panel layout)","description":"REWRITTEN after user pivot from panel-river to Google-Docs-style margin sticky cards. The original panel-river exploration is kept as context (3 candidates explored) but the recommendation now: margin overlay with cards vertically anchored, orphan tray for ambiguous/stale, decorations from 10.2 click-to-focus the margin card. Update the doc at planning/collab/ui/review-panel-design.md in place.","acceptance_criteria":"- planning/collab/ui/review-panel-design.md exists with 2-3 ASCII mockups\n- Recommendation called out explicitly with rationale\n- Covers: grouping (file/snapshot/thread), resolved collapse, density at 30 comments, picker shape for ambiguous\n- Cross-references data-model.md §UI/UX Changes\n- Flagged for human review before Phase 2 panel coding begins","notes":"Spec refs: planning/collab/data-model.md §UI/UX Changes (lines ~776+); amendments.md Decision #15 cutoffs. Relevant existing files: web/src/lib/Sidebar.svelte (rail patterns), web/src/lib/CommandPalette.svelte (overlay patterns). Output path: planning/collab/ui/review-panel-design.md. This blocks Phase 2 panel/ambiguous/stale/decoration issues.","status":"closed","priority":1,"issue_type":"decision","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:43Z","created_by":"James Lal","updated_at":"2026-05-19T01:15:34Z","started_at":"2026-05-18T23:21:24Z","closed_at":"2026-05-19T01:15:34Z","close_reason":"Implemented in parallel worktrees; merged into collab","labels":["human"],"dependencies":[{"issue_id":"attn-nnj.10.1","depends_on_id":"attn-nnj.10","type":"parent-child","created_at":"2026-05-18T16:28:43Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":7,"comment_count":0} -{"id":"attn-nnj.3.8","title":"window.__attn__.reviewAnchorResolution IPC callback","description":"Wire ResolvedAnchor updates from the Rust ReviewManager to the frontend whenever a snapshot lands or the owner's document changes. The frontend store subscribes and re-runs the TS resolver mirror to produce inline decorations. This is the live channel that keeps comment highlights stable across edits.","acceptance_criteria":"- ReviewManager invokes evaluate_script(window, 'window.__attn__.reviewAnchorResolution(...)') with the latest ResolvedAnchor batch on: (a) new SnapshotCreated arriving, (b) new comment/suggestion event arriving, (c) owner save / WorkingCopy revision producing a new currentHash.\n- The IPC payload is JSON: { fileId, roomId, resolutions: [{ eventId, resolved: ResolvedAnchor }] }.\n- web/src/lib/review/store.ts (or equivalent) exposes a reactive store the Editor.svelte review extension subscribes to.\n- The TS resolver mirror runs locally on every PM transaction to update decorations between Rust pushes (no flicker).\n- Extended mock-ipc.ts emits the same callback shape so frontend dev works without a running daemon (per amendments.md §Mock IPC must be extended).","notes":"Spec: planning/collab/data-model.md §Anchor Resolution + planning/collab/amendments.md §Mock IPC must be extended (line ~76). Files: src/review/manager.rs, src/ipc.rs, web/src/lib/review/store.ts, web/src/lib/mock-ipc.ts. Rust side reuses the existing evaluate_script pattern used by other __attn__ callbacks. Frontend should debounce its own re-resolves so a burst of PM transactions doesn't thrash decorations.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:34Z","created_by":"James Lal","updated_at":"2026-05-19T02:30:01Z","started_at":"2026-05-19T01:57:29Z","closed_at":"2026-05-19T02:30:01Z","close_reason":"Implemented (3.8+round-10 retries); merged into collab; 267 Rust + 144 relay tests pass","dependencies":[{"issue_id":"attn-nnj.3.8","depends_on_id":"attn-nnj.2.8","type":"blocks","created_at":"2026-05-18T16:38:21Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.3.8","depends_on_id":"attn-nnj.3","type":"parent-child","created_at":"2026-05-18T16:28:33Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.3.8","depends_on_id":"attn-nnj.3.1","type":"blocks","created_at":"2026-05-18T16:29:46Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.2.1","title":"Crate layout: src/review/ module skeleton","description":"Create the src/review/ module tree as empty stubs with module-level docs taken from data-model.md §New Rust Modules. Wires `mod review;` into src/main.rs so subsequent issues have a place to land code.","acceptance_criteria":"- src/review/{mod.rs, ids.rs, model.rs, store.rs, working_copy.rs, manager.rs, transport.rs, apply.rs, ipc.rs} exist\n- Each file has a module-level //! doc comment summarizing its responsibility (verbatim from data-model.md §New Rust Modules where applicable)\n- src/main.rs declares `mod review;` and compiles\n- `cargo check` passes; no warnings about unused modules (use #[allow(dead_code)] on stubs)\n- No business logic yet — pure scaffolding","notes":"Spec: planning/collab/data-model.md §Rust Architecture Changes §New Rust Modules. Keep mod.rs as just `pub mod ...;` re-exports. The crypto/ subdir lives separately (owned by Phase 0a).","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:33Z","created_by":"James Lal","updated_at":"2026-05-18T23:29:27Z","started_at":"2026-05-18T23:21:23Z","closed_at":"2026-05-18T23:29:27Z","close_reason":"Implemented via parallel worktree agents; merged into collab","dependencies":[{"issue_id":"attn-nnj.2.1","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:28:32Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":8,"comment_count":0} -{"id":"attn-nnj.3.6","title":"Markdown-edit anchor test corpus","description":"Hand-curated test corpus that pins resolver behavior across the realistic markdown edit shapes a reviewer will encounter. The same corpus drives both the Rust and TS resolvers (same Anchor input + same AnchorIndex inputs -\u003e same ResolvedAnchor output). Catching disagreement here is the only way to prove the two impls stay in lockstep.","acceptance_criteria":"- planning/collab/test-vectors/anchor-cases/ contains ~50 numbered case directories.\n- Each case has original.md, edited.md, anchor.json (the Anchor produced from a selection in original.md), and expected.json (the expected ResolvedAnchor verdict against edited.md).\n- Coverage includes: exact byte match, paragraph reordered, heading renamed, list item inserted before, code-block reflow (whitespace inside fence), quote unchanged but block split into two, ambiguous duplicate paragraphs, fully deleted (stale), math/mermaid round-trip, structure-only block-level anchor, fuzzy quote with one-word change, line-proximity-only fallback.\n- A test runner in src/review/anchors/tests.rs and a vitest spec in web/src/lib/review/resolver.test.ts iterate the corpus and assert the Rust + TS resolvers each produce the expected verdict (status + reason + currentRange).\n- README.md in anchor-cases/ documents the corpus contract.","notes":"Spec: planning/collab/data-model.md §Anchor Resolution + planning/collab/amendments.md Decision #15. Build the corpus as JSON-on-disk so both languages consume it without a code-gen step. The math/mermaid cases require the index builder's Decision #16 work to land first. Cases that depend on local pmSteps mapping should include a stepsJournal.json (omit otherwise).","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:21Z","created_by":"James Lal","updated_at":"2026-05-19T01:56:01Z","started_at":"2026-05-19T01:36:53Z","closed_at":"2026-05-19T01:56:01Z","close_reason":"Implemented; merged into collab; 254 Rust tests + 130 relay tests pass; corpus replay green","dependencies":[{"issue_id":"attn-nnj.3.6","depends_on_id":"attn-nnj.3","type":"parent-child","created_at":"2026-05-18T16:28:21Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.3.6","depends_on_id":"attn-nnj.3.4","type":"blocks","created_at":"2026-05-18T16:29:47Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.3.6","depends_on_id":"attn-nnj.3.5","type":"blocks","created_at":"2026-05-18T16:29:47Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.3.5","title":"Anchor resolver — TS mirror impl for inline decorations","description":"Mirror the Rust resolver in TypeScript so the frontend can drive ProseMirror decorations as the user edits, without round-tripping through Rust on every keystroke. Operates purely on plaintext data — the frontend only ever holds decrypted ReviewEvents (Rust does decrypt + verify). Must produce identical ResolvedAnchor verdicts as the Rust resolver for every case in the test corpus.","acceptance_criteria":"- web/src/lib/review/resolver.ts exports resolve(anchor: Anchor, currentIndex: AnchorIndex, pmState: EditorState, localSteps?: Step[]) -\u003e ResolvedAnchor.\n- All eight resolution steps implemented matching the Rust algorithm; combine + dedup logic identical.\n- Same confidence weights and verdict cutoffs as the Rust impl (shared constants exported so both call sites agree).\n- For every case in the markdown-edit test corpus, the TS verdict matches the Rust verdict exactly (status, reason, currentRange).\n- Vitest unit tests cover the same happy paths and ambiguous-threshold edge cases as the Rust tests.","notes":"Spec: planning/collab/data-model.md §Anchor Resolution + planning/collab/amendments.md Decision #15. Drive inline ProseMirror decorations only — apply / verification stay in Rust. Files: web/src/lib/review/resolver.ts. PM step mapping uses the existing prosemirror-transform Step.map API; pmRange is derived locally and not persisted. Confidence weight constants should mirror the Rust constants (consider generating them from a shared JSON in test-vectors/ to prevent drift).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:00Z","created_by":"James Lal","updated_at":"2026-05-19T01:55:48Z","started_at":"2026-05-19T01:36:52Z","closed_at":"2026-05-19T01:55:48Z","close_reason":"Implemented; merged into collab; 254 Rust tests + 130 relay tests pass; corpus replay green","dependencies":[{"issue_id":"attn-nnj.3.5","depends_on_id":"attn-nnj.3","type":"parent-child","created_at":"2026-05-18T16:27:59Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.3.5","depends_on_id":"attn-nnj.3.1","type":"blocks","created_at":"2026-05-18T16:29:44Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} -{"id":"attn-nnj.1.9","title":"Envelope assemble/disassemble end-to-end integration test","description":"Integration test that exercises the full crypto stack: build ReviewEvent → canonicalize → sign → AEAD-encrypt → wrap in MailboxEnvelope JSON → parse → decrypt → verify signature → recover original ReviewEvent. Locks the contract between issues 3-8.","acceptance_criteria":"- tests/review_crypto_envelope.rs (or src/review/crypto/tests.rs) contains the full roundtrip\n- Test uses planning/collab/test-vectors/envelope.json as both input and expected output\n- Asserts: re-serialized envelope is byte-identical to fixture; decrypted body matches original ReviewEvent; signature verifies; signingKeyId matches\n- Tamper tests: flipping any byte in ciphertext/AAD/signature → explicit error (not silent corruption)\n- Test runs in `cargo test` without network or filesystem deps","notes":"Spec: planning/collab/crypto-spec.md §What Is Signed vs. Encrypted + §Envelope Encryption. This is the canary that integrates HKDF + AEAD + Ed25519 + canonical-JSON + IDs. If any of those change subtly, this test breaks. Keep fixtures in test-vectors/envelope.json (shared with future browser impl).","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:52Z","created_by":"James Lal","updated_at":"2026-05-19T00:39:39Z","started_at":"2026-05-19T00:26:05Z","closed_at":"2026-05-19T00:39:39Z","close_reason":"Implemented in parallel; merged into collab; 166 tests pass + relay 35 tests pass","dependencies":[{"issue_id":"attn-nnj.1.9","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:27:52Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.9","depends_on_id":"attn-nnj.1.1","type":"blocks","created_at":"2026-05-18T16:28:10Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.9","depends_on_id":"attn-nnj.1.2","type":"blocks","created_at":"2026-05-18T16:28:14Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.9","depends_on_id":"attn-nnj.1.5","type":"blocks","created_at":"2026-05-18T16:28:18Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.9","depends_on_id":"attn-nnj.1.6","type":"blocks","created_at":"2026-05-18T16:28:19Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.9","depends_on_id":"attn-nnj.1.8","type":"blocks","created_at":"2026-05-18T16:28:19Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":5,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.3.4","title":"Anchor resolver — run-all-and-combine policy (Rust)","description":"Canonical Rust anchor resolver per Decision #15. Given an Anchor (from a decrypted ReviewEvent), the current AnchorIndex, and an optional local pmSteps journal, run every applicable resolution step, dedup candidates by currentRange, and emit a ResolvedAnchor. This is the authoritative verdict used by the apply flow (Phase 5) and surfaced to the frontend via reviewAnchorResolution IPC.","acceptance_criteria":"- src/review/anchors/resolve.rs exposes resolve(anchor: \u0026Anchor, current_index: \u0026AnchorIndex, local_steps: Option\u003c\u0026PmStepJournal\u003e) -\u003e ResolvedAnchor matching data-model.md §Anchor Resolution.\n- All eight steps run: (1) base_hash match -\u003e exact 1.00; (2) mapped pm steps -\u003e exact 0.98; (3) unique exact quote -\u003e remapped 0.90; (4) block fingerprint -\u003e remapped 0.85; (5) structure + quote -\u003e remapped 0.80; (6) context (prefix/quote/suffix) -\u003e remapped 0.70; (7) bounded fuzzy quote -\u003e remapped 0.50-0.75; (8) line proximity -\u003e 0..=0.35.\n- Candidates from all steps are combined into a single set deduped by currentRange (highest confidence wins on dupes).\n- Verdict rules per Decision #15: exactly one candidate \u003e=0.70 -\u003e remapped/exact; two+ candidates \u003e=0.70 within 0.10 of each other -\u003e ambiguous with all candidates \u003e=0.50; otherwise top candidate \u003e=0.35 -\u003e remapped (low-confidence); else stale.\n- Pure function, no I/O, no crypto. Confidence weights live in a single constant so the calibration task can tune them.\n- Unit tests cover each step's happy path + the ambiguous threshold boundary at 0.09/0.10/0.11.","notes":"Spec: planning/collab/data-model.md §Anchor Resolution (lines 443-513) + planning/collab/amendments.md Decision #15 (line ~331) + §Anchor resolver disagreement policy (line ~110). Files: src/review/anchors/resolve.rs. The pmSteps journal type comes from Phase 0b LocalRevision work. Confidence numbers from data-model.md lines 491-506 ship as starting values; do not hard-code them inline — put them behind a ConfidenceWeights struct for the calibration task.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:45Z","created_by":"James Lal","updated_at":"2026-05-19T01:55:35Z","started_at":"2026-05-19T01:36:52Z","closed_at":"2026-05-19T01:55:35Z","close_reason":"Implemented; merged into collab; 254 Rust tests + 130 relay tests pass; corpus replay green","dependencies":[{"issue_id":"attn-nnj.3.4","depends_on_id":"attn-nnj.2.2","type":"blocks","created_at":"2026-05-18T16:38:21Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.3.4","depends_on_id":"attn-nnj.3","type":"parent-child","created_at":"2026-05-18T16:27:44Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.3.4","depends_on_id":"attn-nnj.3.1","type":"blocks","created_at":"2026-05-18T16:29:44Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} -{"id":"attn-nnj.1.8","title":"ID helpers: RoomId, FileId, EventId, EnvelopeId, SnapshotId, ContentHash","description":"Typed newtypes + derivation functions for every ID/hash in the system. EventId is computed from canonical(body + meta-without-eventId) then written back into meta.eventId. EnvelopeId form differs by envelope kind. Use \"attn file v2\" prefix per amendments.md (NOT \"attn file\").","acceptance_criteria":"- src/review/ids.rs defines newtypes: RoomId, FileId, EventId, EnvelopeId, SnapshotId, ContentHash, ParticipantId, DeviceId (each wraps String with base64url-no-pad)\n- derive_event_id(body, meta_without_event_id) -\u003e EventId per crypto-spec.md §EventId\n- derive_envelope_id(kind, body) -\u003e EnvelopeId per §EnvelopeId (event kind uses eventId-based deterministic form; signal/snapshot_blob use clientNonce)\n- derive_file_id(...) uses prefix \"attn file v2\" per amendments.md §Codebase Corrections (NOT \"attn file\")\n- derive_snapshot_id, derive_content_hash match their spec sections\n- planning/collab/test-vectors/event-id.json + envelope.json populated with (inputs, expected_id) tuples\n- Roundtrip tests pass against both fixtures","notes":"Spec: planning/collab/crypto-spec.md §ID Construction (all subsections) + amendments.md §Codebase Corrections (file prefix correction). All IDs serialize as base64url no-pad strings via serde. ContentHash per §ContentHash: canonical UTF-8 markdown bytes, no BOM, LF line endings, preserve trailing-newline as authored.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:37Z","created_by":"James Lal","updated_at":"2026-05-19T00:23:30Z","started_at":"2026-05-19T00:04:12Z","closed_at":"2026-05-19T00:23:30Z","close_reason":"Implemented in parallel worktrees; merged into collab; 142 tests pass","dependencies":[{"issue_id":"attn-nnj.1.8","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:27:36Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.8","depends_on_id":"attn-nnj.1.1","type":"blocks","created_at":"2026-05-18T16:28:09Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.8","depends_on_id":"attn-nnj.1.2","type":"blocks","created_at":"2026-05-18T16:28:14Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.8","depends_on_id":"attn-nnj.1.3","type":"blocks","created_at":"2026-05-18T16:28:17Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":2,"comment_count":0} -{"id":"attn-nnj.3.3","title":"Anchor construction from selection (frontend)","description":"Build the layered Anchor used by review events from a ProseMirror selection plus the AnchorIndex received in the most recent SnapshotCreated event. Produces all available layers — position from selection, quote from selected text, block lookup from the index, bounded context prefix/suffix, structure from headingPath — then hands the plaintext Anchor up to Rust via IPC for signing+encryption. The frontend never sees ciphertext.","acceptance_criteria":"- web/src/lib/review/anchors.ts exports buildAnchor(selection, anchorIndex, fileId, snapshotId, baseHash) -\u003e Anchor matching data-model.md §Anchors schema.\n- position: byteRange + lineRange + pmRange computed from the current ProseMirror selection.\n- quote: exact + exactHash + normalized + normalizedHash for non-empty selections; omitted for block-level comments.\n- block: looked up from the AnchorIndex by covering byteRange; carries snapshotBlockId, contentFingerprint, kind, offsetInBlockBytes, blockByteRange, blockLineRange.\n- context.prefix and context.suffix are bounded to at most 160 characters (data-model.md §Anchors bounded plaintext fields).\n- Unit tests with vitest cover: inline selection inside a paragraph, selection spanning two paragraphs, block-level (caret-only) anchor, selection at file start/end, selection inside a code block.","notes":"Spec: planning/collab/data-model.md §Anchors (lines 381-441). The AnchorIndex arrives via window.__attn__.reviewSnapshot(...) callbacks (mocked in web/src/lib/mock-ipc.ts during Phase 2). Files: web/src/lib/review/anchors.ts. Hashes use the same canonical sha256 helpers as Rust (web/src/lib/review/crypto.ts from Phase 0a). Send the constructed Anchor to Rust via window.__attn__.reviewSubmit or equivalent IPC — actual signing/encrypting happens in ReviewManager.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:33Z","created_by":"James Lal","updated_at":"2026-05-19T02:29:48Z","started_at":"2026-05-19T01:57:29Z","closed_at":"2026-05-19T02:29:48Z","close_reason":"Implemented (3.8+round-10 retries); merged into collab; 267 Rust + 144 relay tests pass","dependencies":[{"issue_id":"attn-nnj.3.3","depends_on_id":"attn-nnj.3","type":"parent-child","created_at":"2026-05-18T16:27:33Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.3.3","depends_on_id":"attn-nnj.3.1","type":"blocks","created_at":"2026-05-18T16:29:43Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.1.7","title":"Hashcash PoW mint + verify with per-method/path token pool","description":"Implement hashcash proof-of-work tokens per crypto-spec.md §Hashcash. Mint runs off-thread via tokio::task::spawn_blocking (cancellable). Maintain a small per-(method,path) token pool so common writes don't block on cold mints.","acceptance_criteria":"- src/review/crypto/pow.rs exposes `mint(resource, difficulty) -\u003e Future\u003cToken\u003e` and `verify(token, resource, difficulty) -\u003e Result\u003c()\u003e`\n- Token format matches crypto-spec.md §Token Format byte-for-byte (version, resource, salt, counter, hash)\n- Mint executes inside tokio::task::spawn_blocking; cancellation drops the task cleanly\n- Per-(method, path) token pool with configurable max-size (e.g., 4 per slot); replenishes lazily\n- Default difficulty 16; room override accepted in [12, 24] inclusive (reject outside)\n- planning/collab/test-vectors/pow.json populated with (resource, difficulty, valid_token, invalid_tokens)\n- Roundtrip + invalid-difficulty + tampered-resource tests pass","notes":"Spec: planning/collab/crypto-spec.md §Hashcash Proof-of-Work (§Token Format, §Hash Function, §Difficulty, §Server Validation, §Replay Protection, §Client Implementation). Hash function is SHA-256 over canonical token bytes. Bits checked are leading zero bits of the digest. Pool is a HashMap\u003c(String, String), VecDeque\u003cToken\u003e\u003e.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:25Z","created_by":"James Lal","updated_at":"2026-05-19T00:23:15Z","started_at":"2026-05-19T00:04:11Z","closed_at":"2026-05-19T00:23:15Z","close_reason":"Implemented in parallel worktrees; merged into collab; 142 tests pass","dependencies":[{"issue_id":"attn-nnj.1.7","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:27:24Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.7","depends_on_id":"attn-nnj.1.1","type":"blocks","created_at":"2026-05-18T16:28:09Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.7","depends_on_id":"attn-nnj.1.2","type":"blocks","created_at":"2026-05-18T16:28:13Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.7","depends_on_id":"attn-nnj.1.3","type":"blocks","created_at":"2026-05-18T16:28:16Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.1.6","title":"Ed25519 sign/verify wrapper","description":"Wrap ed25519-dalek v2 for signing the canonical bytes of (eventMeta || eventBody). Includes signing-key-ID verification: signingKeyId must equal SHA-256(publicSigningKey).","acceptance_criteria":"- src/review/crypto/signature.rs exposes `sign(signing_key, meta, body) -\u003e Signature` and `verify(public_key, meta, body, signature) -\u003e Result\u003c()\u003e`\n- Signed bytes = canonicalize(meta) || canonicalize(body) per crypto-spec.md §Signatures §Canonical Bytes for Signature\n- verify also checks signingKeyId field == base64url-no-pad(SHA-256(public_key_bytes)) and rejects mismatch\n- planning/collab/test-vectors/event-signature.json populated with deterministic (private_key, meta, body, signature, signingKeyId) tuples\n- Roundtrip + bad-signature + wrong-key-id tests pass against fixture","notes":"Spec: planning/collab/crypto-spec.md §Signatures. Use ed25519_dalek::SigningKey and VerifyingKey. SigningKey impls Zeroize. Canonicalization is via the JCS helper from issue 3. SignatureId formatting uses base64url no-pad.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:12Z","created_by":"James Lal","updated_at":"2026-05-19T00:23:00Z","started_at":"2026-05-19T00:04:11Z","closed_at":"2026-05-19T00:23:00Z","close_reason":"Implemented in parallel worktrees; merged into collab; 142 tests pass","dependencies":[{"issue_id":"attn-nnj.1.6","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:27:12Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.6","depends_on_id":"attn-nnj.1.1","type":"blocks","created_at":"2026-05-18T16:28:08Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.6","depends_on_id":"attn-nnj.1.2","type":"blocks","created_at":"2026-05-18T16:28:12Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.6","depends_on_id":"attn-nnj.1.3","type":"blocks","created_at":"2026-05-18T16:28:16Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.3.1","title":"AnchorIndex builder (Rust, comrak-based)","description":"Build the canonical AnchorIndex in Rust from a snapshot's UTF-8 markdown bytes. This is the authoritative anchor index per amendments.md Phase 1 Decision — the frontend never hashes; it only receives the pre-computed AnchorIndex inside SnapshotCreated events and uses it for resolution. Walks the comrak AST and emits AnchorBlock entries that the resolver and the inline-decoration pipeline depend on.","acceptance_criteria":"- src/review/anchors/index.rs exposes a pure function (markdown bytes, snapshot_id) -\u003e AnchorIndex matching data-model.md §Anchor Index schema (docHash, canonicalEncoding='utf8-bytes', lineCount, blocks[], headings[]).\n- Each AnchorBlock has: kind (heading|paragraph|list_item|code_block|blockquote|table|thematic_break|html|math|mermaid|unknown), byteRange, lineRange, headingPath, ordinalInParent, duplicateOrdinal, textHash, normalizedTextHash, previousBlockHash, nextBlockHash, contentFingerprint, snapshotBlockId.\n- contentFingerprint = sha256(kind || normalizedText || headingPath || duplicateOrdinal); snapshotBlockId = sha256(snapshotId || byteRange || contentFingerprint).\n- Duplicate paragraphs/list-items get distinct duplicateOrdinal values; identical content in different headingPaths yields distinct contentFingerprints.\n- Unit tests cover empty docs, single-block docs, nested-heading docs, duplicate paragraphs, and a fixture from tests/fixtures/.","notes":"Spec: planning/collab/data-model.md §Anchor Index (lines 314-379) + planning/collab/amendments.md Phase 1 Decision (line ~242). Use comrak's AST (existing dep in src/markdown.rs). Sibling crate sha2 (already in Cargo.toml per amendments.md Phase 0a). Files: src/review/anchors/index.rs, src/review/anchors/mod.rs. The 'pmRange' field on AnchorBlock is optional and not populated here (frontend-only derivation).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:08Z","created_by":"James Lal","updated_at":"2026-05-19T01:16:00Z","started_at":"2026-05-19T00:47:50Z","closed_at":"2026-05-19T01:16:00Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.3.1","depends_on_id":"attn-nnj.1.8","type":"blocks","created_at":"2026-05-18T16:38:20Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.3.1","depends_on_id":"attn-nnj.3","type":"parent-child","created_at":"2026-05-18T16:27:07Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":5,"comment_count":0} -{"id":"attn-nnj.1.5","title":"AEAD wrapper (XChaCha20-Poly1305 with AAD)","description":"Wrap XChaCha20-Poly1305 with the envelope-binding AAD format from crypto-spec.md. The AAD ties ciphertext to envelope metadata so a misrouted envelope decrypts to a tag failure rather than silent corruption.","acceptance_criteria":"- src/review/crypto/aead.rs exposes `seal(key, plaintext, aad) -\u003e (nonce, ciphertext)` and `open(key, nonce, ciphertext, aad) -\u003e Result\u003cVec\u003cu8\u003e\u003e`\n- Nonce is 24 bytes from `getrandom` (random, not counter)\n- AAD is canonical JSON of {v, roomId, envelopeId, kind, authorId, deviceId, createdAt} per crypto-spec.md §Envelope Encryption\n- Tampered AAD or ciphertext → Open returns explicit AeadError\n- planning/collab/test-vectors/aead.json populated with (key, nonce, aad, plaintext, ciphertext) tuples\n- Roundtrip + tamper tests pass against fixture","notes":"Spec: planning/collab/crypto-spec.md §Envelope Encryption (AEAD) §Nonce Discipline. Use chacha20poly1305::XChaCha20Poly1305. Key/nonce types should be wrappers that Zeroize. The AAD canonicalization MUST use the canonical JSON helper from issue 3.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:02Z","created_by":"James Lal","updated_at":"2026-05-19T00:22:45Z","started_at":"2026-05-19T00:04:10Z","closed_at":"2026-05-19T00:22:45Z","close_reason":"Implemented in parallel worktrees; merged into collab; 142 tests pass","dependencies":[{"issue_id":"attn-nnj.1.5","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:27:01Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.5","depends_on_id":"attn-nnj.1.1","type":"blocks","created_at":"2026-05-18T16:28:08Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.5","depends_on_id":"attn-nnj.1.2","type":"blocks","created_at":"2026-05-18T16:28:12Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.5","depends_on_id":"attn-nnj.1.3","type":"blocks","created_at":"2026-05-18T16:28:15Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.5","depends_on_id":"attn-nnj.1.4","type":"blocks","created_at":"2026-05-18T16:28:18Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":4,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.1.4","title":"HKDF wrapper + room key derivation","description":"Wrap HKDF-SHA-256 and derive the five per-room subkeys from a 32-byte room secret. Info strings must match crypto-spec.md byte-for-byte so a future browser impl produces identical keys.","acceptance_criteria":"- src/review/crypto/kdf.rs exposes `derive_room_keys(room_secret: \u0026[u8; 32]) -\u003e RoomKeys`\n- RoomKeys struct fields: root_key, event_key, snapshot_key, signaling_key, admission_key (each 32 bytes, zeroize on drop)\n- Info strings match spec exactly: \"attn room root v2\", \"attn room event v2\", \"attn room snapshot v2\", \"attn room signaling v2\", \"attn room admission v2\"\n- planning/collab/test-vectors/kdf.json populated with deterministic vectors (fixed room_secret → exact derived keys hex)\n- Roundtrip test verifies all 5 keys match fixture","notes":"Spec: planning/collab/crypto-spec.md §Key Derivation. Use hkdf crate with Sha256. Salt is empty (or zero-filled per HKDF-Extract spec). RoomKeys impls Zeroize + Drop.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:26:37Z","created_by":"James Lal","updated_at":"2026-05-19T00:22:31Z","started_at":"2026-05-19T00:04:10Z","closed_at":"2026-05-19T00:22:31Z","close_reason":"Implemented in parallel worktrees; merged into collab; 142 tests pass","dependencies":[{"issue_id":"attn-nnj.1.4","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:26:37Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.4","depends_on_id":"attn-nnj.1.1","type":"blocks","created_at":"2026-05-18T16:28:07Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.4","depends_on_id":"attn-nnj.1.2","type":"blocks","created_at":"2026-05-18T16:28:11Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.4","depends_on_id":"attn-nnj.1.3","type":"blocks","created_at":"2026-05-18T16:28:15Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.1.3","title":"Canonical JSON helper (RFC 8785 JCS)","description":"Implement RFC 8785 JSON Canonicalization Scheme in src/review/crypto/canonical.rs. Used by every signature and AEAD AAD computation, so determinism is non-negotiable. Generates test vectors into canonical-json.jsonl.","acceptance_criteria":"- src/review/crypto/canonical.rs exposes `canonicalize(value: \u0026serde_json::Value) -\u003e Vec\u003cu8\u003e`\n- Object keys sorted ASCII-ascending; no whitespace; UTF-8 no BOM\n- Integers only in signed payloads — floats reject with explicit error\n- Absent fields are omitted (never serialized as `\"key\": null`)\n- planning/collab/test-vectors/canonical-json.jsonl populated with edge cases (unicode keys, nested objects, integer boundaries, escape sequences)\n- Roundtrip test: every vector parses, re-canonicalizes byte-identical","notes":"Spec: planning/collab/crypto-spec.md §Canonical JSON (RFC 8785 JCS). Don't use serde_json's default serializer — it doesn't sort keys. Either use a BTreeMap intermediate or implement a custom Serializer. Reject NaN/Infinity. UTF-16 surrogate handling per JCS.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:26:27Z","created_by":"James Lal","updated_at":"2026-05-19T00:03:53Z","started_at":"2026-05-18T23:49:35Z","closed_at":"2026-05-19T00:03:53Z","close_reason":"Implemented; merged into collab; 23 canonical tests + corpus filled","dependencies":[{"issue_id":"attn-nnj.1.3","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:26:27Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.3","depends_on_id":"attn-nnj.1.1","type":"blocks","created_at":"2026-05-18T16:28:06Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.1.3","depends_on_id":"attn-nnj.1.2","type":"blocks","created_at":"2026-05-18T16:28:10Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":5,"comment_count":0} -{"id":"attn-nnj.1.2","title":"Test-vector corpus directory + schema headers","description":"Create planning/collab/test-vectors/ as the canonical contract that all crypto implementations (Rust now, browser/WASM later) must satisfy. Each file gets a documented schema header so future revalidators know what to expect.","acceptance_criteria":"- planning/collab/test-vectors/ exists with: kdf.json, canonical-json.jsonl, event-signature.json, event-id.json, aead.json, envelope.json, pow.json\n- Each file has a top-level schema comment / metadata block describing field semantics + spec section reference\n- README.md in test-vectors/ explains how to regenerate and how to validate\n- Files are placeholders (empty arrays / empty .jsonl) — actual vectors are filled in by issues 3-9","notes":"Spec: planning/collab/crypto-spec.md §Test Vectors §Test Vectors (to ship in the repo). JSONL means newline-delimited JSON. The Rust impl writes these on `cargo test --features generate-vectors` (or similar) and reads them on every test run.","status":"closed","priority":1,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:26:16Z","created_by":"James Lal","updated_at":"2026-05-18T23:44:55Z","closed_at":"2026-05-18T23:44:55Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.1.2","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:26:16Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":8,"comment_count":0} -{"id":"attn-nnj.1.1","title":"Cargo deps + binary-size baseline for crypto crate","description":"Add Rust crypto dependencies to Cargo.toml and record binary-size baseline. This establishes the dependency footprint before any crypto code lands so we can compare against the Phase 4 webrtc-rs cost.","acceptance_criteria":"- Cargo.toml adds: sha2, hkdf, chacha20poly1305, ed25519-dalek v2, base64 (URL_SAFE_NO_PAD), getrandom, zeroize\n- `cargo tree -e features --no-default-features --no-dev-dependencies` output captured in notes\n- Release binary size (cargo build --release) recorded as baseline for Phase 4 comparison\n- `task dev` and `cargo check` both pass with new deps\n- No code uses the deps yet — just declared","notes":"Spec: planning/collab/crypto-spec.md §Primitives §Rust crates. Pin versions exactly. Use `base64::engine::general_purpose::URL_SAFE_NO_PAD` (no padding). zeroize is for SecretKey/Drop impls.","status":"closed","priority":1,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:26:06Z","created_by":"James Lal","updated_at":"2026-05-18T23:44:40Z","closed_at":"2026-05-18T23:44:40Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.1.1","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:26:06Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":7,"comment_count":0} -{"id":"attn-nnj.10","title":"UI/UX: Review surfaces design + iteration","description":"Cross-cutting UI/UX workstream. Discovery + interaction design for share button, room mode selector, connection badge, peer strip, review panel layout, comment/suggestion composer, inline highlight system, ambiguous anchor picker, stale comment panel, snapshot badge/age/superseded, three-way apply UI, outbox indicator, reviewer banner. Drives Phase 2 and feeds into Phase 5.","notes":"User direction: UI/UX is important. Treated as a peer workstream rather than tail-end polish.","status":"closed","priority":1,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:18Z","created_by":"James Lal","updated_at":"2026-05-19T18:01:15Z","closed_at":"2026-05-19T18:01:15Z","close_reason":"Phase complete — all implementation issues closed","dependencies":[{"issue_id":"attn-nnj.10","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:18Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.5","title":"Phase 3a: Relay worker (Cloudflare DO+R2)","description":"Implement relay/ from relay-spec.md against Miniflare. WS-only delivery (decision #5), HMAC admission, hashcash PoW on all writes, room TTL alarms (24h hard-max + 1h idle), R2 spillover for large snapshots. Conformance corpus shared with the Rust client tests.","notes":"Spec: planning/collab/relay-spec.md. Wrangler/Miniflare; relay/ package does not yet exist.","status":"closed","priority":1,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:15Z","created_by":"James Lal","updated_at":"2026-05-19T17:59:57Z","closed_at":"2026-05-19T17:59:57Z","close_reason":"Phase complete — all implementation issues closed","dependencies":[{"issue_id":"attn-nnj.5","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:14Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.4","title":"Phase 2: Review UI with mocked transport","description":"Svelte review panel with comment/suggestion decorations in ProseMirror, anchor-aware highlighting, ambiguous picker, stale state. Mock-IPC extended with replayable event stream so UI work isn't blocked on Rust/network. Demonstrate a comment surviving owner edits using only the local anchor engine.","notes":"Spec: data-model.md §UI/UX Changes. UI/UX is a first-class workstream here per user direction.","status":"closed","priority":1,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:14Z","created_by":"James Lal","updated_at":"2026-05-19T17:59:42Z","closed_at":"2026-05-19T17:59:42Z","close_reason":"Phase complete — all implementation issues closed","dependencies":[{"issue_id":"attn-nnj.4","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:13Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.3","title":"Phase 1: Anchor engine","description":"Build AnchorIndex from markdown bytes in Rust (canonical), construct Anchors from selections, implement the run-all-and-combine resolution policy with confidence thresholds, ship hand-curated test corpus.","notes":"Spec: data-model.md §Anchor Index/Anchors/Anchor Resolution + amendments.md §Anchor resolver disagreement policy. AnchorIndex computed in Rust per amendments.md (canonical path); browser gets it pre-computed in SnapshotCreated events.","status":"closed","priority":1,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:13Z","created_by":"James Lal","updated_at":"2026-05-19T17:01:52Z","closed_at":"2026-05-19T17:01:52Z","close_reason":"All children closed","dependencies":[{"issue_id":"attn-nnj.3","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:13Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.2","title":"Phase 0b: Local data model + working copy","description":"Rust-only foundation. Typed IDs, serde model types, JSON/JSONL local store at ~/.attn/reviews/, WorkingCopyService replacing direct fs::write, revision journal, watcher self-write distinction, empty ReviewManager scaffold, AppState refactor for tab+room routing.","notes":"Spec: planning/collab/data-model.md §Local Replicas + §Rust Architecture Changes. AppState shape per amendments.md (RoomRuntimeHandle + file_to_room mapping).","status":"closed","priority":1,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:12Z","created_by":"James Lal","updated_at":"2026-05-19T17:01:37Z","closed_at":"2026-05-19T17:01:37Z","close_reason":"All children closed","dependencies":[{"issue_id":"attn-nnj.2","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:12Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.1","title":"Phase 0a: Crypto foundations","description":"Rust-only crypto crate (attn-collab-crypto): cipher suite primitives, key derivation, hashcash mint+verify, ID helpers. Frontend never holds ciphertext in v2 — IPC delivers plaintext ReviewEvents per data-model.md §Webview IPC Changes, so no TS crypto is needed for native. Test-vector corpus ships alongside Rust impl for forward compat (browser/Phase 6 will revisit WASM-vs-TS).","notes":"Spec: planning/collab/crypto-spec.md. Decision #4 locks the suite: XChaCha20-Poly1305 + Ed25519 + HKDF-SHA-256 + RFC 8785 JCS + base64url-no-pad. No agility in v2.","status":"closed","priority":1,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:11Z","created_by":"James Lal","updated_at":"2026-05-19T17:01:21Z","closed_at":"2026-05-19T17:01:21Z","close_reason":"All children closed","dependencies":[{"issue_id":"attn-nnj.1","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:11Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-0wa","title":"Owner view flips to shared-document mode after reviewer live edit","description":"During the marketing capture workflow, the owner window is correct through reviewer comment/suggestion, but after the reviewer inserts live text the owner DOM reports data-slot=shared-doc-banner and the capture shows the reviewer Shared document banner. Repro via scripts/capture-collab-screenshots.sh before removing the live text insert. Investigate why the owner loses its local active-tab surface after remote collab steps.","notes":"ITEM 2: real bug that hurts editorial flow. App.svelte (~line 133) flips the OWNER into shared-document mode after a reviewer's live edit (isReviewerInRoom gating). Owner should stay on their local doc; only true reviewers render the shared snapshot.","status":"closed","priority":1,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-22T01:19:42Z","created_by":"James Lal","updated_at":"2026-05-23T05:14:03Z","closed_at":"2026-05-23T05:14:03Z","close_reason":"Fixed: review-mode gating now requires a positive reviewer role (daemon 'Joined'-\u003erole reviewer), not just currentShare===null. currentShare is session-only and lost on reconnect/rehydrate, so an owner returning to a remembered room (role 'owner', no share) flipped into shared-doc view. Extracted isReviewerView/collabRoleFor pure helpers in room-ui.ts; App.svelte uses them; 6 regression cases added (incl. the reconnect case). svelte-check clean, all 28 web test files pass.","dependencies":[{"issue_id":"attn-0wa","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:15Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.7.9","title":"H2: AAD-bind signal envelope target.deviceId (anti-relay-redirect)","description":"Security review (11.5) flagged: signal envelope's target.deviceId is not AAD-bound, allowing the relay to redirect to a different device. Mitigation: enforce envelope.target.deviceId == self.device_id in the inbound signal dispatcher OR include target.deviceId in AAD. See planning/collab/security-review.md §H2.","status":"closed","priority":1,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-19T16:18:29Z","created_by":"James Lal","updated_at":"2026-05-19T17:36:29Z","closed_at":"2026-05-19T17:36:29Z","close_reason":"Round 21 (final push): implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.7.9","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-19T10:18:28Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.5.17","title":"H1: require Attn-Owner-Signature on first POST /v2/rooms/:roomId","description":"Security review (11.5) flagged: room-create POST is un-admitted by design, allowing race attacks. Mitigation: require Attn-Owner-Signature header carrying ownerSigningKey self-sig over the canonical request body on the first POST. See planning/collab/security-review.md §H1.","status":"closed","priority":1,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-19T16:18:28Z","created_by":"James Lal","updated_at":"2026-05-19T17:36:15Z","closed_at":"2026-05-19T17:36:15Z","close_reason":"Round 21 (final push): implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.5.17","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-19T10:18:27Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.11.10","title":"Nested-file nav + breadcrumb regression in directory mode","description":"scripts/test-e2e.sh reports 2 FAILs against the current collab HEAD (2d76b45):\n\n1. Clicking 'nested/child.md' in the sidebar does not change the rendered content. The body remains on the previously-selected basic.md ('Project Status'). Expected to load child.md ('Nested Document').\n\n2. The breadcrumb element is absent. Neither '[class*=\"breadcrumb\"]' nor 'nav[aria-label]' is present in the DOM.\n\nVisual proof: /tmp/attn-e2e-screenshots/06-nested-file.png — sidebar shows child.md selected (highlighted), but the body still shows basic.md content, and no breadcrumb is rendered above the h1.\n\nRepro:\n scripts/test-e2e.sh\n # See suite 2 'Navigate Between Files' — last two assertions FAIL.\n\nNOT caused by attn-nnj.11.2 (doc-only change). Likely surfaced by recent Round-13/14 sidebar/tab/breadcrumb refactors. Probably impacts users navigating directories in real use. Discovered during epic-level e2e verification of attn-nnj.11.","notes":"Discovered during attn-nnj.11 epic-level e2e verification. Test command: scripts/test-e2e.sh. Other harnesses (test-dual-instance-smoke 10/10 PASS, test-review-e2e 12 PASS + 1 PEND) are clean — this is isolated to single-instance directory nav.","status":"closed","priority":1,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-19T04:35:47Z","created_by":"James Lal","updated_at":"2026-05-19T15:06:29Z","closed_at":"2026-05-19T15:06:29Z","close_reason":"Implemented; merged; 412 Rust + 213 relay tests pass (6 conformance scenarios deferred to 5.16)","dependencies":[{"issue_id":"attn-nnj.11.10","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T22:35:46Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.2.11","title":"IpcMessage + SocketMessage review variants (additive)","description":"Pure additive enum extension. In src/ipc.rs:9-59 add IpcMessage variants: review_share, review_join, review_create_comment, review_create_suggestion, review_accept_suggestion, review_resolve_anchor. In src/daemon.rs:62-76 add SocketMessage variants: ReviewShare, ReviewJoin, ReviewPull, ReviewStop, ReviewInbox. Handlers stub to a TODO that the Manager (0b-8) will fill. Lets the frontend stubs (0c-5) wire end-to-end without waiting for ReviewManager.","acceptance_criteria":"- IpcMessage + SocketMessage variants added with serde tagged-enum discrimination\\n- handle_message / handle_client dispatch new variants to TODO!/log handlers without crashing\\n- Frontend can post a review_share message via mock and Rust receives it cleanly\\n- Existing variants untouched","notes":"Audits show both enums are already serde-tagged — this is purely additive. Decouples 0c frontend work from 0b-8 ReviewManager scaffolding.","status":"closed","priority":1,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-19T04:29:36Z","created_by":"James Lal","updated_at":"2026-05-19T17:00:01Z","closed_at":"2026-05-19T17:00:01Z","close_reason":"Duplicate of 12.6/12.7/12.8/12.9/12.10/2.9 — already closed","dependencies":[{"issue_id":"attn-nnj.2.11","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T22:29:36Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.12.15","title":"review/store.ts scaffold (Svelte 5 runes)","description":"Scaffold web/src/lib/review/store.ts as the global review state holder using Svelte 5 runes. Minimal API: for panelOpen, currentRoomId, peers[], threads[] (empty), pendingOutbox (empty). Subscribers to window.__attn__.reviewEvent / reviewStatus push into this. Phase 2 issue 4.2 layers derived selectors on top (comments-on-current-snapshot, ambiguous-list, outbox-count).","acceptance_criteria":"- web/src/lib/review/store.ts exists with -based shape and typed via 0c-4 interfaces\\n- IPC callbacks from 0c-3 push events into the store\\n- No reactivity bugs: subscribing components see updates via \\n- Empty initial state renders cleanly","notes":"Use Svelte 5 runes per project convention (svelte5-best-practices skill exists). Phase 2 4.2 extends this with derived selectors.","status":"closed","priority":1,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-19T04:29:35Z","created_by":"James Lal","updated_at":"2026-05-19T16:59:45Z","closed_at":"2026-05-19T16:59:45Z","close_reason":"Duplicate of 12.6/12.7/12.8/12.9/12.10/2.9 — already closed","dependencies":[{"issue_id":"attn-nnj.12.15","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T22:29:35Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.12.14","title":"keyboard.ts hooks: comment/suggestion/panel-toggle","description":"Extend KeyboardConfig in web/src/lib/keyboard.ts with optional onCommentComposer, onSuggestionComposer, onToggleReviewPanel handlers. Default bindings: Cmd+. (comment), Cmd+Shift+. (suggestion), Cmd+J (toggle panel). Existing shortcuts unaffected. Update KeyboardShortcutsDialog.svelte to surface the new bindings only when a review room is active.","acceptance_criteria":"- 3 new handler keys on KeyboardConfig (all optional)\\n- Default keybinds registered when handlers provided\\n- KeyboardShortcutsDialog conditionally shows the review section\\n- No collision with existing shortcuts (j/k scroll, g/G top-bottom, t theme, q quit, e edit, f sidebar, Cmd+W/[/], etc.)","notes":"CLAUDE.md keyboard table is authoritative for existing shortcuts.","status":"closed","priority":1,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-19T04:29:34Z","created_by":"James Lal","updated_at":"2026-05-19T16:59:28Z","closed_at":"2026-05-19T16:59:28Z","close_reason":"Duplicate of 12.6/12.7/12.8/12.9/12.10/2.9 — already closed","dependencies":[{"issue_id":"attn-nnj.12.14","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T22:29:34Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.12.13","title":"popover-anchor utility (selection → DOM rect → constrained pop)","description":"Shared utility web/src/lib/review/popover-anchor.ts: given a ProseMirror EditorView + selection range, return a DOMRect and a constrained position for a popover (clamped within viewport, flipped above/below as needed). Used by comment composer (2-4), suggestion composer (2-5), and the ambiguous anchor picker (2-7) so all three pop in the same spot.","acceptance_criteria":"- Pure TS function: (view, from, to) → { rect, recommendedAnchor: { top, left, side: 'above'|'below' } }\\n- Handles multi-line selections (uses leading rect)\\n- Unit test against a simulated EditorView\\n- No types","notes":"Reference impl: web/src/lib/CommandPalette.svelte may have positioning logic to reuse.","status":"closed","priority":1,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-19T04:29:33Z","created_by":"James Lal","updated_at":"2026-05-19T16:59:14Z","closed_at":"2026-05-19T16:59:14Z","close_reason":"Duplicate of 12.6/12.7/12.8/12.9/12.10/2.9 — already closed","dependencies":[{"issue_id":"attn-nnj.12.13","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T22:29:33Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.12.12","title":"Theme CSS variables for review surfaces","description":"app.css today has surfaces/accent/sidebar/code-block vars but no decoration/panel vars. Add light + dark mode vars: --comment-highlight, --suggestion-bg, --suggestion-deletion, --confidence-high, --confidence-med, --confidence-low, --moved-badge-bg, --moved-badge-fg, --panel-surface, --panel-border, --peer-avatar-bg-{owner,reviewer,agent}, --stale-anchor-fg. Coordinate values via the existing OKLCH ramp.","acceptance_criteria":"- All new vars defined for both :root (light) and .dark themes\\n- Toggle via existing theme system works without flicker\\n- Variables surface in CSS, ready to be referenced by Phase 2 decoration plugin and components\\n- Use rampa-colors / theme-foundation skills if generating new ramps","notes":"Use OKLCH per project convention. Available skills: rampa-colors, theme-foundation.","status":"closed","priority":1,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-19T04:29:32Z","created_by":"James Lal","updated_at":"2026-05-19T16:59:02Z","closed_at":"2026-05-19T16:59:02Z","close_reason":"Duplicate of 12.6/12.7/12.8/12.9/12.10/2.9 — already closed","dependencies":[{"issue_id":"attn-nnj.12.12","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T22:29:32Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.12.11","title":"mock-ipc.ts: review callback shims (no scenario data)","description":"Extend mock-ipc.ts so the mocked window.ipc.postMessage understands review_* commands (logs them, echoes a fake ack via the review callbacks). No scripted scenario data yet — that's Phase 2 issue 4.1, which builds on this surface. Without this, frontend dev breaks the moment any review_* call is made.","acceptance_criteria":"- Mock dispatches review_* commands to no-op handlers\\n- Mock invokes window.__attn__.reviewStatus/reviewEvent/etc handlers when called via a test helper\\n- No console errors on baseline Starting Vite dev server on http://127.0.0.1:5173\nWaiting for Vite to be ready...\n\n \u001b[32m\u001b[1mVITE\u001b[22m v6.4.1\u001b[39m \u001b[2mready in \u001b[0m\u001b[1m1065\u001b[22m\u001b[2m\u001b[0m ms\u001b[22m\n\n \u001b[32m➜\u001b[39m \u001b[1mLocal\u001b[22m: \u001b[36mhttp://127.0.0.1:\u001b[1m5173\u001b[22m/\u001b[39m\nLaunching attn with HMR enabled (path: .)\n\u001b[2m4:50:22 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32m✨ new dependencies optimized: \u001b[33mshiki/themes, shiki/langs\u001b[32m\u001b[39m\n\u001b[2m4:50:22 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32m✨ optimized dependencies changed. reloading\u001b[39m\n\u001b[2m5:10:53 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/app.css\u001b[22m\n\u001b[2m5:11:10 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/app.css\u001b[22m\n\u001b[2m5:11:21 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/app.css\u001b[22m\n\u001b[2m5:12:47 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/app.css\u001b[22m\n\u001b[2m5:14:39 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/app.css\u001b[22m\n\u001b[2m5:15:01 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/app.css\u001b[22m\n\u001b[2m5:16:23 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mpage reload \u001b[39m\u001b[2msrc/lib/types.ts\u001b[22m\n\u001b[2m5:16:23 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/App.svelte, /src/app.css\u001b[22m\n\u001b[2m5:16:23 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/lib/KeyboardShortcutsDialog.svelte, /src/app.css\u001b[22m\n\u001b[2m5:16:23 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/App.svelte, /src/app.css\u001b[22m\n\u001b[2m5:16:24 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/app.css\u001b[22m\n\u001b[2m5:16:24 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/lib/Editor.svelte, /src/app.css\u001b[22m\n\u001b[2m5:27:05 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mpage reload \u001b[39m\u001b[2msrc/lib/mock-ipc.ts\u001b[22m\n\u001b[2m5:27:05 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/App.svelte, /src/app.css\u001b[22m\n\u001b[2m5:42:43 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/App.svelte, /src/app.css\u001b[22m\n\u001b[2m6:13:05 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mhmr update \u001b[39m\u001b[2m/src/App.svelte, /src/app.css, /src/lib/Sidebar.svelte, /src/lib/Editor.svelte, /src/lib/PathBreadcrumb.svelte, /src/lib/components/ui/sonner/sonner.svelte, /src/lib/FileTree.svelte\u001b[22m\n\u001b[2m6:13:05 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mpage reload \u001b[39m\u001b[2msrc/lib/types.ts\u001b[22m\n\u001b[2m6:32:31 PM\u001b[22m \u001b[36m\u001b[1m[vite]\u001b[22m\u001b[39m \u001b[90m\u001b[2m(client)\u001b[22m\u001b[39m \u001b[32mpage reload \u001b[39m\u001b[2msrc/lib/mock-ipc.ts\u001b[22m","notes":"Phase 2 issue 4.1 layers scripted scenarios on top of this.","status":"closed","priority":1,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-19T04:29:31Z","created_by":"James Lal","updated_at":"2026-05-19T16:58:49Z","closed_at":"2026-05-19T16:58:49Z","close_reason":"Duplicate of 12.6/12.7/12.8/12.9/12.10/2.9 — already closed","dependencies":[{"issue_id":"attn-nnj.12.11","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T22:29:31Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.11.8","title":"Dual-instance E2E harness (owner + reviewer via ATTN_HOME)","description":"Package the multi-instance automation pattern as a reusable test harness. Boots two attn daemons under ATTN_HOME=/tmp/attn-collab-owner and ATTN_HOME=/tmp/attn-collab-reviewer (plus optionally local Miniflare), exposes shell helpers attn_owner(...) and attn_reviewer(...) that prefix the right ATTN_HOME, and demonstrates a baseline assertion script that pokes both daemons via --query / --eval / --click. All review surface E2E tests (Phase 2 demo 4.14, Phase 5 e2e 8.6, Phase 4 WebRTC e2e 7.7) reuse this harness rather than wiring two-daemon plumbing themselves.","acceptance_criteria":"- scripts/lib/dual-instance.sh: sourced library exposing attn_owner / attn_reviewer / start_dual / stop_dual / wait_for_dual\\n- scripts/test-dual-instance-smoke.sh: a smoke test that boots two daemons, confirms each --info reports the right ATTN_HOME, drives --query on each independently, tears down cleanly\\n- Trap-based cleanup so Ctrl+C/early-exit kills both daemons\\n- README section in CLAUDE.md documents the pattern with a copy-pasteable example\\n- The Phase 2 demo (4.14) and Phase 5 e2e (8.6) test scripts source this library — no duplicated start/stop boilerplate","notes":"Depends on 2.10 (ATTN_HOME, done) and 11.4 (e2e scaffolding shape — in flight). The harness should NOT depend on Miniflare being up — leave that as a separate optional flag so this can run before the relay lands. Adopters: 4.14 + 7.7 + 8.6 + 11.7 dev-collab.sh.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T23:26:31Z","created_by":"James Lal","updated_at":"2026-05-18T23:57:28Z","closed_at":"2026-05-18T23:57:28Z","close_reason":"Implemented; merged into collab","dependencies":[{"issue_id":"attn-nnj.11.8","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T17:26:31Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.11.7","title":"scripts/dev-collab.sh: one-command local collab harness","description":"Boot the whole local collab stack with one command. Starts: Miniflare/wrangler relay on :8787 (background), owner daemon with ATTN_HOME=/tmp/attn-collab-owner pointing at a fixture markdown file, reviewer daemon with ATTN_HOME=/tmp/attn-collab-reviewer joining the room via attn://review/... copied from owner share. Tails logs from both. Ctrl+C kills everything cleanly. Sibling to the existing scripts/test-e2e.sh — same conventions.","acceptance_criteria":"- scripts/dev-collab.sh runs and produces a working owner + reviewer pair connected via local relay\\n- Default fixture file: tests/fixtures/basic.md (or a new collab-specific one)\\n- Environment can be overridden via env vars (ATTN_RELAY_URL, FIXTURE_PATH)\\n- Ctrl+C tears down all 3 processes; no orphans\\n- README section 'Local collab testing' explains the workflow","notes":"Depends on ATTN_HOME (0b-10), the relay scaffold (5-1), and the Rust mailbox transport bootstrap flow (6-6) being usable. Useful from Phase 3 onward; can stub-deploy earlier with just the relay running.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:58:47Z","created_by":"James Lal","updated_at":"2026-05-19T17:36:01Z","closed_at":"2026-05-19T17:36:01Z","close_reason":"Round 21 (final push): implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.11.7","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T16:58:46Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.11.7","depends_on_id":"attn-nnj.2.10","type":"blocks","created_at":"2026-05-18T16:58:47Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.11.7","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:58:48Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.11.7","depends_on_id":"attn-nnj.6.6","type":"blocks","created_at":"2026-05-18T16:58:49Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.2.10","title":"ATTN_HOME env override for multi-instance dev","description":"src/daemon.rs:109-121 runtime_dir() picks /tmp/attn-\u003cexe-hash\u003e in debug and ~/.attn in release with no env override. Add ATTN_HOME (or ATTN_RUNTIME_DIR) env var that, when set, overrides both code paths. This unblocks running two attn daemons on one machine for local collab testing: ATTN_HOME=/tmp/attn-owner attn ... and ATTN_HOME=/tmp/attn-reviewer attn .... Also threads through to the future review store path so ~/.attn/reviews/ becomes $ATTN_HOME/reviews/.","acceptance_criteria":"- ATTN_HOME env var, when set, replaces the runtime_dir() default in BOTH debug and release\\n- Socket path, fingerprint, log, and (future) reviews/ all live under $ATTN_HOME\\n- Two daemons started with different ATTN_HOME values don't clobber each other's sockets/state\\n- README or CLAUDE.md updated with the multi-instance dev recipe\\n- Existing single-instance behavior unchanged when ATTN_HOME is unset","notes":"Touches src/daemon.rs runtime_dir(). Also update src/projects.rs:73 to fall back to ATTN_HOME before XDG_STATE_HOME so project registry shares the namespace. Tiny change, unblocks Phase 3+ local testing.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:58:45Z","created_by":"James Lal","updated_at":"2026-05-18T23:18:42Z","closed_at":"2026-05-18T23:18:42Z","close_reason":"Implemented and merged into collab via parallel worktree agents; pnpm build + cargo check both clean post-merge","dependencies":[{"issue_id":"attn-nnj.2.10","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:58:45Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":3,"comment_count":0} +{"id":"attn-nnj.2.9","title":"IpcMessage + SocketMessage review variants (additive)","description":"Pure additive enum extension. In src/ipc.rs:9-59 add IpcMessage variants: review_share, review_join, review_create_comment, review_create_suggestion, review_accept_suggestion, review_resolve_anchor. In src/daemon.rs:62-76 add SocketMessage variants: ReviewShare, ReviewJoin, ReviewPull, ReviewStop, ReviewInbox. Handlers stub to a TODO that the Manager (0b-8) will fill. Lets the frontend stubs (0c-5) wire end-to-end without waiting for ReviewManager.","acceptance_criteria":"- IpcMessage + SocketMessage variants added with serde tagged-enum discrimination\\n- handle_message / handle_client dispatch new variants to TODO!/log handlers without crashing\\n- Frontend can post a review_share message via mock and Rust receives it cleanly\\n- Existing variants untouched","notes":"Audits show both enums are already serde-tagged - this is purely additive. Decouples 0c frontend work from 0b-8 ReviewManager scaffolding.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:53:21Z","created_by":"James Lal","updated_at":"2026-05-18T23:56:36Z","closed_at":"2026-05-18T23:56:36Z","close_reason":"Implemented; merged into collab; 27 tests pass","dependencies":[{"issue_id":"attn-nnj.2.9","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:53:21Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.9","depends_on_id":"attn-nnj.2.1","type":"blocks","created_at":"2026-05-18T16:54:00Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.9","depends_on_id":"attn-nnj.2.2","type":"blocks","created_at":"2026-05-18T16:54:00Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} +{"id":"attn-nnj.12.10","title":"review/store.ts scaffold (Svelte 5 runes)","description":"Scaffold web/src/lib/review/store.ts as the global review state holder using Svelte 5 runes. Minimal API: state for panelOpen, currentRoomId, peers[], threads[] (empty), pendingOutbox (empty). Subscribers to window.__attn__.reviewEvent / reviewStatus push into this. Phase 2 issue 4.2 layers derived selectors on top (comments-on-current-snapshot, ambiguous-list, outbox-count).","acceptance_criteria":"- web/src/lib/review/store.ts exists with rune-based state and typed via 0c-4 interfaces\\n- IPC callbacks from 0c-3 push events into the store\\n- No reactivity bugs: subscribing components see updates via derived\\n- Empty initial state renders cleanly","notes":"Use Svelte 5 runes per project convention (svelte5-best-practices skill exists). Phase 2 4.2 extends this with derived selectors.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:53:20Z","created_by":"James Lal","updated_at":"2026-05-18T23:45:22Z","closed_at":"2026-05-18T23:45:22Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.12.10","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:53:20Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.12.10","depends_on_id":"attn-nnj.12.3","type":"blocks","created_at":"2026-05-18T16:53:46Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.12.10","depends_on_id":"attn-nnj.12.4","type":"blocks","created_at":"2026-05-18T16:53:44Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.12.9","title":"keyboard.ts hooks: comment/suggestion/panel-toggle","description":"Extend KeyboardConfig in web/src/lib/keyboard.ts with optional onCommentComposer, onSuggestionComposer, onToggleReviewPanel handlers. Default bindings: Cmd+. (comment), Cmd+Shift+. (suggestion), Cmd+J (toggle panel). Existing shortcuts unaffected. Update KeyboardShortcutsDialog.svelte to surface the new bindings only when a review room is active.","acceptance_criteria":"- 3 new handler keys on KeyboardConfig (all optional)\\n- Default keybinds registered when handlers provided\\n- KeyboardShortcutsDialog conditionally shows the review section\\n- No collision with existing shortcuts (j/k scroll, g/G top-bottom, t theme, q quit, e edit, f sidebar, Cmd+W/[/], etc.)","notes":"CLAUDE.md keyboard table is authoritative for existing shortcuts.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:53:20Z","created_by":"James Lal","updated_at":"2026-05-18T23:18:28Z","closed_at":"2026-05-18T23:18:28Z","close_reason":"Implemented and merged into collab via parallel worktree agents; pnpm build + cargo check both clean post-merge","dependencies":[{"issue_id":"attn-nnj.12.9","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:53:19Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0} +{"id":"attn-nnj.12.8","title":"popover-anchor utility (selection -\u003e DOM rect -\u003e constrained pop)","description":"Shared utility web/src/lib/review/popover-anchor.ts: given a ProseMirror EditorView + selection range, return a DOMRect and a constrained position for a popover (clamped within viewport, flipped above/below as needed). Used by comment composer (2-4), suggestion composer (2-5), and the ambiguous anchor picker (2-7) so all three pop in the same spot.","acceptance_criteria":"- Pure TS function: (view, from, to) returns { rect, recommendedAnchor: { top, left, side: above|below } }\\n- Handles multi-line selections (uses leading rect)\\n- Unit test against a simulated EditorView\\n- No any types","notes":"Reference impl: web/src/lib/CommandPalette.svelte may have positioning logic to reuse.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:53:19Z","created_by":"James Lal","updated_at":"2026-05-18T23:18:15Z","closed_at":"2026-05-18T23:18:15Z","close_reason":"Implemented and merged into collab via parallel worktree agents; pnpm build + cargo check both clean post-merge","dependencies":[{"issue_id":"attn-nnj.12.8","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:53:18Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":3,"comment_count":0} +{"id":"attn-nnj.12.7","title":"Theme CSS variables for review surfaces","description":"app.css today has surfaces/accent/sidebar/code-block vars but no decoration/panel vars. Add light + dark mode vars: --comment-highlight, --suggestion-bg, --suggestion-deletion, --confidence-high, --confidence-med, --confidence-low, --moved-badge-bg, --moved-badge-fg, --panel-surface, --panel-border, --peer-avatar-bg-{owner,reviewer,agent}, --stale-anchor-fg. Coordinate values via the existing OKLCH ramp.","acceptance_criteria":"- All new vars defined for both :root (light) and .dark themes\\n- Toggle via existing theme system works without flicker\\n- Variables surface in CSS, ready to be referenced by Phase 2 decoration plugin and components\\n- Use rampa-colors / theme-foundation skills if generating new ramps","notes":"Use OKLCH per project convention. Available skills: rampa-colors, theme-foundation.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:53:18Z","created_by":"James Lal","updated_at":"2026-05-18T23:18:02Z","closed_at":"2026-05-18T23:18:02Z","close_reason":"Implemented and merged into collab via parallel worktree agents; pnpm build + cargo check both clean post-merge","dependencies":[{"issue_id":"attn-nnj.12.7","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:53:17Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":3,"comment_count":0} +{"id":"attn-nnj.12.6","title":"mock-ipc.ts: review callback shims (no scenario data)","description":"Extend mock-ipc.ts so the mocked window.ipc.postMessage understands review_* commands (logs them, echoes a fake ack via the review callbacks). No scripted scenario data yet — that's Phase 2 issue 4.1, which builds on this surface. Without this, frontend dev breaks the moment any review_* call is made.","acceptance_criteria":"- Mock dispatches review_* commands to no-op handlers; no console errors\\n- Mock invokes window.__attn__.reviewStatus/reviewEvent/reviewSnapshot/reviewAnchorResolution handlers when called via a test helper\\n- No regression in existing mocked surface","notes":"Phase 2 issue 4.1 layers scripted scenarios on top of this.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:53:17Z","created_by":"James Lal","updated_at":"2026-05-19T00:40:12Z","closed_at":"2026-05-19T00:40:12Z","close_reason":"Implemented in parallel; merged into collab; 166 tests pass + relay 35 tests pass","dependencies":[{"issue_id":"attn-nnj.12.6","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:53:17Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.12.6","depends_on_id":"attn-nnj.12.3","type":"blocks","created_at":"2026-05-18T16:53:45Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.12.6","depends_on_id":"attn-nnj.12.4","type":"blocks","created_at":"2026-05-18T16:53:44Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.12.6","depends_on_id":"attn-nnj.12.5","type":"blocks","created_at":"2026-05-18T16:53:45Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.12.4","title":"types.ts: ReviewEvent / Anchor / ResolvedAnchor interfaces","description":"Add review domain TypeScript interfaces matching the Rust serde shapes (camelCase): ReviewEvent (with EventMeta/Body discriminated union), ReviewStatus, ReviewSnapshot, ReviewAnchorResolutionUpdate, Anchor (with PositionAnchor/QuoteAnchor/BlockAnchor/ContextAnchor/StructureAnchor sub-types), ResolvedAnchor (exact|remapped|ambiguous|stale variants), SuggestionOperation. Source of truth is data-model.md.","acceptance_criteria":"- No types (user instruction)\\n- All variants from data-model.md represented\\n- Includes JSDoc citing data-model.md section per type\\n- Roundtrips through JSON.parse(JSON.stringify(x)) without loss","notes":"data-model.md §Anchors, §Anchor Resolution, §Review Events.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:50:13Z","created_by":"James Lal","updated_at":"2026-05-18T23:17:47Z","closed_at":"2026-05-18T23:17:47Z","close_reason":"Implemented and merged into collab via parallel worktree agents; pnpm build + cargo check both clean post-merge","dependencies":[{"issue_id":"attn-nnj.12.4","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:50:12Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":4,"comment_count":0} +{"id":"attn-nnj.12.5","title":"ipc.ts: review_* outbound command stubs","description":"Extend web/src/lib/ipc.ts with typed outbound commands: review_share, review_join, review_create_comment, review_create_suggestion, review_accept_suggestion, review_resolve_anchor. Each is a thin wrapper around window.ipc.postMessage with the typed payload. No backend yet; calls land at the Rust IpcMessage handler stub (0b-9).","acceptance_criteria":"- 6 typed exported functions in web/src/lib/ipc.ts\\n- Payloads typed via types.ts (depends on 0c-4)\\n- Real ipc.ts uses postMessage; mock-ipc.ts (depends on 0c-6) routes to local handler\\n- Functions are async and return a typed Result/Promise","notes":"Spec: data-model.md §Webview IPC Changes.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:50:13Z","created_by":"James Lal","updated_at":"2026-05-19T00:23:43Z","closed_at":"2026-05-19T00:23:43Z","close_reason":"Implemented in parallel worktrees; merged into collab; 142 tests pass","dependencies":[{"issue_id":"attn-nnj.12.5","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:50:13Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.12.5","depends_on_id":"attn-nnj.12.4","type":"blocks","created_at":"2026-05-18T16:53:43Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.12.5","depends_on_id":"attn-nnj.2.9","type":"blocks","created_at":"2026-05-18T16:54:02Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":5,"comment_count":0} +{"id":"attn-nnj.12.2","title":"Editor.svelte: $props-injectable plugins + nodeViews","description":"Editor.svelte:137-186 buildPlugins() is monolithic; nodeViews dict at 475-484 is closed. Extend to accept optional plugins?: Plugin[] and nodeViews?: Record\u003cstring, NodeViewConstructor\u003e via Svelte 5 , appended after built-ins. Enables Phase 2 decorations plugin and any future collab plugins without re-hardcoding.","acceptance_criteria":"- extended with plugins?, nodeViews?\\n- Built-in plugins still loaded first; injected plugins appended\\n- Existing callers unaffected (props are optional)\\n- Stub test: passing an empty decoration plugin must not regress existing nodeViews (math, mermaid, tables, code-highlight)","notes":"Model after the existing prosemirror/code-highlight.ts factory shape.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:50:11Z","created_by":"James Lal","updated_at":"2026-05-18T23:17:33Z","closed_at":"2026-05-18T23:17:33Z","close_reason":"Implemented and merged into collab via parallel worktree agents; pnpm build + cargo check both clean post-merge","dependencies":[{"issue_id":"attn-nnj.12.2","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:50:10Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.12.3","title":"window.__attn__ bridge: review callback registration","description":"App.svelte:1003-1019 registers setContent/updateContent/font scale only. Extend with no-op stubs for reviewStatus(payload), reviewEvent(payload), reviewSnapshot(snapshot), reviewAnchorResolution(update). Real handlers wire to the review store later (Phase 0c-10 / Phase 2).","acceptance_criteria":"- window.__attn__ exposes the 4 new methods, each typed via types.ts (depends on 0c-4)\\n- Default impl: console.debug only, so Rust can already evaluate_script without errors\\n- Type definitions in web/src/vite-env.d.ts (or wherever Window augmentation lives) updated","notes":"Spec: data-model.md §Webview IPC Changes.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:50:11Z","created_by":"James Lal","updated_at":"2026-05-18T23:29:47Z","closed_at":"2026-05-18T23:29:47Z","close_reason":"Implemented via parallel worktree agents; merged into collab","dependencies":[{"issue_id":"attn-nnj.12.3","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:50:11Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.12.3","depends_on_id":"attn-nnj.12.4","type":"blocks","created_at":"2026-05-18T16:53:42Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} +{"id":"attn-nnj.12.1","title":"3-column layout: right-rail slot in App.svelte","description":"App.svelte:1352-1373 SidebarInset is 2-column today (sidebar + editor). Extend to 3-column with a named right-rail slot for the future ReviewPanel. Slot collapses when no review room is active (no chrome shift).","acceptance_criteria":"- App.svelte exposes a right-rail snippet/slot that mounts a placeholder div when no review session\\n- Layout uses CSS flex/grid, not absolute positioning\\n- Sidebar toggle (existing) still works; right-rail toggles independently via Cmd+J (placeholder shortcut)\\n- No visual regression with attn ./planning/ baseline","notes":"Touches App.svelte mainContent snippet. Don't render ReviewPanel yet — just the slot.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:50:09Z","created_by":"James Lal","updated_at":"2026-05-18T23:17:18Z","closed_at":"2026-05-18T23:17:18Z","close_reason":"Implemented and merged into collab via parallel worktree agents; pnpm build + cargo check both clean post-merge","dependencies":[{"issue_id":"attn-nnj.12.1","depends_on_id":"attn-nnj.12","type":"parent-child","created_at":"2026-05-18T16:50:09Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":6,"comment_count":0} +{"id":"attn-nnj.12","title":"Phase 0c: UI/IPC plumbing","description":"Frontend + IPC infrastructure that has to land before Phase 2 features can drop in cleanly. Pure plumbing — no review UI rendered yet. Audit grounded: App.svelte:1352-1373 has only 2-column SidebarInset; Editor.svelte:137-186 hardcodes 8 plugins; App.svelte:1003-1019 window.__attn__ bridge registers no review callbacks; mock-ipc.ts has no review surface; ipc.ts has no review_* commands; types.ts has no ReviewEvent shapes; theme has no decoration vars.","notes":"Sequencing: blocks Phase 2 features. Per user direction, all crypto stays in Rust — frontend only handles plaintext ReviewEvent objects.","status":"closed","priority":1,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:50:08Z","created_by":"James Lal","updated_at":"2026-05-19T17:02:10Z","closed_at":"2026-05-19T17:02:10Z","close_reason":"All children closed","dependencies":[{"issue_id":"attn-nnj.12","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:50:07Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.11.5","title":"Security review pass","description":"After Phase 3a / 3b / 4 land and before any public release: full security review of crypto envelope handling. Specifically check: signature verification ordering (decrypt under AEAD FIRST, then verify the plaintext signature — never the reverse, which would let an attacker substitute a verified ciphertext); AAD binding completeness on every encrypt/decrypt call site (no naked AEAD calls); PoW token replay window (the 5-minute window from decision #6 actually enforced); ownerSigningKey TOFU correctness (first signature wins, subsequent different keys rejected); browser fragment-stripping race window (no observer can see the fragment between page load and replaceState). Use the security-review skill if available.","acceptance_criteria":"planning/collab/security-review.md created with findings + severity per checked area.\nEach of the five focus areas (decrypt-then-verify order, AAD binding, PoW replay window, owner TOFU, browser fragment race) has a section with: pass/fail, code references, evidence.\nAny HIGH or CRITICAL findings have follow-up bd issues created and linked.\nReview covers Rust client AND TS browser client AND relay worker — all three speak crypto.\nSign-off recorded in the doc with date and reviewer.","notes":"Specs: planning/collab/crypto-spec.md (entire), planning/collab/relay-spec.md §Anti-Abuse + §Admission, planning/collab/amendments.md §Decision #6 + §Decision #13 + §Owner identity. Files: planning/collab/security-review.md (new), src/review/crypto.rs, relay/src/*, web/src/lib/review/*. Skills: security-review skill is available — invoke it explicitly. Schedule AFTER Phase 4 lands (so all crypto codepaths exist) but BEFORE public release.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:35:24Z","created_by":"James Lal","updated_at":"2026-05-19T16:25:30Z","closed_at":"2026-05-19T16:25:30Z","close_reason":"Round 19: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.11.5","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T16:35:24Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.11.5","depends_on_id":"attn-nnj.5.15","type":"blocks","created_at":"2026-05-18T16:38:34Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.11.5","depends_on_id":"attn-nnj.6.7","type":"blocks","created_at":"2026-05-18T16:38:35Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.11.5","depends_on_id":"attn-nnj.7.7","type":"blocks","created_at":"2026-05-18T16:38:35Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.11.5","depends_on_id":"attn-nnj.8.6","type":"blocks","created_at":"2026-05-18T16:38:36Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.11.4","title":"E2E test scaffolding for review surfaces","description":"Extend scripts/test-e2e.sh with a review/ test suite that boots the daemon under task dev with a mock-IPC scenario file pre-loaded, then uses --eval / --query / --wait-for to assert review-panel state (comment count, anchor status, suggestion list, etc.). Lays groundwork for Phase 2's 'comment survives owner edits' demo AND Phase 5's apply integration test. Reuses the daemon's existing automation flags so we don't need a separate Playwright runner.","acceptance_criteria":"scripts/test-e2e.sh has a new review/ section that loads a mock-IPC fixture into web/src/lib/mock-ipc.ts via env var or query param.\nHelpers: wait_for_review_state, assert_comment_count, assert_anchor_status etc. (bash functions) that wrap --query and --wait-for.\nA scaffold test boots the daemon with a fixture containing 2 mock comments and asserts both render in the panel.\nTest passes locally on macOS without requiring a relay or webrtc-rs (mock-IPC drives the frontend in isolation).\nScreenshot captured to /tmp/attn-e2e-screenshots/review-*.png for visual review.\nPattern documented so Phase 2 + Phase 5 authors can extend without reinventing.","notes":"Specs: planning/collab/amendments.md §existing automation flags affect ReviewManager design, §Mock IPC must be extended. Files: scripts/test-e2e.sh, tests/fixtures/review/ (new), web/src/lib/mock-ipc.ts (extend). Mock-IPC extension is described in amendments.md §Mock IPC — coordinate so this scaffolding lands alongside or after that extension. window.__attn__.reviewState() should be a stable shape that this test depends on.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:35:09Z","created_by":"James Lal","updated_at":"2026-05-18T23:41:35Z","closed_at":"2026-05-18T23:41:35Z","close_reason":"Re-close after DB restore: implemented; merged into collab; 8 PASS / 5 PEND / 0 FAIL","dependencies":[{"issue_id":"attn-nnj.11.4","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T16:35:09Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0} +{"id":"attn-nnj.6.7","title":"Conformance integration tests against Miniflare","description":"Run the relay conformance corpus from Phase 3a issue 14 against the Rust client. CI script: boot 'wrangler dev --local' (Miniflare) from the relay/ package, then run cargo test --features mailbox-integration on the Rust crate. Covers happy path and every error code the relay returns so the Rust client stays in lock-step with the relay's wire contract.","acceptance_criteria":"- scripts/test-mailbox-integration.sh (or task target): starts wrangler dev --local in background, waits for /health, runs cargo test --features mailbox-integration -- --test-threads=1, tears down\n- Rust tests under src/review/transport/tests/ (or tests/ integration crate) load relay/test/conformance/cases.json via serde and execute each case via the real MailboxTransport\n- Coverage matches Phase 3a issue 14 corpus: room lifecycle, WS backfill (full / mid / 4005), all caps + batch=32, owner-only ops, PoW failures, hibernation roundtrip, rate limits, longSession clamping\n- CI integration: GitHub Actions job runs this script and fails on any case mismatch\n- Tests fail fast on any new relay-side error code missing from the Rust mapping (helps catch wire drift early)","notes":"Spec: planning/collab/relay-spec.md §Test Plan (687-705). Consumes conformance corpus from 3a-14 — make sure the JSON schema is serde-friendly. Blocked by 3b-6 since bootstrap is the precondition for every other case.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:58Z","created_by":"James Lal","updated_at":"2026-05-19T13:50:28Z","closed_at":"2026-05-19T13:50:28Z","close_reason":"Implemented (partially for 5.14 — scaffold + skip-on-empty, follow-up to fill cases.json); 372 Rust + 184 relay tests pass","dependencies":[{"issue_id":"attn-nnj.6.7","depends_on_id":"attn-nnj.2.10","type":"blocks","created_at":"2026-05-18T16:58:49Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.7","depends_on_id":"attn-nnj.5.14","type":"blocks","created_at":"2026-05-18T16:35:59Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.7","depends_on_id":"attn-nnj.6","type":"parent-child","created_at":"2026-05-18T16:34:57Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.7","depends_on_id":"attn-nnj.6.1","type":"blocks","created_at":"2026-05-18T16:36:09Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.7","depends_on_id":"attn-nnj.6.6","type":"blocks","created_at":"2026-05-18T16:36:12Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":4,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.6.6","title":"Device + room bootstrap flow (Share + Join commands)","description":"Wire ReviewManager into ReviewCommand::Share and ReviewCommand::Join. Share: derive room keys (event/snapshot/signal/admission per crypto-spec §Key Derivation), POST /v2/rooms/:roomId with ownerSigningKey + clamped policy, POST /devices with kind=owner and self-signature, populate the local /devices cache, emit ReviewUpdate::RoomCreated. Join: parse the invite URL (crypto-spec §Invite URLs), derive the same per-kind keys, POST /devices with kind=reviewer or agent, GET /devices to populate the verification cache, emit ReviewUpdate::ParticipantJoined.","acceptance_criteria":"- Share flow: generate room secret → derive event/snapshot/signal/admission keys + ownerSigningKey → POST /v2/rooms/:roomId (mint PoW from pool, send admissionKey, ownerSigningKey, clamped policy) → POST /devices (kind=owner, selfSignature over canonical device bytes) → store room state under ~/.attn/reviews/rooms/\u003croomId\u003e/ → emit ReviewUpdate::RoomCreated{invite_url}\n- Join flow: parse invite URL → derive keys identically → POST /devices (kind=reviewer|agent per CLI flag) → GET /devices → cache the device roster (publicSigningKey by deviceId) → emit ReviewUpdate::ParticipantJoined{room, peers}\n- Owner-key handling: ownerSigningKey is generated client-side at Share, stored locally as private key, public half sent in the create body; never written outside ~/.attn (file perms 600)\n- Both flows install the WS client (issue 3b-3) and outbox processor (3b-2) for the room after bootstrap completes\n- Bootstrap errors map: room create 409 ATTN_ROOM_EXISTS_DIFFERENT_POLICY → ReviewUpdate::ShareConflict; device 409 ATTN_DEVICE_KEY_CHANGED → ReviewUpdate::JoinKeyConflict\n- Tests: Share against Miniflare, Join against same Miniflare instance, key derivation determinism, owner-key mismatch on rejoin attempt, peer roster cached and refreshed","notes":"Spec: planning/collab/crypto-spec.md §Key Derivation (39-77), §Invite URLs (59-77), §Signing-Key Publication (344-403). planning/collab/relay-spec.md §POST /v2/rooms/:roomId (114-167), §POST /v2/rooms/:roomId/devices (169-217). Consumes issue 3b-3 (WS client) and 3b-2 (outbox) once bootstrap completes.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:57Z","created_by":"James Lal","updated_at":"2026-05-19T04:07:48Z","closed_at":"2026-05-19T04:07:48Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.6.6","depends_on_id":"attn-nnj.11.1","type":"blocks","created_at":"2026-05-18T16:38:27Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.6","depends_on_id":"attn-nnj.5.5","type":"blocks","created_at":"2026-05-18T16:38:27Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.6","depends_on_id":"attn-nnj.5.6","type":"blocks","created_at":"2026-05-18T16:38:26Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.6","depends_on_id":"attn-nnj.6","type":"parent-child","created_at":"2026-05-18T16:34:56Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.6","depends_on_id":"attn-nnj.6.1","type":"blocks","created_at":"2026-05-18T16:36:09Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.6","depends_on_id":"attn-nnj.6.3","type":"blocks","created_at":"2026-05-18T16:36:11Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":5,"dependent_count":2,"comment_count":0} +{"id":"attn-nnj.6.5","title":"Cursor management + 4005 cursor-too-old recovery","description":"Persist last_seen_seq per room in ~/.attn/reviews/rooms/\u003croomId\u003e/cursors.json. Update after every successfully imported envelope (issue 3b-4 wires the actual write). On WS error{ATTN_CURSOR_TOO_OLD, resyncFromSeq}: discard the current cursor, attempt a P2P snapshot request from a peer device (defer this branch to Phase 4 if WebRTC transport isn't yet wired; in the meantime fall back to re-subscribe from resyncFromSeq, accepting the pre-resync history loss explicitly via ReviewUpdate::HistoryGap).","acceptance_criteria":"- ~/.attn/reviews/rooms/\u003croomId\u003e/cursors.json holds {last_seen_seq, oldest_retained_seq, updated_at}; atomic write via temp+rename\n- Read on startup; passed to issue 3b-3 WS client for the initial subscribe.after\n- On TransportError::CursorTooOld{resync_from_seq}: log warning, emit ReviewUpdate::HistoryGap{lost_from, lost_to}, set last_seen_seq=resync_from_seq, write cursors.json, reconnect with subscribe{after: resync_from_seq}\n- Stub for P2P snapshot recovery: a clear TODO('phase-4 webrtc') with the signature of a future async fn request_snapshot_from_peer(peer_device_id) so Phase 4 can plug in\n- Tests: cursor persistence across restart, 4005 fallback to resync (no P2P), atomic write under crash simulation, HistoryGap emitted exactly once per 4005","notes":"Spec: planning/collab/relay-spec.md §WebSocket Protocol (372-422, error frame) and §Close Codes (4005). amendments.md decision #5 (no GET /envelopes backfill means 4005 is the only way history is exposed). Snapshot-from-peer plumbing depends on Phase 4 WebRTC and is intentionally stubbed here.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:56Z","created_by":"James Lal","updated_at":"2026-05-19T04:30:01Z","closed_at":"2026-05-19T04:30:01Z","close_reason":"Implemented; merged","dependencies":[{"issue_id":"attn-nnj.6.5","depends_on_id":"attn-nnj.6","type":"parent-child","created_at":"2026-05-18T16:34:55Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.5","depends_on_id":"attn-nnj.6.1","type":"blocks","created_at":"2026-05-18T16:36:08Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.5","depends_on_id":"attn-nnj.6.3","type":"blocks","created_at":"2026-05-18T16:36:11Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.6.4","title":"Inbound envelope pipeline (decrypt + verify + import)","description":"Per ServerFrame::Envelope from the WS stream: look up the per-kind key (eventKey | snapshotKey | signalKey) from room state, AES-256-GCM decrypt, verify Ed25519 signature against the deviceId's publicSigningKey from the cached /devices roster, dedupe by EventId (against ~/.attn/reviews/rooms/\u003croomId\u003e/events.jsonl ledger), append to events.jsonl, update the persistent sync cursor. On success emit ReviewUpdate::EventImported into the ReviewManager event loop.","acceptance_criteria":"- For each ServerFrame::Envelope: select key by envelope.kind; decrypt (AES-256-GCM, nonce per crypto-spec §Nonce Discipline) — failure → ReviewUpdate::DecryptFailed and continue (do not crash, do not advance cursor)\n- Ed25519 verify plaintext signature against device.publicSigningKey from cached /devices snapshot; signature failure → drop + ReviewUpdate::SignatureInvalid (do not advance cursor for that envelope)\n- Dedupe by EventId: if already present in events.jsonl skip the append (but still advance the seq cursor)\n- Append the canonical event bytes to events.jsonl (append-only, fsync per batch)\n- Update last_seen_seq AFTER successful append (so a crash mid-write replays cleanly)\n- Emit ReviewUpdate::EventImported{event} to the consumer (Phase 4 UI / ReviewManager)\n- Cached /devices roster auto-refreshed when an envelope arrives signed by an unknown deviceId (issue GET /devices and retry verify once)\n- Tests: happy path, wrong key, wrong sig, duplicate EventId, unknown device triggers refresh + verify","notes":"Spec: planning/collab/crypto-spec.md §Envelope Encryption (79-115), §Nonce Discipline (108-115), §Signatures (199-258). planning/collab/data-model.md §Transport Model. /devices cache is populated by issue 3b-6 bootstrap and refreshed lazily.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:55Z","created_by":"James Lal","updated_at":"2026-05-19T04:06:29Z","closed_at":"2026-05-19T04:06:29Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.6.4","depends_on_id":"attn-nnj.6","type":"parent-child","created_at":"2026-05-18T16:34:55Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.4","depends_on_id":"attn-nnj.6.1","type":"blocks","created_at":"2026-05-18T16:36:08Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.11.3","title":"Binary-size verification CI gate","description":"Add scripts/check-binary-size.sh that runs after cargo build --release and asserts the .app bundle (or stripped binary if not bundled) is under 25 MiB. Wire into CI so Phase 4 (webrtc-rs) cannot regress past the budget without an explicit waiver. This is the safety net that prevents future PRs from silently breaking the decision #1 tradeoff.","acceptance_criteria":"scripts/check-binary-size.sh exists; runs du on the appropriate artifact (.app bundle preferred, falls back to stripped binary).\nExits non-zero with a clear message if size \u003e 25 MiB; prints the size + the budget either way.\nCI workflow (.github/workflows/*.yml) invokes it on every PR that touches Cargo.toml or src/**.\nFailure can be waived only by setting ATTN_SIZE_BUDGET_WAIVER=1 in CI env (or equivalent) with a comment in the PR.\nDocumented in CLAUDE.md or RELEASE_SETUP.md so future contributors know the rule.","notes":"Specs: planning/collab/amendments.md §Decision #1, §Phase 4. Files: scripts/check-binary-size.sh (new), .github/workflows/*.yml. Use the same release/bundle output the existing scripts/build.sh produces. The 25 MiB number comes directly from Decision #1's tradeoff statement. Blocks Phase 4 work in the sense that Phase 4 issue 1 should consume this gate.","status":"closed","priority":1,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:54Z","created_by":"James Lal","updated_at":"2026-05-18T23:45:09Z","closed_at":"2026-05-18T23:45:09Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.11.3","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T16:34:54Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.11.1","title":"attn://review/... custom-scheme handler","description":"In src/main.rs custom protocol handler, route attn://review/\u003croomId\u003e#key=... to a new SocketMessage::ReviewJoin{invite} BEFORE falling through to the existing attn://localhost/... file-serving path. Reject attn://localhost/review/... explicitly (it's reserved per amendments.md). Without this, clicking an invite URL on macOS won't work and Phase 4/6 invite handling is blocked.","acceptance_criteria":"src/main.rs custom protocol handler matches attn://review/\u003c...\u003e before the existing file-serving route.\nMatched invites become a SocketMessage::ReviewJoin{invite: String} dispatched to the daemon.\nattn://localhost/review/... returns a 400-equivalent (refused with a clear log line) — reserved path collision.\nNon-review attn://localhost/* paths continue to serve files as today (existing behavior unchanged).\nUnit test or integration test (via --eval) confirms a synthetic attn://review/abc#key=xyz hits the ReviewJoin handler.\nSocketMessage::ReviewJoin variant defined and wired through daemon.rs.","notes":"Specs: planning/collab/amendments.md §Custom attn:// scheme handler. Files: src/main.rs (~1207 lines — locate the existing custom_protocol registration), src/daemon.rs (new SocketMessage variant), src/ipc.rs (potentially). Critical detail: the route match must come BEFORE the fallthrough, not after. Blocks Phase 4 (issue 7 e2e test needs to click an invite) and Phase 6 (browser invite parse logic on the native side too).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:30Z","created_by":"James Lal","updated_at":"2026-05-18T23:33:30Z","closed_at":"2026-05-18T23:33:30Z","close_reason":"Implemented; merged into collab; cargo check + cargo test clean","dependencies":[{"issue_id":"attn-nnj.11.1","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T16:34:30Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.9.7","title":"CLI subcommands for local agents","description":"Full set of attn review subcommands so a local agent (e.g., a coding assistant running on the owner's machine) can drive the daemon: share \u003cpath\u003e [--mode live|async|hybrid] [--ttl 7d]; join \u003cinvite\u003e; inbox [--json]; submit-comment \u003cfile\u003e; submit-suggestion \u003cfile\u003e; pull; stop. Each maps to a SocketMessage variant per data-model.md §Daemon Socket Commands. Local CLI uses owner's daemon identity by default (per amendments.md §Agent CLI key handling) — distinct from remote agents in Phase 6 issue 6.","acceptance_criteria":"Each subcommand parses via clap and dispatches a typed SocketMessage to the running daemon.\nshare creates a new room with the given policy fields, prints the invite URL (attn://review/...) for the owner to share.\njoin takes either an attn://review/... URL or a raw invite string; daemon joins the room and starts receiving envelopes.\ninbox lists pending review actions on the owner's open rooms; --json emits structured output for agent consumption.\nsubmit-comment / submit-suggestion take a JSON file (or stdin) describing the anchor + content; daemon adds to outbox with the owner's identity by default.\npull manually triggers a relay catchup. stop ends a room (owner only — requires owner signature).\nCLI help (attn review --help) lists all subcommands with clear examples.","notes":"Specs: planning/collab/amendments.md §Agent CLI key handling, planning/collab/data-model.md §Daemon Socket Commands. Files: src/cli/review.rs (new), src/daemon.rs (new SocketMessage variants). Owner-identity default is the load-bearing detail vs Phase 6 issue 6 (remote agent has its own key). --as-agent \u003cname\u003e override comes from Phase 6 issue 6's implementation but the flag itself can be wired here.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:15Z","created_by":"James Lal","updated_at":"2026-05-19T15:06:14Z","closed_at":"2026-05-19T15:06:14Z","close_reason":"Implemented; merged; 412 Rust + 213 relay tests pass (6 conformance scenarios deferred to 5.16)","dependencies":[{"issue_id":"attn-nnj.9.7","depends_on_id":"attn-nnj.2.8","type":"blocks","created_at":"2026-05-18T16:39:10Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.9.7","depends_on_id":"attn-nnj.9","type":"parent-child","created_at":"2026-05-18T16:34:14Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.6.3","title":"WebSocket client + reconnect with backoff","description":"Tokio-tungstenite (or async-tungstenite) WebSocket client. Subprotocol header: 'attn.v2, \u003cbase64url admission HMAC\u003e'. Handles server ping by responding with pong inside 60s. On disconnect: exponential backoff reconnect (200ms → 30s cap with jitter), replay subscribe{after: last_seen_seq} on each reconnect. Maps close codes 4000/4001/4002/4005 to typed transport errors so the ReviewManager can surface them as ReviewUpdate variants.","acceptance_criteria":"- Connects with subprotocol 'attn.v2' and admission HMAC piggybacked (matches issue 3a-11 handshake)\n- After connect, sends subscribe{after: last_seen_seq} and starts receiving ServerFrame items as a Stream\n- Ping handler: replies with pong within 60s; if no server ping in 90s, force reconnect (defensive)\n- Disconnect handler: exponential backoff (200ms, 400ms, 800ms, ... cap 30s) with ±25% jitter\n- Close codes mapped: 4000→TransportError::AdmissionInvalid (non-retryable), 4001→RoomDeleted (terminal), 4002→RoomExpired (terminal), 4005→CursorTooOld{resync_from_seq} (handled by issue 3b-5), 1001→Timeout (retryable)\n- Replays subscribe{after: last_seen_seq} on every reconnect so the stream resumes from the persistent cursor\n- Tests: happy-path frame roundtrip (against Miniflare from 3a-11), ping/pong, reconnect after server-initiated close, 4005 surfacing, 4001/4002 terminal","notes":"Spec: planning/collab/relay-spec.md §WebSocket Protocol (362-459) and §Close Codes (423-431). Implements the client side of issue 3a-11. Cursor persistence lives in 3b-5.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:51Z","created_by":"James Lal","updated_at":"2026-05-19T04:07:35Z","closed_at":"2026-05-19T04:07:35Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.6.3","depends_on_id":"attn-nnj.5.11","type":"blocks","created_at":"2026-05-18T16:38:25Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.3","depends_on_id":"attn-nnj.6","type":"parent-child","created_at":"2026-05-18T16:33:50Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.3","depends_on_id":"attn-nnj.6.1","type":"blocks","created_at":"2026-05-18T16:36:07Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} +{"id":"attn-nnj.6.2","title":"Outbox processor (~/.attn/reviews/rooms/\u003croomId\u003e/outbox.jsonl)","description":"Persistent outbox that survives crashes. Each line in outbox.jsonl is a queued MailboxEnvelope plus state metadata (attempts, last_error, status: pending|in_flight|sent). Processor reads pending entries, mints a PoW token from the Phase 0a token pool, batches up to 32, POSTs to /v2/rooms/:roomId/envelopes, and on success marks them sent (compaction sweeps sent lines out periodically). Backoff on 429/507/ATTN_POW_*. EnvelopeId is deterministic (see crypto-spec) so retries are server-side idempotent.","acceptance_criteria":"- ~/.attn/reviews/rooms/\u003croomId\u003e/outbox.jsonl created on first send; append-only with periodic compaction of sent entries\n- Each line: serde JSON {envelope, attempts, last_error?, status, queued_at}\n- Processor task per room: read pending → mint PoW (use Phase 0a token pool) → batch up to 32 → POST → on 2xx mark sent\n- Backoff: 429 honors Retry-After header; 507 ATTN_ROOM_EVENT_CAP/STORAGE_FULL → exponential backoff with cap (e.g., 1s, 2s, 4s, 8s, 30s); ATTN_POW_* → mint fresh token immediately (token reuse failure)\n- Crash safety: status:in_flight entries are reset to pending on startup so they re-send; deterministic envelopeId guarantees server dedupe\n- Emits ReviewUpdate::EnvelopeSent on success and ReviewUpdate::SendFailed{retryable} on terminal failure\n- Tests: happy path, batch=32 boundary, retry on 429, retry on 507, PoW-replay recovery, crash mid-send dedupe","notes":"Spec: planning/collab/relay-spec.md §POST /v2/rooms/:roomId/envelopes (218-269). amendments.md decision #7 (batch cap 32, single PoW per request). crypto-spec.md §EnvelopeId (283-301) for deterministic ID. Phase 0a token pool is a prerequisite reference (assume exists or stub if not yet implemented).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:50Z","created_by":"James Lal","updated_at":"2026-05-19T02:29:35Z","closed_at":"2026-05-19T02:29:35Z","close_reason":"Implemented (3.8+round-10 retries); merged into collab; 267 Rust + 144 relay tests pass","dependencies":[{"issue_id":"attn-nnj.6.2","depends_on_id":"attn-nnj.1.7","type":"blocks","created_at":"2026-05-18T16:38:24Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.2","depends_on_id":"attn-nnj.5.7","type":"blocks","created_at":"2026-05-18T16:38:25Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.2","depends_on_id":"attn-nnj.6","type":"parent-child","created_at":"2026-05-18T16:33:49Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.2","depends_on_id":"attn-nnj.6.1","type":"blocks","created_at":"2026-05-18T16:36:06Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.6.1","title":"src/review/transport.rs scaffold + Transport trait","description":"Define the transport abstraction that all relay variants implement. src/review/ does not yet exist — create the module tree. Trait: Transport { async connect(roomId, deviceId) -\u003e Result\u003c()\u003e; async send_envelopes(Vec\u003cMailboxEnvelope\u003e) -\u003e Result\u003cSendReceipt\u003e; subscribe(after_seq: u64) -\u003e impl Stream\u003cItem=Result\u003cServerFrame\u003e\u003e }. Two implementations are planned: MailboxTransport (this phase, issues 3b-2..3b-6) and WebRTCTransport (Phase 4). The frontend never sees raw transport — only typed ReviewUpdate variants emitted by ReviewManager after decrypt+verify+import.","acceptance_criteria":"- src/review/mod.rs added to src/lib.rs (or main module tree)\n- src/review/transport.rs defines Transport trait with the signatures above\n- Concrete types: MailboxEnvelope (canonical bytes + metadata), SendReceipt {envelope_id, server_seq}, ServerFrame enum (Hello, Envelope, Presence, PolicyChanged, Ping, Error) matching the WS protocol from relay-spec.md\n- TransportError enum maps relay error codes (ATTN_ADMISSION_INVALID, ATTN_POW_*, ATTN_CURSOR_TOO_OLD, ATTN_ROOM_EXPIRED, ATTN_ROOM_DELETED, ATTN_RATE_LIMITED, ...) to typed Rust errors with retryable/non-retryable classification\n- A NoopTransport test impl for use in unit tests of ReviewManager\n- cargo check + cargo clippy clean (no any-equivalent — use proper types per repo conventions)\n- No frontend exposure: the trait lives behind ReviewManager, which is what emits ReviewUpdate","notes":"Spec: planning/collab/relay-spec.md §WebSocket Protocol (362-459) defines the frame shapes. planning/collab/data-model.md §Transport Model. Code conventions: TypeScript repo for the relay, but this Rust crate avoids 'any'/dyn-without-bounds equivalents. No backwards-compat shim with any prior transport — this is the new module from scratch.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:49Z","created_by":"James Lal","updated_at":"2026-05-19T01:56:27Z","closed_at":"2026-05-19T01:56:27Z","close_reason":"Implemented; merged into collab; 254 Rust tests + 130 relay tests pass; corpus replay green","dependencies":[{"issue_id":"attn-nnj.6.1","depends_on_id":"attn-nnj.1.9","type":"blocks","created_at":"2026-05-18T16:38:24Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.6.1","depends_on_id":"attn-nnj.6","type":"parent-child","created_at":"2026-05-18T16:33:48Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":7,"comment_count":0} +{"id":"attn-nnj.9.5","title":"CORS + browser allowlist on the relay","description":"Confirm relay-spec.md §Browser Considerations is implemented in the worker: Origin allowlist on WS upgrade requests (reject non-allowlisted with 403), Access-Control-Allow-Origin headers on HTTP responses (POST /envelopes etc.) pulled from ALLOWED_BROWSER_ORIGINS env var. Test from a non-allowlisted origin and confirm 403. Without this, the browser client cannot function due to same-origin policy.","acceptance_criteria":"WS upgrade handler reads ALLOWED_BROWSER_ORIGINS env var and rejects upgrades with non-allowlisted Origin via HTTP 403.\nHTTP endpoints (POST /envelopes, POST /devices, POST /acks, POST /blobs) emit Access-Control-Allow-Origin matching the request Origin if in allowlist.\nOPTIONS preflight is handled with the right Access-Control-Allow-Methods + Access-Control-Allow-Headers (including Attn-PoW, Attn-Owner-Signature).\nIntegration test in the conformance corpus: request from https://evil.example → 403; request from https://attn.dev → 200/204.\nALLOWED_BROWSER_ORIGINS documented in relay deployment notes.","notes":"Specs: planning/collab/relay-spec.md §Browser Considerations. Files: relay/src/cors.ts (or wherever the worker entry middleware lives). Origin must be a strict match, not a prefix or wildcard. Note: the Rust client doesn't send an Origin header, so a missing Origin should be permitted (Rust path) but a present-but-not-allowlisted Origin should be denied.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:45Z","created_by":"James Lal","updated_at":"2026-05-19T16:56:25Z","closed_at":"2026-05-19T16:56:25Z","close_reason":"Round 20: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.9.5","depends_on_id":"attn-nnj.5.7","type":"blocks","created_at":"2026-05-18T16:39:11Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.9.5","depends_on_id":"attn-nnj.9","type":"parent-child","created_at":"2026-05-18T16:33:44Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.9.4","title":"Browser review UI (subset of native UI)","description":"Reuse Phase 2 Svelte review components where possible — the snapshot viewer, comment threads, suggestion decorations. Reviewer-only surface: NO share button (browser cannot own a room), NO apply UI (browser cannot mutate the owner's working copy). Browser CAN add comments and suggestions; those get added to an in-memory outbox that uploads via POST /envelopes from the browser context (with admission HMAC + hashcash PoW).","acceptance_criteria":"Browser /review/:roomId page renders the latest snapshot and existing comment/suggestion threads.\nReviewer can add a comment anchored to a selection; comment uploads via POST /envelopes (hashcash mined off-thread in a Web Worker per amendments.md).\nReviewer can add a suggestion (text replacement) anchored to a range; same upload path.\nNO share button, NO apply button, NO 'create room' affordance in the browser UI.\nShared Svelte components from Phase 2 render identically to native (within visual-diff tolerance).\nBrowser-specific empty/error states for: invalid invite, expired room (close 4001), stale cursor (close 4005), failed admission (403).","notes":"Specs: planning/collab/amendments.md §Phase 6, planning/collab/relay-spec.md §Browser Considerations. Files: web/src/routes/review/[roomId]/ (new) + reuse web/src/lib/review/* components from Phase 2. Depends on: invite parsing (Phase 6 issue 2), WS client (Phase 6 issue 3), CORS configured (Phase 6 issue 5). PoW miner from Phase 0a should already exist as a Web Worker — reuse it.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:31Z","created_by":"James Lal","updated_at":"2026-05-19T17:57:56Z","closed_at":"2026-05-19T17:57:56Z","close_reason":"Round 22 (final): implemented; merged","dependencies":[{"issue_id":"attn-nnj.9.4","depends_on_id":"attn-nnj.9","type":"parent-child","created_at":"2026-05-18T16:33:31Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.9.4","depends_on_id":"attn-nnj.9.2","type":"blocks","created_at":"2026-05-18T16:36:14Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.9.4","depends_on_id":"attn-nnj.9.3","type":"blocks","created_at":"2026-05-18T16:36:15Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.9.4","depends_on_id":"attn-nnj.9.5","type":"blocks","created_at":"2026-05-18T16:36:16Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.9.3","title":"Browser WebSocket client + envelope import","description":"TS counterpart of the Phase 3b WebSocket client: connect to the relay's wss://.../v2/rooms/:roomId/ws, attach admission HMAC piggyback per relay-spec, receive hello + envelope frames, decrypt under eventKey/snapshotKey, signature-verify, import into an in-memory replica of the event log and snapshot graph. Uses whichever crypto path won the Phase 6 issue 1 decision (WASM or TS). Browser is reviewer-only — no owner-signing-key flows here.","acceptance_criteria":"WebSocket client connects with admission HMAC (matches the Rust client's behavior bit-for-bit on the test corpus).\nBackfill via hello + envelope frames per relay-spec.md §WebSocket (no GET /envelopes per decision #5).\nStale cursor → close code 4005 handled with a re-bootstrap path (re-fetch snapshot, replay from there).\nDecrypt → verify ORDER is correct (decrypt under eventKey FIRST, then signature-verify the plaintext per crypto-spec.md).\nIn-memory store survives WS disconnect+reconnect without duplicate events (EventId dedupe).\nEnd-to-end test: a comment added on the Rust client appears in the browser within 1s.","notes":"Specs: planning/collab/relay-spec.md §WebSocket + §Signaling, planning/collab/crypto-spec.md §Envelope Format, planning/collab/amendments.md §Decision #5 (WebSocket-only). Files: web/src/lib/review/transport.ts (new). Depends on browser crypto path from Phase 6 issue 1 and on invite/key derivation from Phase 6 issue 2. NO PERSISTENCE — everything in-memory (decision #13).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:17Z","created_by":"James Lal","updated_at":"2026-05-19T17:35:47Z","closed_at":"2026-05-19T17:35:47Z","close_reason":"Round 21 (final push): implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.9.3","depends_on_id":"attn-nnj.5.11","type":"blocks","created_at":"2026-05-18T16:38:32Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.9.3","depends_on_id":"attn-nnj.5.6","type":"blocks","created_at":"2026-05-18T16:38:33Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.9.3","depends_on_id":"attn-nnj.5.7","type":"blocks","created_at":"2026-05-18T16:38:33Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.9.3","depends_on_id":"attn-nnj.9","type":"parent-child","created_at":"2026-05-18T16:33:16Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.9.3","depends_on_id":"attn-nnj.9.1","type":"blocks","created_at":"2026-05-18T16:36:13Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.9.3","depends_on_id":"attn-nnj.9.2","type":"blocks","created_at":"2026-05-18T16:36:14Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":5,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.5.15","title":"Test plan acceptance suite (Miniflare integration tests)","description":"Implement the 14 numbered test scenarios from relay-spec.md §Test Plan as vitest integration tests under relay/test/integration/. Each test boots Miniflare (programmatic API), exercises the endpoints, and asserts on response codes, error codes, DO storage state (via wrapped get/list), and R2 contents. Consumes the conformance corpus from issue 3a-14 where applicable but adds in-depth assertions Miniflare can introspect.","acceptance_criteria":"- relay/test/integration/*.test.ts mirrors the 14 scenarios from §Test Plan\n- Each test boots a fresh Miniflare instance per case (or per file) so state is isolated\n- Asserts on: HTTP status, error code in body, DO storage keys via Miniflare's getDurableObjectStorage, R2 keys via getR2Bucket\n- Uses fake timers / Miniflare's setCurrentTime to test TTL alarms deterministically\n- Runs in CI via npm test (relay package) — green required before merge\n- Covers: room create+idempotency, device register+conflict, envelope ingest+caps, batch cap=32, WS backfill happy path, WS backfill 4005 path, hibernation roundtrip, owner-ack+delete, anonymous-ack no-delete, DELETE room, idle expiry, hard-max expiry, longSession 7d, rate-limit per-IP+per-device+anti-enum","notes":"Spec: planning/collab/relay-spec.md §Test Plan (687-705). Uses conformance corpus from 3a-14 as the source of request/response pairs but is the canonical pass/fail gate for the relay.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:08Z","created_by":"James Lal","updated_at":"2026-05-19T16:55:51Z","closed_at":"2026-05-19T16:55:51Z","close_reason":"Round 20: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.5.15","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:33:07Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.15","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:31Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.15","depends_on_id":"attn-nnj.5.14","type":"blocks","created_at":"2026-05-18T16:35:59Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.5.14","title":"Conformance corpus (relay/test/conformance/cases.json)","description":"Build a shared conformance corpus consumed by BOTH the Miniflare integration suite (Phase 3a issue 15) AND the Rust transport tests (Phase 3b issue 7). Each case is a full {request, expectedResponse, expectedSideEffects} record covering the entire HTTP+WS surface and every error code in the spec.","acceptance_criteria":"- relay/test/conformance/cases.json (or one file per category) with deterministic fixtures (fixed keys, fixed timestamps in mock clock)\n- Coverage: room lifecycle (create idempotent, create policy-conflict, delete), WS backfill (after=0 full replay, after=last no replay, after=deleted→4005 with resyncFromSeq), all caps (32 batch, maxEvents, maxRoomBytes, maxEventBytes, maxSnapshotBytes, signal sub-cap eviction), owner-only ops (ack+delete with/without sig, DELETE room), PoW failures (insufficient bits, expired, resource mismatch, replayed), hibernation roundtrip (write → eject DO → reconnect → backfill), rate limits (per-IP, per-device, anti-enum), longSession clamping\n- A loader/runner abstraction in TypeScript that interprets cases and executes them against any HTTP+WS target (Miniflare or live wrangler dev)\n- Same JSON loadable from Rust via serde (matching schema documented at the top of the file)\n- README explaining how to add a new case","notes":"Spec source of truth: planning/collab/relay-spec.md §Test Plan (687-705) lists 14 scenarios — these are the minimum coverage. crypto-spec.md §Test Vectors (421-433) for PoW vectors. Phase 3b issue 7 will run this same corpus from Rust against wrangler dev --local. Plan the file format to be serde-deserializable from the start.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:07Z","created_by":"James Lal","updated_at":"2026-05-19T15:05:43Z","closed_at":"2026-05-19T15:05:43Z","close_reason":"Implemented; merged; 412 Rust + 213 relay tests pass (6 conformance scenarios deferred to 5.16)","dependencies":[{"issue_id":"attn-nnj.5.14","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:33:07Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.14","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:30Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} +{"id":"attn-nnj.5.13","title":"Rate limiting (per-IP, per-device, anti-enumeration)","description":"Two-tier rate limiting. Worker edge (before DO): per-IP 600/min sliding window using Cloudflare's rate-limiting binding or a KV-backed sliding window, and an anti-enumeration counter that returns 429 after 30 unknown rooms in 5 minutes from one IP. Per-device 120/min in DO storage (sliding window over rate:\u003cdeviceId\u003e:\u003cbucket\u003e) checked after admission. All 429 responses include retryAfterMs in body and Retry-After header.","acceptance_criteria":"- Edge per-IP: 600 requests/min sliding window across all rooms; over → 429 ATTN_RATE_LIMITED with retryAfterMs\n- Edge anti-enum: 30 unknown rooms (404 ATTN_ROOM_NOT_FOUND triggers) from one IP in 5 min → 429 ATTN_ANTI_ENUMERATION; the unknown-room counter is keyed by IP, not roomId, so the attacker cannot tell which roomId tripped it\n- DO per-device: 120 requests/min sliding window over rate:\u003cdeviceId\u003e:\u003cminute-bucket\u003e entries; over → 429 ATTN_RATE_LIMITED\n- 429 responses set Retry-After header (seconds, rounded up) AND body {error:{code, retryAfterMs}}\n- Edge limits checked before the request crosses to the DO (cost protection)\n- Per-device limits checked after admission so they're attributable\n- Tests: per-IP cap, per-device cap, anti-enum trip with mixed unknown rooms, Retry-After format","notes":"Spec: planning/collab/relay-spec.md §Anti-Abuse (565-571). Numbers come from amendments and user's brief (600/min per IP, 120/min per device, 30 unknown/5min). Implement edge limits in src/index.ts before the DO fetch, device limits inside the DO.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:06Z","created_by":"James Lal","updated_at":"2026-05-19T15:27:48Z","closed_at":"2026-05-19T15:27:48Z","close_reason":"Round 17: implemented; merged; 409 Rust + 237 relay tests pass","dependencies":[{"issue_id":"attn-nnj.5.13","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:33:06Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.13","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:30Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.13","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:49Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.13","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:49Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.9.2","title":"Browser invite URL parsing + memory-only secret handling","description":"At https://attn.dev/review/:roomId#key=... — on load, parse the fragment, IMMEDIATELY strip it from the visible URL via history.replaceState(null, '', location.pathname + location.search), and hold roomSecret only in JS heap memory. Derive rootKey then derive the subkeys (eventKey, snapshotKey, signalingKey, admissionKey). Zero/overwrite the original fragment string where possible. Reload requires re-paste — no sessionStorage, no IndexedDB, no cookies (decision #13). This is the entire trust-on-browser story.","acceptance_criteria":"On page load, location.hash is parsed exactly once and immediately stripped via history.replaceState.\nroomSecret never written to sessionStorage, localStorage, IndexedDB, cookies, or service-worker caches — verify via grep + manual audit.\nrootKey + subkeys derived synchronously after fragment parse; derivation matches crypto-spec.md.\nAfter fragment strip, location.href shows the bare URL with no #key= visible (e.g., to the page title, devtools history list, or any other observer).\nReload reproduces the 'paste invite to join' UX rather than silently rejoining.\nUnit test (jsdom or playwright) asserts replaceState fires before any other code accesses location.hash a second time.","notes":"Specs: planning/collab/amendments.md §Decision #13, planning/collab/crypto-spec.md §Invite URLs + §Key Derivation. Files: web/src/lib/review/invite.ts (new), web/src/routes/review/[roomId]/+page.svelte (or similar). The strip-fragment-before-anything-else ordering matters — see Phase 6 issue in cross-cutting security review for the race-window concern. Zeroize the fragment string in JS is best-effort (string immutability in JS limits us); the goal is no PERSISTENT trace.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:33:01Z","created_by":"James Lal","updated_at":"2026-05-19T16:06:19Z","closed_at":"2026-05-19T16:06:19Z","close_reason":"Round 18: implemented; merged; 414+ Rust tests pass","dependencies":[{"issue_id":"attn-nnj.9.2","depends_on_id":"attn-nnj.9","type":"parent-child","created_at":"2026-05-18T16:33:00Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0} +{"id":"attn-nnj.9.1","title":"Browser crypto sourcing decision (WASM vs TS)","description":"Real architectural fork: compile attn-collab-crypto Rust crate to WASM (one source of truth, larger bundle, identical behavior with Rust client) vs hand-write TS crypto against the shared test-vector corpus (smaller bundle, two implementations with drift risk). Bundle-size budget is the hard constraint — target the hosted JS bundle under 500 KiB gzipped. Output a decision doc with the recommendation and the measurements that back it. This issue blocks all browser WS/crypto work in Phase 6, so resolve it early.","acceptance_criteria":"planning/collab/ui/browser-crypto-decision.md created with both options spelled out and bundle-size measurements.\nWASM path: build attn-collab-crypto with wasm-pack (or wasm-bindgen), measure brotli/gzip size, note startup cost.\nTS path: scoped libraries identified (e.g., @noble/ciphers for XChaCha20-Poly1305, @noble/ed25519, @noble/hashes for HKDF-SHA256), estimate gzipped bundle size for the subset used.\nRecommendation includes a concrete number for the resulting bundle in both cases and a winner.\nFlagged with bd human — owner sign-off needed before downstream Phase 6 issues unblock.","notes":"Specs: planning/collab/crypto-spec.md §Primitives (XChaCha20-Poly1305 + Ed25519 + HKDF-SHA256 + canonical JSON RFC 8785 + base64url-no-pad), planning/collab/amendments.md (Decision #4 cipher locked, Decision #13 browser memory-only). Test-vector corpus must validate whichever path is chosen — the corpus is shared, not Rust-specific. If TS path: @noble/* libs are audited and tree-shake well.","status":"closed","priority":1,"issue_type":"decision","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:32:30Z","created_by":"James Lal","updated_at":"2026-05-19T16:25:13Z","closed_at":"2026-05-19T16:25:13Z","close_reason":"Round 19: implemented; merged; build clean","labels":["human"],"dependencies":[{"issue_id":"attn-nnj.9.1","depends_on_id":"attn-nnj.9","type":"parent-child","created_at":"2026-05-18T16:32:30Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.5.12","title":"Alarms: idle + hard-max TTL + pow-prune","description":"DO alarms drive room TTL cleanup. Cloudflare DO supports only one pending alarm at a time, so always schedule at min(hard_max_at, last_event_at + idleTimeoutMs, next_pow_prune_at). On fire, determine which deadline tripped and act: idle/hard-max → close all WS with 4002 (room expired), wipe DO storage, schedule R2 cleanup; pow-prune → delete pow_seen entries past expiresAt+10min. Every WS connect calls cleanup_check() if now is within 1h of expires_at (decision #9). Hard-max defaults: 24h, or 7d when policy.longSession=true (decision #8). Idle default: 1h.","acceptance_criteria":"- alarm() handler reads all candidate deadlines (hard_max_at, last_event_at+idleTimeoutMs, pow_prune_at) and reschedules at the next earliest after acting on any that have fired\n- Idle/hard-max fire: broadcast close 4002 with reason 'room_expired', deleteAll() DO storage, enqueue R2 prefix delete, set tombstone so further requests 410 ATTN_ROOM_EXPIRED for 24h\n- pow-prune fire: scan meta:pow_seen:* and delete entries where extracted expiresAt+10min \u003c now\n- POST /envelopes updates last_event_at and reschedules the alarm\n- Every WS upgrade calls cleanup_check() — if now within 1h of expires_at, run the same scan and reschedule\n- hard_max_at computed at room creation: created + (longSession ? 7d : 24h), clamped by policy.expiresAt\n- Tests: idle expiry path, hard-max expiry path, longSession 7-day cap, single-alarm scheduling correctness when multiple deadlines compete, cleanup_check on WS connect, pow_seen prune","notes":"Spec: planning/collab/relay-spec.md §Alarms (514-529), §Close Codes (423-431). amendments.md decisions #8 (TTL defaults + longSession) and #9 (R2 lifecycle as safety net, DO alarm primary, cleanup_check on WS connect). Coordinates with PoW replay protection from 3a-3 and WS close from 3a-11.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:32:29Z","created_by":"James Lal","updated_at":"2026-05-19T13:49:59Z","closed_at":"2026-05-19T13:49:59Z","close_reason":"Implemented (partially for 5.14 — scaffold + skip-on-empty, follow-up to fill cases.json); 372 Rust + 184 relay tests pass","dependencies":[{"issue_id":"attn-nnj.5.12","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:32:28Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.12","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:29Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.12","depends_on_id":"attn-nnj.5.11","type":"blocks","created_at":"2026-05-18T16:35:58Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.12","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:47Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.12","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:48Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.5.11","title":"WebSocket protocol + DO hibernation","description":"Implement the WebSocket transport with subprotocol 'attn.v2' and admission HMAC piggybacked as the second protocol value. Use state.acceptWebSocket() for hibernation so idle sessions don't burn DO CPU; tag attached sockets with [deviceId, participantId]. Server frames: hello, envelope, presence, policy_changed, ping, error. Client frames: subscribe, pong. On subscribe.after \u003c meta:oldest_retained_seq emit error{code:ATTN_CURSOR_TOO_OLD, resyncFromSeq} and close with code 4005. 30s ping interval, 60s pong timeout → close 1001.","acceptance_criteria":"- Upgrade handshake: subprotocol header must include 'attn.v2' and a second value carrying the admission HMAC; missing/invalid → 401 ATTN_ADMISSION_INVALID\n- Uses state.acceptWebSocket(ws, [deviceId, participantId]) for hibernation; webSocketMessage/webSocketClose handlers route by tag\n- Backfill on subscribe: replay envelope frames from storage where seq \u003e subscribe.after (decision #5 — no GET /envelopes endpoint)\n- On subscribe.after \u003c meta:oldest_retained_seq → send error{code:ATTN_CURSOR_TOO_OLD, resyncFromSeq:meta:oldest_retained_seq} then close 4005\n- Server frames implemented: hello{serverSeq, oldestRetainedSeq, peers}, envelope{seq, envelope}, presence{deviceId, state}, policy_changed{policy}, ping, error\n- Client frames handled: subscribe{after}, pong\n- 30s server ping interval (per-session scheduled via alarm or setTimeout-substitute); no pong within 60s → close 1001 ATTN_TIMEOUT\n- Broadcast helper used by POST /envelopes routes through getSession-by-tag lookup\n- Tests: handshake reject, backfill from 0, backfill from mid, 4005 on too-old cursor, hibernation roundtrip (eject + re-deliver after restart), ping/pong, presence broadcast","notes":"Spec: planning/collab/relay-spec.md §WebSocket Protocol (362-459), §Hibernation Tags (531-533). amendments.md decision #5 (WS-only delivery, GET /envelopes REMOVED). Uses admission middleware from 3a-2.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:32:28Z","created_by":"James Lal","updated_at":"2026-05-19T04:06:15Z","closed_at":"2026-05-19T04:06:15Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.5.11","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:32:28Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.11","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:29Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.11","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:46Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.11","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:47Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":3,"comment_count":0} +{"id":"attn-nnj.5.10","title":"DELETE /v2/rooms/:roomId — owner room wipe","description":"Owner-only endpoint to nuke the room. Requires both Attn-PoW and Attn-Owner-Signature. Disconnects all WebSocket sessions with close code 4001 (room deleted), wipes all DO storage for the room, and schedules R2 cleanup by listing and deleting all objects under rooms/\u003croomId\u003e/.","acceptance_criteria":"- Requires valid Attn-Admission, Attn-PoW, AND Attn-Owner-Signature; missing/invalid owner sig → 403 ATTN_OWNER_SIG_REQUIRED / ATTN_OWNER_KEY_MISMATCH\n- Closes every active WebSocket with close code 4001, reason 'room_deleted'\n- Wipes all DO storage keys (envelope:*, acks:*, devices:*, meta:*, pow_seen:*) via storage.deleteAll() or scoped delete\n- Schedules R2 cleanup: list objects with prefix rooms/\u003croomId\u003e/ and delete in batches (paginate)\n- Returns 200 {deleted:true} synchronously even if R2 cleanup is still draining\n- Idempotent: re-DELETE on already-gone room returns 404 ATTN_ROOM_NOT_FOUND\n- Test: WS sessions observe 4001 close before storage wipe; subsequent admission attempts return 404","notes":"Spec: planning/collab/relay-spec.md §DELETE /v2/rooms/:roomId (305-311), §Close Codes (423-431). amendments.md decision #3 (owner-only). Owner-sig from issue 3a-4.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:32:27Z","created_by":"James Lal","updated_at":"2026-05-19T04:29:44Z","closed_at":"2026-05-19T04:29:44Z","close_reason":"Implemented; merged","dependencies":[{"issue_id":"attn-nnj.5.10","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:32:26Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.10","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:28Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.10","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:45Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.10","depends_on_id":"attn-nnj.5.3","type":"blocks","created_at":"2026-05-18T16:35:58Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.10","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:46Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.7.7","title":"WebRTC e2e integration test","description":"Boot two local attn daemons in a test harness, share a doc from one, join from the other, exchange comments over the DataChannel. Confirms decision #1's Rust-owned WebRTC path actually works in wry/tao on macOS. Use the daemon's existing automation flags (--eval, --query, --wait-for) to drive both sides without needing a separate test runner. This is the integration test that proves Phase 4 has actually shipped.","acceptance_criteria":"scripts/test-e2e.sh gains a review/webrtc.sh (or equivalent) that boots two daemon instances on isolated state dirs.\nOwner shares a markdown fixture; reviewer joins via the invite URL (attn://review/...).\nA comment from the reviewer arrives on the owner side within 2s, asserted via --query on the owner's review panel.\nDataChannel connection state is asserted Connected on both sides via --eval against window.__attn__.reviewState().\nTest passes in CI on macOS (and Linux if relay tests already run there).\nCaptures connection logs to /tmp/attn-e2e-screenshots/ on failure.","notes":"Specs: planning/collab/amendments.md §existing automation flags affect ReviewManager design + §Phase 4. Files: scripts/test-e2e.sh + new tests/fixtures/review/*.md. Two daemons on the same machine need different ATTN_HOME (or equivalent) and different socket paths — see existing single-instance protocol in src/daemon.rs. Window.__attn__.reviewState() should be exposed earlier in Phase 2/3 — verify it exists before writing this test.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:32:15Z","created_by":"James Lal","updated_at":"2026-05-19T16:07:08Z","closed_at":"2026-05-19T16:07:08Z","close_reason":"Round 18: implemented; merged; 414+ Rust tests pass","dependencies":[{"issue_id":"attn-nnj.7.7","depends_on_id":"attn-nnj.2.10","type":"blocks","created_at":"2026-05-18T16:58:50Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.7.7","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-18T16:32:15Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.7.7","depends_on_id":"attn-nnj.7.1","type":"blocks","created_at":"2026-05-18T16:35:53Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.7.7","depends_on_id":"attn-nnj.7.3","type":"blocks","created_at":"2026-05-18T16:36:03Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.7.5","title":"Mode-aware transport selector in ReviewManager","description":"In ReviewManager, route outbound events based on policy.mode: hybrid sends through BOTH transports (DataChannel when connected, mailbox always-on); live uses WebRTC only; async uses mailbox only. Inbound dedupe is handled by the existing EventId-keyed import so the receiver doesn't double-process when both transports deliver. This is the integration point where mailbox + WebRTC become 'one transport with two wires' from the manager's perspective.","acceptance_criteria":"ReviewManager has a transport selector that consults policy.mode at send time.\nhybrid: outbound envelope is enqueued to BOTH transports; receiver dedupes by EventId on import.\nlive: outbound goes only to WebRTC; if WebRTC is Failed, send returns an error that surfaces as RoomStatusChanged(DirectFailed) (per issue 4).\nasync: outbound goes only to mailbox; WebRTC never initialized.\nInbound import is idempotent — receiving the same EventId twice (once from each transport) is a no-op the second time.\nTested with all three mode values.","notes":"Specs: planning/collab/amendments.md §Phase 4 (mode semantics + dedupe). Files: src/review/manager.rs. Idempotent import probably already exists from Phase 0b/3b store work — verify and reuse. Don't add EventId-tracking state here; the store layer owns dedupe.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:49Z","created_by":"James Lal","updated_at":"2026-05-19T14:20:07Z","closed_at":"2026-05-19T14:20:07Z","close_reason":"Implemented; 400 Rust + 193 relay tests pass","dependencies":[{"issue_id":"attn-nnj.7.5","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-18T16:31:48Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.7.5","depends_on_id":"attn-nnj.7.1","type":"blocks","created_at":"2026-05-18T16:35:52Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.7.5","depends_on_id":"attn-nnj.7.3","type":"blocks","created_at":"2026-05-18T16:36:02Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.7.5","depends_on_id":"attn-nnj.7.4","type":"blocks","created_at":"2026-05-18T16:36:03Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.5.9","title":"POST /v2/rooms/:roomId/acks — acknowledgment + optional delete","description":"Mark envelopes as ACKed by deviceId. Body: {acks: [{envelopeId, deviceId}], delete?:boolean}. Idempotent. If delete=true AND policy.deleteEventsAfterOwnerAck==true AND the request carries a valid Attn-Owner-Signature, delete envelopes that have been ACKed by ANY owner-kind device. Otherwise just record the ACK without deletion (multi-device safety per decision #12).","acceptance_criteria":"- Validates body schema; deviceId must be registered in the room\n- Records ACK in DO storage under acks:\u003cenvelopeId\u003e:\u003cdeviceId\u003e\n- delete=true is conditional on: policy.deleteEventsAfterOwnerAck===true (default false per decision #12) AND owner signature verifies AND envelope has at least one owner-device ACK\n- Deletion is by-envelope: removes envelope:\u003cseq\u003e entries (and blob R2 keys for snapshot_blob), decrements meta:envelope_count and meta:bytes_used\n- Updates meta:oldest_retained_seq if a leading run of envelopes is deleted\n- Idempotent: re-ACK is a no-op; re-delete of already-deleted envelope returns 200 (count: 0)\n- Returns {acked:[envelopeId...], deleted:[envelopeId...]}\n- Tests: ack-only, ack+delete with owner sig, ack+delete without owner sig (no-op deletion), ack+delete with policy.deleteEventsAfterOwnerAck=false (no-op deletion)","notes":"Spec: planning/collab/relay-spec.md §POST /v2/rooms/:roomId/acks (277-303). amendments.md decisions #3 (owner-sig gating) and #12 (deleteEventsAfterOwnerAck default false). Owner-sig verification reused from issue 3a-4.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:48Z","created_by":"James Lal","updated_at":"2026-05-19T04:08:01Z","closed_at":"2026-05-19T04:08:01Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.5.9","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:31:47Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.9","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:27Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.9","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:44Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.9","depends_on_id":"attn-nnj.5.3","type":"blocks","created_at":"2026-05-18T16:35:57Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.9","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:44Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.5.8","title":"POST + GET /v2/rooms/:roomId/blobs — R2 spillover","description":"When kind==snapshot_blob and ciphertextBytes \u003e 1 MiB, /envelopes is rejected and the client must use the blob flow. POST /v2/rooms/:roomId/blobs returns a presigned PUT URL (15-min TTL) at R2 key rooms/\u003croomId\u003e/blobs/\u003cenvelopeId\u003e. Client uploads ciphertext directly, then re-POSTs /envelopes with the same envelopeId and a small BlobRef payload referencing the R2 key. GET /v2/rooms/:roomId/blobs/:envelopeId returns a 5-min presigned GET URL.","acceptance_criteria":"- POST /blobs body validates {envelopeId, kind:'snapshot_blob', ciphertextBytes, hash}; rejects when ciphertextBytes ≤ 1 MiB (use /envelopes path instead)\n- Returns {uploadUrl, expiresAt} — presigned PUT, 15-min TTL, key=rooms/\u003croomId\u003e/blobs/\u003cenvelopeId\u003e\n- After upload, client re-POSTs /envelopes with kind=snapshot_blob and ciphertext = canonical BlobRef payload ({r2Key, ciphertextBytes, hash})\n- GET /blobs/:envelopeId returns {downloadUrl, expiresAt} — presigned GET, 5-min TTL\n- Validates envelopeId belongs to this room (check DO state) before issuing GET URL\n- Counts blob bytes against meta:bytes_used (against policy.maxRoomBytes)\n- Tests: upload roundtrip, undersized rejection, replay (same envelopeId returns same key), unauthorized GET","notes":"Spec: planning/collab/relay-spec.md §POST /v2/rooms/:roomId/blobs (313-360), §R2 Integration (557-563). Lifecycle TTL = 7 days as safety net (decision #9); primary cleanup is the DO alarm in issue 3a-12.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:47Z","created_by":"James Lal","updated_at":"2026-05-19T14:19:52Z","closed_at":"2026-05-19T14:19:52Z","close_reason":"Implemented; 400 Rust + 193 relay tests pass","dependencies":[{"issue_id":"attn-nnj.5.8","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:31:47Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.8","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:27Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.8","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:42Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.8","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:43Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.5.7","title":"POST /v2/rooms/:roomId/envelopes — batched ingest","description":"Accept up to 32 envelopes per HTTP request (decision #7). Each envelope is validated end-to-end: ciphertextBytes equals base64url-decoded ciphertext length; size against policy.max{Event|Snapshot}Bytes by kind; deviceId and authorId are registered in the room. Single PoW token covers the whole batch. On any cap overflow (envelope_count \u003e policy.maxEvents OR bytes_used + delta \u003e policy.maxRoomBytes) the whole batch fails with 507. Idempotent on envelopeId. Signal envelopes use sub-caps with FIFO eviction. On success, allocates serverSeq, updates counters, reschedules idle alarm.","acceptance_criteria":"- Batch cap 32: 33+ envelopes → 400 ATTN_BATCH_TOO_LARGE before any work\n- Single Attn-PoW token verified once for the whole HTTP request (resource binds METHOD + PATH, not per-envelope)\n- Per envelope: ciphertextBytes === base64url_decoded.length else 400 ATTN_ENVELOPE_SIZE_MISMATCH; kind-specific size cap against policy.max{Event|Snapshot}Bytes else 413 ATTN_ENVELOPE_TOO_LARGE; deviceId + authorId registered else 403 ATTN_UNKNOWN_DEVICE\n- serverSeq allocated atomically per envelope (monotonic, per relay-spec §serverSeq Allocation)\n- Idempotency: existing envelopeId returns its stored serverSeq with no state change\n- Whole-batch overflow: 507 ATTN_ROOM_EVENT_CAP or ATTN_ROOM_STORAGE_FULL (no partial commit)\n- Signal envelopes: maxSignalEnvelopes=64 per (authorId, target.deviceId), FIFO-evict oldest in DO storage\n- Updates meta:envelope_count, meta:bytes_used, meta:last_event_at; reschedules idle alarm\n- Broadcasts envelope frames to subscribed WS sessions (hibernation-safe)\n- Response: {accepted:[{envelopeId, serverSeq}], serverSeq:\u003cmax\u003e}\n- Tests: batch over cap, mixed kinds, idempotent retry, room-full, signal eviction, PoW reuse across batch","notes":"Spec: planning/collab/relay-spec.md §POST /v2/rooms/:roomId/envelopes (218-269), §serverSeq Allocation (499-512), §Caps (535-555). amendments.md decisions #7 (batch cap + single PoW) and #5 (WS-only delivery: this endpoint feeds the WS broadcast, no HTTP GET pull).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:46Z","created_by":"James Lal","updated_at":"2026-05-19T02:29:21Z","closed_at":"2026-05-19T02:29:21Z","close_reason":"Implemented (3.8+round-10 retries); merged into collab; 267 Rust + 144 relay tests pass","dependencies":[{"issue_id":"attn-nnj.5.7","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:31:46Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.7","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:26Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.7","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:41Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.7","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:42Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":3,"comment_count":0} +{"id":"attn-nnj.7.4","title":"Connection state machine + ICE handling","description":"Trickle ICE per relay-spec.md §Signaling. Track the peer-connection lifecycle through Connecting / Connected / Reconnecting / Failed states. Behavior on Failed is mode-dependent: in live mode, surface direct-connection failure explicitly via ReviewUpdate::RoomStatusChanged(DirectFailed) so the UI can tell the user the live channel is gone; in hybrid mode, silently rely on mailbox (no user-visible disruption — that's the whole point of hybrid). Reconnecting handles transient NAT rebinds without bouncing the user.","acceptance_criteria":"Connection state enum {Connecting, Connected, Reconnecting, Failed} drives WebRTC peer lifecycle.\nTrickle ICE: candidates emit as individual kind=signal envelopes as they're gathered (not batched at end-of-gathering).\nOn Failed in live mode → emit ReviewUpdate::RoomStatusChanged(DirectFailed). Frontend can surface 'live connection lost' UI.\nOn Failed in hybrid mode → no user-visible event; mailbox continues serving traffic.\nReconnecting attempts ICE restart before transitioning to Failed.\nState transitions covered by unit tests against a mock PeerConnection.","notes":"Specs: planning/collab/relay-spec.md §Signaling (trickle ICE protocol), planning/collab/amendments.md §Phase 4 (mode semantics). Files: src/review/transport/webrtc.rs (state machine), src/review/manager.rs (ReviewUpdate emission). Mode comes from policy.mode on the room. RoomStatusChanged is a new ReviewUpdate variant — add to the enum.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:35Z","created_by":"James Lal","updated_at":"2026-05-19T13:50:43Z","closed_at":"2026-05-19T13:50:43Z","close_reason":"Implemented (partially for 5.14 — scaffold + skip-on-empty, follow-up to fill cases.json); 372 Rust + 184 relay tests pass","dependencies":[{"issue_id":"attn-nnj.7.4","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-18T16:31:35Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.7.4","depends_on_id":"attn-nnj.7.1","type":"blocks","created_at":"2026-05-18T16:35:51Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.7.4","depends_on_id":"attn-nnj.7.2","type":"blocks","created_at":"2026-05-18T16:36:01Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.7.4","depends_on_id":"attn-nnj.7.3","type":"blocks","created_at":"2026-05-18T16:36:01Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.7.3","title":"DataChannel arm of Transport trait","description":"Implement WebRTCTransport against the Transport trait defined in Phase 3b. The crucial property (decision #14, amendments.md §Phase 4): the DataChannel envelope FORMAT is identical to mailbox envelopes — same AEAD under eventKey/snapshotKey, same routing, same import path. Only the wire differs. Frontend never sees raw transport; ReviewManager decrypts, signature-verifies, then emits typed ReviewUpdate. This is what lets hybrid mode dedupe by EventId across both transports for free.","acceptance_criteria":"WebRTCTransport struct implements the same Transport trait as MailboxTransport from Phase 3b.\nOutbound: events go through the existing outbox; when DataChannel is connected, send_envelope writes the same AEAD-encrypted envelope bytes to the channel.\nInbound: DataChannel on_message decrypts under eventKey/snapshotKey and feeds into the same envelope-import pipeline.\nNo plaintext on the wire (decision #14) — snapshot bytes are application-encrypted, never relying on DTLS for confidentiality.\nTrait abstraction allows a single ReviewManager codepath to consume both transports interchangeably.","notes":"Specs: planning/collab/amendments.md §Phase 4 + Decision #14. Files: src/review/transport/webrtc.rs (new), src/review/transport.rs (trait). Reuses the import pipeline built in Phase 3b — do NOT duplicate decrypt/verify logic. The wire is different but the envelope is the same — this is load-bearing for hybrid mode.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:21Z","created_by":"James Lal","updated_at":"2026-05-19T04:30:20Z","closed_at":"2026-05-19T04:30:20Z","close_reason":"Implemented; merged","dependencies":[{"issue_id":"attn-nnj.7.3","depends_on_id":"attn-nnj.6.1","type":"blocks","created_at":"2026-05-18T16:38:28Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.7.3","depends_on_id":"attn-nnj.6.4","type":"blocks","created_at":"2026-05-18T16:38:29Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.7.3","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-18T16:31:20Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.7.3","depends_on_id":"attn-nnj.7.1","type":"blocks","created_at":"2026-05-18T16:35:51Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.7.3","depends_on_id":"attn-nnj.7.2","type":"blocks","created_at":"2026-05-18T16:36:00Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":4,"dependent_count":3,"comment_count":0} +{"id":"attn-nnj.5.6","title":"POST + GET /v2/rooms/:roomId/devices — device registration","description":"POST /devices: upsert device record. Verifies selfSignature (Ed25519 over canonical device bytes) against publicSigningKey. If kind==owner, publicSigningKey MUST equal the room's ownerSigningKey, else 403 ATTN_OWNER_KEY_MISMATCH. Upsert is keyed by (participantId, deviceId); attempting to change publicSigningKey for an existing entry returns 409 ATTN_DEVICE_KEY_CHANGED. GET /devices returns the peer list in original registration order.","acceptance_criteria":"- POST validates schema: {participantId, deviceId, kind in [owner|reviewer|agent], publicSigningKey, selfSignature, capabilities?}\n- selfSignature verifies against publicSigningKey over the canonical device bytes from crypto-spec.md §Signing-Key Publication\n- kind==owner: publicSigningKey === room.ownerSigningKey else 403 ATTN_OWNER_KEY_MISMATCH\n- Upsert by (participantId, deviceId): if exists and key matches, return 200 with stored record; if key differs, 409 ATTN_DEVICE_KEY_CHANGED\n- GET /devices returns array in registration order; includes a server-stable 'registeredAt' timestamp\n- Updates meta:peer_count, enforces policy.maxPeers (8 cap) — 403 ATTN_ROOM_FULL when exceeded\n- Tests cover: fresh register, idempotent re-register, key-change rejection, owner-key mismatch, peer cap","notes":"Spec: planning/collab/relay-spec.md §POST /v2/rooms/:roomId/devices (169-217). crypto-spec.md §Signing-Key Publication (344-403). The devices list is the source of truth for sig verification on inbound envelopes (Phase 3b consumes this).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:08Z","created_by":"James Lal","updated_at":"2026-05-19T01:56:14Z","closed_at":"2026-05-19T01:56:14Z","close_reason":"Implemented; merged into collab; 254 Rust tests + 130 relay tests pass; corpus replay green","dependencies":[{"issue_id":"attn-nnj.5.6","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:31:07Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.6","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:26Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.6","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:40Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.6","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:40Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":2,"comment_count":0} +{"id":"attn-nnj.5.5","title":"POST /v2/rooms/:roomId — idempotent room creation","description":"Implement POST /v2/rooms/:roomId. Idempotent: first call creates the room with the supplied policy + ownerSigningKey, subsequent calls with the same body return the stored policy unchanged (no mutation). Body is validated against the zod schema from the scaffold issue. Policy values are clamped to spec maxima before storage. Computes and returns ownerSigningKeyId = base64url(SHA-256(ownerSigningKey)).","acceptance_criteria":"- Body validates against zod RoomCreateRequest schema (admissionKey, ownerSigningKey, policy, ...)\n- Policy clamping: maxPeers ≤ 8; expiresAt ≤ created+24h, or +7d when longSession=true; idleTimeoutMs ≥ 60_000 and ≤ wall-clock TTL; powBits ∈ [12,24]; max{Event,Snapshot,RoomBytes} ≤ HARD_MAX_*\n- Stores ownerSigningKey, computes ownerSigningKeyId = base64url(SHA-256(key))\n- First create: 201 with {policy, ownerSigningKeyId, serverSeq:0, oldestRetainedSeq:0}\n- Replay of same body: 200 with the stored values (no mutation, idempotent)\n- Different body for an existing room: 409 ATTN_ROOM_EXISTS_DIFFERENT_POLICY\n- Initial bytes_used=0, envelope_count=0, hard_max_at = created + min(policy.expiresAt - created, 24h|7d cap)\n- Test: clamping behavior, idempotency, conflict on policy diff","notes":"Spec: planning/collab/relay-spec.md §POST /v2/rooms/:roomId (lines 114-167). §Caps (535-555). amendments.md decision #8 (TTLs, longSession). Note: this endpoint does NOT require pre-existing admission since it establishes the admissionKey. PoW IS still required (decision #6).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:07Z","created_by":"James Lal","updated_at":"2026-05-19T01:33:37Z","closed_at":"2026-05-19T01:33:37Z","close_reason":"Implemented; merged into collab; 212 Rust tests + 98 relay tests pass","dependencies":[{"issue_id":"attn-nnj.5.5","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:31:06Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.5","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:25Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.5","depends_on_id":"attn-nnj.5.2","type":"blocks","created_at":"2026-05-18T16:35:39Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.5","depends_on_id":"attn-nnj.5.4","type":"blocks","created_at":"2026-05-18T16:35:39Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.7.2","title":"Encrypted signaling envelopes (signalingKey + AAD)","description":"Build the signaling layer for WebRTC negotiation. SDP offers/answers and ICE candidates ride the same mailbox transport as regular envelopes — same POST /envelopes upload, same WS delivery, just kind=signal. Cleartext payload is canonical({kind: offer|answer|ice, sdp|ice[], from: deviceId}), encrypted with signalingKey under standard envelope AAD. On the receive side, WS envelopes with kind=signal decrypt and dispatch into webrtc-rs as SDP/ICE inputs. This is what makes the data-channel handshake survive NAT without a separate signaling channel.","acceptance_criteria":"src/review/transport/signaling.rs (new) builds offer/answer/ice envelopes with kind=signal, encrypted under signalingKey with AAD binding.\nOutbound signaling envelopes upload via the existing POST /envelopes pipeline from Phase 3b — no new HTTP path.\nInbound WS envelope dispatcher routes kind=signal frames into the signaling decoder, decrypts, dispatches to webrtc-rs callbacks.\nUnit test: round-trip an offer/answer pair through encrypt → decrypt and assert canonical equality.\nsignalingKey derivation matches crypto-spec.md (HKDF subkey under rootKey).","notes":"Specs: planning/collab/relay-spec.md §Signaling, planning/collab/crypto-spec.md §Key Derivation, planning/collab/amendments.md §Phase 4. Files: src/review/transport/signaling.rs (new), src/review/transport.rs (dispatcher integration). Reuses outbox + WS envelope plumbing from Phase 3b. AAD must bind (roomId, envelopeId, kind=signal, from).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:31:02Z","created_by":"James Lal","updated_at":"2026-05-19T04:08:15Z","closed_at":"2026-05-19T04:08:15Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.7.2","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-18T16:31:02Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.7.2","depends_on_id":"attn-nnj.7.1","type":"blocks","created_at":"2026-05-18T16:35:50Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} +{"id":"attn-nnj.7.1","title":"webrtc-rs dependency + binary-size gate","description":"Add webrtc-rs to Cargo.toml as the foundation for Phase 4. Pre-merge gate: confirm release binary stays under 25 MiB target (decision #1 tradeoff). webrtc-rs transitively pulls tokio, rcgen, sctp, dtls, openssl-sys or rustls — this issue is the gate where we discover whether to feature-flag aggressively or escalate. Blocks all downstream Phase 4 work; if the gate fails we re-scope before sinking effort into signaling/datachannel code.","acceptance_criteria":"webrtc-rs added to Cargo.toml with chosen feature set documented inline.\ncargo build --release succeeds on macOS aarch64.\ncargo tree -e features --no-default-features --no-dev-dependencies output captured and committed under planning/collab/ or attached to issue notes.\ndu -h on the release .app bundle (or stripped binary if not bundled) is recorded; total under 25 MiB.\nIf gate exceeded: feature flags to evaluate are listed in a comment, OR issue is escalated via bd human and downstream work is held.","notes":"Spec: planning/collab/amendments.md §Phase 4 WebRTC + Decision #1. Files: Cargo.toml (root). Use rustls backend (not openssl-sys) by default to keep the binary smaller and avoid system-OpenSSL coupling. Verify gate via the existing scripts/build.sh release path so it matches what ships. Blocks: every other Phase 4 issue.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:46Z","created_by":"James Lal","updated_at":"2026-05-19T04:06:55Z","closed_at":"2026-05-19T04:06:55Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.7.1","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-18T16:30:45Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":6,"comment_count":0} +{"id":"attn-nnj.4.14","title":"'Comment survives owner edits' demo + e2e check","description":"End-to-end scripted scenario using the mock-ipc scenario stream: owner edits a paragraph, the reviewer's previously-attached comment remaps via the anchor engine, decoration stays attached, and the 'moved' badge state shows in the panel. Repeatable via existing automation flags (--query, --eval).","acceptance_criteria":"- Scenario JSON in web/src/lib/mock-ipc-scenarios/ drives the demo\n- Repeatable test invocation documented (uses attn --query / --eval)\n- Asserts: decoration present after edit, status is remapped (0.70-0.89) or exact (\u003e=0.90)\n- Asserts: 'moved' badge shown in panel when remapped\n- Wired into scripts/test-e2e.sh or a sibling script","notes":"Spec refs: amendments.md Decision #15 cutoffs; data-model.md §Anchor engine. Uses attn --query and --eval automation flags (debug builds only). Depends on 2-1 mock-ipc, 2-6 decorations. Aligns with project rule: prefer in-app UI assertions; no 'any' types.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:43Z","created_by":"James Lal","updated_at":"2026-05-19T17:35:33Z","closed_at":"2026-05-19T17:35:33Z","close_reason":"Round 21 (final push): implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.4.14","depends_on_id":"attn-nnj.11.4","type":"blocks","created_at":"2026-05-18T16:38:31Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.14","depends_on_id":"attn-nnj.3.5","type":"blocks","created_at":"2026-05-18T16:38:22Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.14","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:42Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.14","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:28Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.14","depends_on_id":"attn-nnj.4.6","type":"blocks","created_at":"2026-05-18T16:31:34Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.4.11","title":"Connection badge","description":"Header connection indicator with four states from data-model.md: Live direct / Mailbox / Offline / Direct failed. Subscribes to reviewStatus updates from the store. Direct-failed state surfaces a retry affordance.","acceptance_criteria":"- Connection badge present in header at location decided in UX-3\n- All four states render with distinct visual treatment\n- Subscribes to reviewStatus via review store\n- Direct-failed state includes retry affordance (button or click action)\n- Retry action calls appropriate IPC (mocked)","notes":"Spec refs: data-model.md §UI/UX Changes (owner connection badge: Live direct / Mailbox / Offline / Direct failed). Depends on UX-3, 2-1, 2-2. No 'any' types.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:41Z","created_by":"James Lal","updated_at":"2026-05-19T16:24:57Z","closed_at":"2026-05-19T16:24:57Z","close_reason":"Round 19: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.4.11","depends_on_id":"attn-nnj.10.1","type":"blocks","created_at":"2026-05-18T16:30:59Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.11","depends_on_id":"attn-nnj.10.2","type":"blocks","created_at":"2026-05-18T16:31:04Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.11","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:11Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.11","depends_on_id":"attn-nnj.12.1","type":"blocks","created_at":"2026-05-18T16:53:49Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.11","depends_on_id":"attn-nnj.12.7","type":"blocks","created_at":"2026-05-18T16:53:53Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.11","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:40Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.11","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:26Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.11","depends_on_id":"attn-nnj.4.2","type":"blocks","created_at":"2026-05-18T16:31:31Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":7,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.4.10","title":"Share button + room mode selector","description":"Toolbar share affordance opens a dialog with room mode (Live / Async 24h / Async 7d / Hybrid), displays the generated attn://review/... URL, and provides a copy button. Wires to IPC review_share (mocked in this phase via 2-1).","acceptance_criteria":"- Share button present in toolbar at location decided in UX-3\n- Dialog opens with mode selector: Live / Async 24h / Async 7d / Hybrid\n- Generated attn://review/... URL displayed\n- Copy-to-clipboard button works (uses in-app feedback, not alert())\n- Calls IPC review_share; mock-ipc returns a fake URL for now\n- Dialog is in-app modal (no window.confirm/prompt)","notes":"Spec refs: data-model.md §UI/UX Changes (owner share + room mode); UX-3 for placement. Depends on UX-3, 2-1 mock-ipc. No 'any' types. No window.confirm/alert/prompt — use in-app modal.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:39Z","created_by":"James Lal","updated_at":"2026-05-19T16:06:04Z","closed_at":"2026-05-19T16:06:04Z","close_reason":"Round 18: implemented; merged; 414+ Rust tests pass","dependencies":[{"issue_id":"attn-nnj.4.10","depends_on_id":"attn-nnj.10.1","type":"blocks","created_at":"2026-05-18T16:30:59Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.10","depends_on_id":"attn-nnj.10.2","type":"blocks","created_at":"2026-05-18T16:31:04Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.10","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:10Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.10","depends_on_id":"attn-nnj.12.1","type":"blocks","created_at":"2026-05-18T16:53:48Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.10","depends_on_id":"attn-nnj.12.5","type":"blocks","created_at":"2026-05-18T16:53:59Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.10","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:39Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.10","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:25Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":6,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.4.9","title":"Snapshot badge + status row in editor header","description":"Editor header status row showing snapshot state. Owner-side: 'Snapshot current' / 'Snapshot superseded' / 'Reviewer on older snapshot'. Reviewer-side: snapshot age (e.g., '3 min ago') + 'owner is on a newer snapshot' notice. Subscribes to reviewSnapshot updates from the store.","acceptance_criteria":"- Editor header includes snapshot badge row\n- Owner states implemented: current / superseded / reviewer-on-older\n- Reviewer states implemented: age display + newer-snapshot notice\n- Subscribes to reviewSnapshot updates from review store\n- Visual treatment matches planning/collab/ui/presence-identity.md and connection-share.md","notes":"Spec refs: data-model.md §UI/UX Changes (snapshot badge owner/reviewer); UX-5 (presence-identity) for reviewer-on-older treatment. Depends on UX-1, UX-3, 2-2 store. No 'any' types.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:39Z","created_by":"James Lal","updated_at":"2026-05-19T16:55:34Z","closed_at":"2026-05-19T16:55:34Z","close_reason":"Round 20: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.4.9","depends_on_id":"attn-nnj.10.1","type":"blocks","created_at":"2026-05-18T16:30:58Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.9","depends_on_id":"attn-nnj.10.2","type":"blocks","created_at":"2026-05-18T16:31:03Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.9","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:10Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.9","depends_on_id":"attn-nnj.10.5","type":"blocks","created_at":"2026-05-18T16:31:45Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.9","depends_on_id":"attn-nnj.12.1","type":"blocks","created_at":"2026-05-18T16:53:47Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.9","depends_on_id":"attn-nnj.12.7","type":"blocks","created_at":"2026-05-18T16:53:52Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.9","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:38Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.9","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:24Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":7,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.4.8","title":"Stale comment panel state","description":"When a ResolvedAnchor has status 'stale', the panel renders a status pill 'could not find this text anymore' and a 'Re-anchor manually' affordance that switches the editor into a select-text-in-editor mode; the next text selection re-anchors the comment.","acceptance_criteria":"- Stale-state card shows clear status pill and original anchor preview\n- 'Re-anchor manually' button enters editor select mode\n- Next editor selection re-anchors and exits select mode\n- Cancel/escape exits select mode without re-anchoring\n- Store updates resolution to exact after re-anchor","notes":"Spec refs: data-model.md §ResolvedAnchor status 'stale'; amendments.md Decision #15 ('stale → panel-only, requires manual re-anchor'). Depends on UX-1, 2-2, 2-3 panel. No 'any' types.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:38Z","created_by":"James Lal","updated_at":"2026-05-19T16:57:52Z","closed_at":"2026-05-19T16:57:52Z","close_reason":"Round 20: implemented (force; design-doc dep)","dependencies":[{"issue_id":"attn-nnj.4.8","depends_on_id":"attn-nnj.10.1","type":"blocks","created_at":"2026-05-18T16:30:58Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.8","depends_on_id":"attn-nnj.10.2","type":"blocks","created_at":"2026-05-18T16:31:03Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.8","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:09Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.8","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:37Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.8","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:24Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.8","depends_on_id":"attn-nnj.4.2","type":"blocks","created_at":"2026-05-18T16:31:30Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":5,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.4.7","title":"Ambiguous anchor picker","description":"When a ResolvedAnchor has status 'ambiguous', the review panel surfaces a picker listing each candidate with its preview text and confidence score. Owner clicks one candidate → emits AnchorManuallyResolved event via IPC, store updates, decoration moves to inline.","acceptance_criteria":"- Ambiguous-state card in panel shows candidate list with preview + confidence\n- Picker UI is keyboard-navigable (arrow keys + enter)\n- Selecting a candidate emits AnchorManuallyResolved via IPC\n- Store transitions the anchor to a non-ambiguous resolution after pick\n- Decoration moves from panel-only to inline after pick","notes":"Spec refs: data-model.md §ResolvedAnchor status 'ambiguous'; amendments.md Decision #15 ('ambiguous → panel-only with picker'). Depends on UX-1, 2-1, 2-2, 2-3 panel. No 'any' types.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:37Z","created_by":"James Lal","updated_at":"2026-05-19T16:24:40Z","closed_at":"2026-05-19T16:24:40Z","close_reason":"Round 19: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.4.7","depends_on_id":"attn-nnj.10.1","type":"blocks","created_at":"2026-05-18T16:30:57Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.7","depends_on_id":"attn-nnj.10.2","type":"blocks","created_at":"2026-05-18T16:31:01Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.7","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:09Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.7","depends_on_id":"attn-nnj.12.5","type":"blocks","created_at":"2026-05-18T16:53:58Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.7","depends_on_id":"attn-nnj.12.8","type":"blocks","created_at":"2026-05-18T16:53:55Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.7","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:37Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.7","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:23Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.7","depends_on_id":"attn-nnj.4.2","type":"blocks","created_at":"2026-05-18T16:31:30Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":7,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.4.6","title":"Inline decoration system (ProseMirror plugin)","description":"New ProseMirror plugin at web/src/lib/prosemirror/review-decorations.ts. Reads ResolvedAnchor entries from the review store. Emits decorations per amendments.md Decision #15 cutoffs: ≥0.90 inline highlight no badge; 0.70-0.89 inline highlight + 'moved' badge in panel; ambiguous panel-only; stale panel-only. Handles overlap. Hover focuses the corresponding panel entry; clicking a panel entry scrolls editor to the decoration.","acceptance_criteria":"- web/src/lib/prosemirror/review-decorations.ts plugin exists, mirroring existing PM plugin pattern (math/tables/etc)\n- Reads from review store; updates as ResolvedAnchor entries change\n- Cutoffs implemented exactly per amendments.md Decision #15\n- Overlap handling implemented (stacked / layered)\n- Hover decoration ↔ focus panel entry wired both ways\n- Click panel entry → editor scrolls to decoration\n- No 'any' types","notes":"Spec refs: amendments.md Decision #15 (verbatim cutoffs); planning/collab/ui/inline-decorations.md (from UX-2). Existing PM plugin pattern: web/src/lib/prosemirror/{math,tables,code-highlight,code-block-nodeview,mermaid-nodeview}.ts. Depends on UX-2, 2-1 mock-ipc, 2-2 store.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:36Z","created_by":"James Lal","updated_at":"2026-05-19T15:29:39Z","closed_at":"2026-05-19T15:29:39Z","close_reason":"Implemented; merged; 409 Rust + 237 relay tests pass; store reassembled from 4.6 + 8.3 merge race","dependencies":[{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.10.1","type":"blocks","created_at":"2026-05-18T16:30:57Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.10.2","type":"blocks","created_at":"2026-05-18T16:31:01Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:05Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.12.2","type":"blocks","created_at":"2026-05-18T16:53:50Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.12.7","type":"blocks","created_at":"2026-05-18T16:53:52Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.3.5","type":"blocks","created_at":"2026-05-18T16:38:22Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:35Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:22Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.6","depends_on_id":"attn-nnj.4.2","type":"blocks","created_at":"2026-05-18T16:31:29Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":8,"dependent_count":2,"comment_count":0} +{"id":"attn-nnj.4.5","title":"Suggestion composer from selection","description":"ProseMirror selection → 'Suggest' UI offering replace / delete / insert-before / insert-after operations. Captures expectedText automatically for replace and delete from the current selection. Validates non-empty replacement before allowing submit. Submits via IPC review_create_suggestion.","acceptance_criteria":"- Selection surfaces a 'Suggest' affordance distinct from 'Comment'\n- Four operation modes: replace, delete, insert-before, insert-after\n- expectedText auto-captured from selection for replace/delete\n- Empty replacement blocked from submission (with inline error, not alert())\n- Submits via IPC review_create_suggestion; mock-ipc echoes back\n- Cancel/escape closes cleanly","notes":"Spec ref: data-model.md §Suggestion + §UI/UX Changes (suggestion card). Pairs with 2-4 comment composer. Depends on 2-1 mock-ipc, 2-2 store. No window.confirm/alert. No 'any' types.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:35Z","created_by":"James Lal","updated_at":"2026-05-19T16:24:24Z","closed_at":"2026-05-19T16:24:24Z","close_reason":"Round 19: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.4.5","depends_on_id":"attn-nnj.12.5","type":"blocks","created_at":"2026-05-18T16:53:58Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.5","depends_on_id":"attn-nnj.12.8","type":"blocks","created_at":"2026-05-18T16:53:54Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.5","depends_on_id":"attn-nnj.12.9","type":"blocks","created_at":"2026-05-18T16:53:56Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.5","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:34Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.5","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:22Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.4.4","title":"Comment composer from selection","description":"ProseMirror selection → 'Comment' popover near selection → text body input → submit. Constructs an Anchor from the selection range and calls IPC review_create_comment. For mock IPC, fakes the event back via reviewEvent so the panel updates immediately.","acceptance_criteria":"- Selection in ProseMirror surfaces a 'Comment' affordance (popover or floating button)\n- Body input supports multi-line text\n- Submit builds a valid Anchor (per data-model.md) from the selection\n- Calls IPC review_create_comment; mock-ipc returns and echoes the event\n- Cancel/escape closes without submitting\n- Empty body blocked from submission","notes":"Spec ref: data-model.md §Comment composer from selection and Anchor structure. Wire alongside web/src/lib/Editor.svelte (ProseMirror view). Depends on 2-1 mock-ipc, 2-2 store. No 'any' types.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:34Z","created_by":"James Lal","updated_at":"2026-05-19T16:24:08Z","closed_at":"2026-05-19T16:24:08Z","close_reason":"Round 19: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.4.4","depends_on_id":"attn-nnj.12.5","type":"blocks","created_at":"2026-05-18T16:53:57Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.4","depends_on_id":"attn-nnj.12.8","type":"blocks","created_at":"2026-05-18T16:53:54Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.4","depends_on_id":"attn-nnj.12.9","type":"blocks","created_at":"2026-05-18T16:53:55Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.4","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:33Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.4","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:21Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.4.3","title":"ReviewMargin.svelte: Google-Docs-style margin sticky cards","description":"Right-margin overlay rendering review cards positioned to vertically align with their anchor in the editor. Replaces the earlier panel-river design after the user pivoted to Google Docs spatial model (cards live next to their anchored text, not in a separate triage list). Includes an 'orphan tray' at the top of the margin for ambiguous/stale cards that have no valid anchor position. Reuses Phase 0c plumbing (review store, types, theme vars, popover helper); only the layout slot from 12.1 needs an overlay adjustment (already on disk, not yet a rewrite).","acceptance_criteria":"- web/src/lib/ReviewPanel.svelte exists and renders from review store\n- Grouping matches planning/collab/ui/review-panel-design.md\n- Comment and suggestion card variants implemented with author/anchor/body/status/actions\n- Empty and loading states implemented\n- Keyboard shortcut registered (consistent with KeyboardShortcutsDialog.svelte)\n- No window.confirm/alert — uses in-app UI only","notes":"Depends on: UX-1 (panel design), 2-2 (store). Existing patterns: Sidebar.svelte for rail, KeyboardShortcutsDialog.svelte for shortcut registration. Svelte 5 runes throughout. No 'any' types.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:33Z","created_by":"James Lal","updated_at":"2026-05-19T16:05:48Z","closed_at":"2026-05-19T16:05:48Z","close_reason":"Round 18: implemented; merged; 414+ Rust tests pass","dependencies":[{"issue_id":"attn-nnj.4.3","depends_on_id":"attn-nnj.10.1","type":"blocks","created_at":"2026-05-18T16:30:56Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.3","depends_on_id":"attn-nnj.10.2","type":"blocks","created_at":"2026-05-18T16:31:00Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.3","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:05Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.3","depends_on_id":"attn-nnj.12.1","type":"blocks","created_at":"2026-05-18T16:53:47Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.3","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:33Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.3","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:21Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.3","depends_on_id":"attn-nnj.4.2","type":"blocks","created_at":"2026-05-18T16:31:28Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":6,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.4.1","title":"Mock-IPC extension: review event stream","description":"Extend web/src/lib/mock-ipc.ts to emit window.__attn__.reviewStatus(payload), reviewEvent(payload), reviewSnapshot(snapshot), and reviewAnchorResolution(update). Add a replayable scripted scenario JSON in web/src/lib/mock-ipc-scenarios/ that demonstrates: owner edits paragraph → reviewer comments → owner edits more → reviewer's anchor remaps → owner accepts. Enables Phase 2 frontend work to proceed without any Rust crates landing.","acceptance_criteria":"- mock-ipc.ts exposes reviewStatus, reviewEvent, reviewSnapshot, reviewAnchorResolution on window.__attn__\n- web/src/lib/mock-ipc-scenarios/ contains at least one JSON scenario file\n- Replay runs deterministically via dev-tools trigger (button or window helper)\n- Scenario covers owner-edits → reviewer-comments → remap → accept flow\n- Documented in a short README in mock-ipc-scenarios/","notes":"Spec refs: amendments.md §Mock IPC must be extended for parallel frontend dev (line ~74); data-model.md lines 1088-1091 callback list. Existing file to extend: web/src/lib/mock-ipc.ts (100 lines). Use Svelte 5 runes patterns in any new helpers. This unblocks all subsequent Phase 2 issues.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:32Z","created_by":"James Lal","updated_at":"2026-05-19T15:30:09Z","closed_at":"2026-05-19T15:30:09Z","close_reason":"Implemented; merged; 409 Rust + 237 relay tests pass; store reassembled from 4.6 + 8.3 merge race","dependencies":[{"issue_id":"attn-nnj.4.1","depends_on_id":"attn-nnj.12.6","type":"blocks","created_at":"2026-05-18T16:53:51Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.1","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:31Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":13,"comment_count":0} +{"id":"attn-nnj.4.2","title":"Frontend review store (Svelte 5 runes)","description":"Create web/src/lib/review/store.ts: a $state-based store holding rooms, threads, snapshots, anchorResolutions, and outbox. Subscribes to window.__attn__.reviewStatus/reviewEvent/reviewSnapshot/reviewAnchorResolution callbacks. Provides $derived selectors for 'comments on current file/snapshot', 'ambiguous anchors', and 'outbox count'. No 'any' types — proper TypeScript throughout.","acceptance_criteria":"- web/src/lib/review/store.ts exists with $state-based store\n- Subscribes to all four window.__attn__ review callbacks\n- Exposes derived selectors: commentsOnCurrent, ambiguousAnchors, outboxCount\n- Fully typed (no 'any'); types align with data-model.md ReviewEvent/Anchor/Snapshot shapes\n- Unit-testable shape (pure functions for selectors where possible)","notes":"Spec refs: data-model.md §Frontend Bridge (lines ~1080-1100) for callback contract; §UI/UX Changes for what the store must surface. Use Svelte 5 runes ($state, $derived, $effect). Follow project rule: no 'any' types. Depends on 2-1 mock-ipc extension for runtime emissions.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:32Z","created_by":"James Lal","updated_at":"2026-05-19T15:30:23Z","closed_at":"2026-05-19T15:30:23Z","close_reason":"Implemented; merged; 409 Rust + 237 relay tests pass; store reassembled from 4.6 + 8.3 merge race","dependencies":[{"issue_id":"attn-nnj.4.2","depends_on_id":"attn-nnj.12.10","type":"blocks","created_at":"2026-05-18T16:53:57Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.2","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:32Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.2","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:19Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":7,"comment_count":0} +{"id":"attn-nnj.2.8","title":"ReviewManager scaffold + UserEvent::Review event-loop wiring","description":"Scaffold src/review/manager.rs as a struct holding store + working_copy + rooms map. Wire it into the EXISTING tao event loop in src/main.rs by adding a UserEvent::Review(ReviewUpdate) arm — per amendments.md §Phase 0b, do NOT factor out a new event loop. Forwards ReviewUpdates to the webview via window.__attn__.reviewEvent(...).","acceptance_criteria":"- src/review/manager.rs defines `pub struct ReviewManager { store, working_copy, rooms: HashMap\u003cRoomId, RoomRuntime\u003e }` + new() + a tokio mpsc channel for ReviewUpdate\n- src/main.rs UserEvent enum gains a Review(ReviewUpdate) variant\n- The existing event_loop.run match adds an arm: UserEvent::Review(update) =\u003e { webview.evaluate_script(\u0026format!(\"window.__attn__.reviewEvent({})\", serde_json::to_string(\u0026update)?))?; }\n- Manager constructed during daemon startup; channel sender stashed in AppState (or accessible globally)\n- No real room/document logic yet — just lifecycle: manager starts, channel works, a smoke test sends a stub ReviewUpdate and the webview receives it (verified via --eval window.__attn__.lastReviewEvent)\n- ReviewUpdate is a typed enum (not serde_json::Value) — initial variants can be small but explicit","notes":"Spec: planning/collab/amendments.md §Codebase Corrections (main.rs is 1207 lines, not thin) + §Phase 0b (integrates into EXISTING event loop). data-model.md §Review Manager + §Webview IPC Changes for the JS bridge shape. Critical: do NOT introduce a second event loop or factor out main.rs's. Add to the existing match arms only.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:14Z","created_by":"James Lal","updated_at":"2026-05-19T01:33:11Z","closed_at":"2026-05-19T01:33:11Z","close_reason":"Implemented; merged into collab; 212 Rust tests + 98 relay tests pass","dependencies":[{"issue_id":"attn-nnj.2.8","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:30:14Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.8","depends_on_id":"attn-nnj.2.1","type":"blocks","created_at":"2026-05-18T16:30:26Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.8","depends_on_id":"attn-nnj.2.2","type":"blocks","created_at":"2026-05-18T16:30:28Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.8","depends_on_id":"attn-nnj.2.3","type":"blocks","created_at":"2026-05-18T16:30:29Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.8","depends_on_id":"attn-nnj.2.7","type":"blocks","created_at":"2026-05-18T16:30:30Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.8","depends_on_id":"attn-nnj.2.9","type":"blocks","created_at":"2026-05-18T16:54:01Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":5,"dependent_count":2,"comment_count":0} +{"id":"attn-nnj.2.7","title":"AppState refactor: review_rooms HashMap + file_to_room routing","description":"Refactor AppState in src/ipc.rs per amendments.md §Codebase Corrections: AppState becomes routing context holding `review_rooms: HashMap\u003cRoomId, RoomRuntimeHandle\u003e` and `file_to_room: HashMap\u003cPathBuf, RoomId\u003e`. Heavy state lives in ReviewManager — AppState just looks up which room a file belongs to.","acceptance_criteria":"- src/ipc.rs AppState fields: review_rooms: HashMap\u003cRoomId, RoomRuntimeHandle\u003e, file_to_room: HashMap\u003cPathBuf, RoomId\u003e (in addition to existing tab/project state)\n- RoomRuntimeHandle is a lightweight Arc/channel-sender to the manager (NOT the full ReviewRoom struct)\n- All AppState construction sites + call sites updated\n- Lookup helper: AppState::room_for_path(\u0026Path) -\u003e Option\u003cRoomRuntimeHandle\u003e\n- Existing IPC handlers compile and still pass tests\n- No flat list of rooms anywhere — file_to_room is the only path→room oracle","notes":"Spec: planning/collab/amendments.md §Codebase Corrections (Tabs and projects are first-class; the plan's AppState is wrong). This DIVERGES from the original data-model.md AppState design — amendments wins. RoomRuntimeHandle is defined here as a thin handle (clonable, Send+Sync); the real ReviewManager fills in the actual struct in issue 8.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:59Z","created_by":"James Lal","updated_at":"2026-05-19T00:23:59Z","closed_at":"2026-05-19T00:23:59Z","close_reason":"Implemented in parallel worktrees; merged into collab; 142 tests pass","dependencies":[{"issue_id":"attn-nnj.2.7","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:29:58Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.7","depends_on_id":"attn-nnj.2.1","type":"blocks","created_at":"2026-05-18T16:30:25Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.5.4","title":"Hashcash PoW verification + replay protection","description":"relay/src/pow.ts: verify the Attn-PoW header on every write endpoint (POST /devices, POST /envelopes, POST /acks, POST /blobs, DELETE). Token is 8 colon-separated fields, v2 format. Validation order per crypto-spec.md §Server Validation. Replay protection via meta:pow_seen:\u003cexpiresAt\u003e:\u003chash\u003e DO storage entries; a scheduled alarm prunes entries past expiresAt+10min. No client type is exempt (decision #6); default difficulty 16 bits, hard floor 12.","acceptance_criteria":"- Parse the 8-field v2 token; reject unknown version\n- Validation in order: parse → v==v2 → difficulty\u003e=max(policy.powBits,12) → expiresAt within 5-min window (not past, not \u003e5min future) → resource matches (roomId, deviceId, base64url(SHA-256(METHOD + space + PATH))[:8]) → leading-zero bits in SHA-256(token bytes) → not in pow_seen replay set\n- On success: insert meta:pow_seen:\u003cexpiresAt\u003e:\u003chash\u003e with TTL\n- Errors map to ATTN_POW_INVALID (parse/format), ATTN_POW_INSUFFICIENT_DIFFICULTY, ATTN_POW_EXPIRED, ATTN_POW_RESOURCE_MISMATCH, ATTN_POW_REPLAYED — all 403\n- Single PoW token per HTTP request — for batch /envelopes the same token covers the whole batch (decision #7)\n- Alarm-driven prune loop removes pow_seen entries with expiresAt+10min \u003c now\n- Unit tests use vectors from crypto-spec.md §Test Vectors","notes":"Spec: planning/collab/crypto-spec.md §Hashcash Proof-of-Work (lines 117-197), §Server Validation (152-165), §Replay Protection (166-169). amendments.md decisions #6 (universal PoW) and #7 (single token per batch). Used by all write endpoints — implement as composable middleware.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:51Z","created_by":"James Lal","updated_at":"2026-05-19T01:16:27Z","closed_at":"2026-05-19T01:16:27Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.5.4","depends_on_id":"attn-nnj.1.2","type":"blocks","created_at":"2026-05-18T16:38:23Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.4","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:29:51Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.4","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:24Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":9,"comment_count":0} +{"id":"attn-nnj.5.3","title":"Owner-signature verification","description":"relay/src/owner-sig.ts: verify the Attn-Owner-Signature header (base64url Ed25519 signature over the same canonicalRequest used for admission HMAC: METHOD\\nPATH\\nCANONICAL_QUERY\\nSHA256(body)). Verifies against ownerSigningKey stored at room creation; mismatching key returns 403 ATTN_OWNER_KEY_MISMATCH. Required for DELETE /v2/rooms/:roomId at all times, and required for POST /v2/rooms/:roomId/acks when the request asks for deletion AND policy.deleteEventsAfterOwnerAck==true. Missing header on a path that requires it returns 403 ATTN_OWNER_SIG_REQUIRED.","acceptance_criteria":"- Canonical bytes match the admission canonicalRequest construction (single shared helper)\n- Verify Ed25519 sig with @noble/ed25519 or Web Crypto subtle\n- Required: DELETE /v2/rooms/:roomId (always)\n- Required: POST /acks when delete=true requested AND policy.deleteEventsAfterOwnerAck==true\n- 403 ATTN_OWNER_SIG_REQUIRED when header missing on a required path\n- 403 ATTN_OWNER_KEY_MISMATCH when signature does not verify against the stored ownerSigningKey\n- Stored ownerSigningKeyId is base64url(SHA-256(ownerSigningKey)) — exposed in room policy responses\n- Unit tests cover valid sig, wrong key, tampered body, missing header, non-owner action attempt","notes":"Spec: planning/collab/relay-spec.md §Identity, Keys, and Admission \u003e Owner Distinction (lines 69-74), §POST /v2/rooms/:roomId (114-167), §POST /v2/rooms/:roomId/acks (277-303), §DELETE /v2/rooms/:roomId (305-311). amendments.md decision #3 (owner-only ops). Shares canonicalRequest helper with admission middleware.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:50Z","created_by":"James Lal","updated_at":"2026-05-19T01:16:13Z","closed_at":"2026-05-19T01:16:13Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.5.3","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:29:50Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.3","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:23Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} +{"id":"attn-nnj.5.2","title":"Admission HMAC verification module","description":"relay/src/admission.ts: middleware that verifies the Attn-Admission header on every authenticated endpoint. HMAC-SHA256 covers METHOD\\nPATH\\nCANONICAL_QUERY\\nSHA256(body). Trust model is URL-as-bearer (decision #2): the relay derives the admissionKey from material the client supplies at room creation and stores it on the room record; subsequent requests must present a matching HMAC. Wrong HMAC returns 401 with error code ATTN_ADMISSION_INVALID.","acceptance_criteria":"- Canonical string per spec: METHOD\\nPATH\\nCANONICAL_QUERY (sorted, percent-encoded)\\nSHA256(body) hex\n- HMAC constant-time compared; failure returns 401 {error:{code:'ATTN_ADMISSION_INVALID'}}\n- Admission key is loaded from room storage; missing room → 404 ATTN_ROOM_NOT_FOUND (before admission check to avoid timing oracle? — actually per spec admission failure must NOT leak room existence: return 401 for missing-room too)\n- Unit tests cover: wrong HMAC, missing header, body tampering, query reordering, missing room (uniform 401)\n- Exported as a Hono/itty middleware reused by all authenticated routes","notes":"Spec: planning/collab/relay-spec.md §Identity, Keys, and Admission (lines 37-67) and §Wire Conventions (89-99). Decision #2 in amendments.md (URL-as-bearer trust model). This middleware is the gatekeeper for every endpoint except POST /v2/rooms/:roomId (room creation, which establishes the admission key) and GET /health.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:49Z","created_by":"James Lal","updated_at":"2026-05-19T00:40:28Z","closed_at":"2026-05-19T00:40:28Z","close_reason":"Implemented in parallel; merged into collab; 166 tests pass + relay 35 tests pass","dependencies":[{"issue_id":"attn-nnj.5.2","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:29:49Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.5.2","depends_on_id":"attn-nnj.5.1","type":"blocks","created_at":"2026-05-18T16:35:22Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":9,"comment_count":0} +{"id":"attn-nnj.2.6","title":"Watcher self-write distinction with TTL-tracked hashes","description":"Teach src/watcher.rs to distinguish between our own writes (via WorkingCopyService) and external file changes (editor, git checkout). Tracks recent self-writes by (path, ContentHash) with a short TTL; matching events attach to the existing LocalRevision, others emit a new ExternalFileChange revision.","acceptance_criteria":"- src/watcher.rs maintains a HashMap\u003c(PathBuf, ContentHash), Instant\u003e of recent self-writes\n- WorkingCopyService::save inserts into this map immediately after a successful write\n- TTL configurable, default 5s; expired entries pruned on access\n- On notify event: compute new ContentHash; if (path, hash) hit → attach to existing LocalRevision (no new journal entry); else emit a new LocalRevision{source: ExternalFileChange} via the store + existing reload signal\n- Existing reload-the-webview behavior is preserved end-to-end\n- Unit tests cover: self-write skipped, external write journaled, TTL expiry causes external-classification, repeated identical external content still journaled once","notes":"Spec: planning/collab/data-model.md §File Watcher Integration. The watcher already debounces; reuse that. Don't journal from inside the watcher directly — call store.append_revision via a channel to the ReviewManager so single-writer ordering holds.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:46Z","created_by":"James Lal","updated_at":"2026-05-19T01:33:24Z","closed_at":"2026-05-19T01:33:24Z","close_reason":"Implemented; merged into collab; 212 Rust tests + 98 relay tests pass","dependencies":[{"issue_id":"attn-nnj.2.6","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:29:45Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.6","depends_on_id":"attn-nnj.2.1","type":"blocks","created_at":"2026-05-18T16:30:25Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.6","depends_on_id":"attn-nnj.2.5","type":"blocks","created_at":"2026-05-18T16:30:30Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.2.5","title":"Revision journal appended on every WorkingCopyService save","description":"On every WorkingCopyService::save, append a LocalRevision to ~/.attn/reviews/rooms/\u003croomId\u003e/revisions/\u003cfileId\u003e.jsonl. Captures parentHash → nextHash + optional pmSteps/patchText so future anchor code can replay the document history.","acceptance_criteria":"- WorkingCopyService::save appends a LocalRevision JSONL line per call\n- LocalRevision fields: revisionId, fileId, parentHash, nextHash, source (SaveSource), timestamp, optional pmSteps, optional patchText\n- Source variants wired: UserEdit, CheckboxToggle, ExternalFileChange, SnapshotLoaded, ManualReanchor (AcceptedSuggestion stub is fine — Phase 5 wires it)\n- File path: ~/.attn/reviews/rooms/\u003croomId\u003e/revisions/\u003cfileId\u003e.jsonl\n- Append is atomic per line (single write syscall + fsync)\n- Unit tests cover: single save → single revision, sequential saves produce parentHash chain, replay reads back identical sequence","notes":"Spec: planning/collab/data-model.md §Local Replicas (LocalRevision struct) + §Working Copy Service. pmSteps/patchText are optional now — Phase 1 anchor engine fills them in. RoomId routing comes from AppState (Phase 0b issue 7); for save calls outside any room, use a sentinel \"orphan\" or skip journaling.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:33Z","created_by":"James Lal","updated_at":"2026-05-19T01:15:47Z","closed_at":"2026-05-19T01:15:47Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.2.5","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:29:33Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.5","depends_on_id":"attn-nnj.2.1","type":"blocks","created_at":"2026-05-18T16:30:24Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.5","depends_on_id":"attn-nnj.2.2","type":"blocks","created_at":"2026-05-18T16:30:27Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.5","depends_on_id":"attn-nnj.2.4","type":"blocks","created_at":"2026-05-18T16:30:29Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.8.5","title":"Emit SuggestionAccepted and SuggestionRejected","description":"After a successful apply (clean or three-way accepted), ReviewManager constructs a SuggestionAccepted ReviewEvent referencing the LocalRevision id and the SaveResult.resultingHash, signs+encrypts it, and enqueues it on the outbox for relay/DataChannel delivery. The reject path emits SuggestionRejected with an optional reason. Both flow through the same outbox machinery as comment events.","acceptance_criteria":"- src/review/manager.rs on apply success builds SuggestionAccepted { suggestionId, appliedRevisionId, resultingHash } matching data-model.md §Suggestion Events.\n- On reject (owner picked 'Keep current'), builds SuggestionRejected { suggestionId, reason } where reason is optional and may come from the UI.\n- Events are signed with the owner's signing key, encrypted under eventKey, and appended to outbox.jsonl exactly like comment events (single code path).\n- meta.parentEventIds includes the original SuggestionCreated event id so receivers can reconstruct the thread.\n- Unit test: simulate full apply path, assert one SuggestionAccepted envelope sits in outbox with the expected fields; simulate reject path, assert one SuggestionRejected envelope.","notes":"Spec: planning/collab/data-model.md §Suggestion Events (lines 628-640). Files: src/review/manager.rs, src/review/outbox.rs. Reuse the existing outbox enqueue path — do not introduce a parallel writer. Outbox mutability rule from amendments.md §Outbox mutability and freezing applies (these events are immutable once first-send-attempted).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:25Z","created_by":"James Lal","updated_at":"2026-05-19T15:06:00Z","closed_at":"2026-05-19T15:06:00Z","close_reason":"Implemented; merged; 412 Rust + 213 relay tests pass (6 conformance scenarios deferred to 5.16)","dependencies":[{"issue_id":"attn-nnj.8.5","depends_on_id":"attn-nnj.8","type":"parent-child","created_at":"2026-05-18T16:29:24Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.8.5","depends_on_id":"attn-nnj.8.4","type":"blocks","created_at":"2026-05-18T16:29:53Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.8.4","title":"Apply through WorkingCopyService","description":"When the owner accepts a suggestion (clean apply or three-way 'accept'/'accept_edited'), the resulting bytes must be written to the file via WorkingCopyService::save with SaveSource::AcceptedSuggestion { room_id, suggestion_id }. This records a LocalRevision in the journal so the watcher distinguishes the apply from an external edit and so the SaveResult.resultingHash can feed the SuggestionAccepted event.","acceptance_criteria":"- src/review/apply.rs apply_accepted(verdict_or_edited_bytes, source: SaveSource::AcceptedSuggestion { room_id, suggestion_id }) calls WorkingCopyService::save and returns a SaveResult with resultingHash.\n- The LocalRevision entry created has source=AcceptedSuggestion and references both room_id and suggestion_id so the revision journal can be queried by room.\n- File watcher sees its own write and suppresses the reload bounce (existing self-write distinction in src/watcher.rs).\n- After write, the editor's in-memory document updates without losing the user's cursor (PM transaction rather than a full reload where possible).\n- Integration test in src/review/apply.rs writes a fixture file, applies a suggestion, asserts both file contents and a new LocalRevision entry.","notes":"Spec: planning/collab/data-model.md §Suggestion Events apply flow steps 5-6 (lines 648-649) + amendments.md §watcher.rs does more than reload (line ~21). Files: src/review/apply.rs, src/working_copy.rs (Phase 0b). WorkingCopyService::save is the only write path — never std::fs::write directly.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:22Z","created_by":"James Lal","updated_at":"2026-05-19T04:28:54Z","closed_at":"2026-05-19T04:28:54Z","close_reason":"Round 13: implemented; merged; all tests pass","dependencies":[{"issue_id":"attn-nnj.8.4","depends_on_id":"attn-nnj.2.4","type":"blocks","created_at":"2026-05-18T16:38:30Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.8.4","depends_on_id":"attn-nnj.8","type":"parent-child","created_at":"2026-05-18T16:29:21Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.8.4","depends_on_id":"attn-nnj.8.1","type":"blocks","created_at":"2026-05-18T16:29:52Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.5.1","title":"Relay package scaffold + Wrangler config","description":"Create the relay/ workspace package that hosts the Cloudflare Worker: package.json, tsconfig.json, wrangler.toml, src/index.ts (router stub), src/room-do.ts (Durable Object stub), src/schema.ts (zod request/response validators), and test/ folder layout. wrangler dev --local must boot Miniflare with the RELAY_ROOMS Durable Object binding and RELAY_BLOBS R2 bucket binding so subsequent issues can integration-test against it.","acceptance_criteria":"- relay/ directory exists with package.json, tsconfig.json, wrangler.toml, src/{index,room-do,schema}.ts, test/{integration,conformance}/ scaffolding\n- wrangler.toml: compatibility_date=2026-01-01, RELAY_ROOMS Durable Object class binding, RELAY_BLOBS R2 binding, HARD_MAX_ROOM_BYTES / HARD_MAX_EVENT_BYTES / HARD_MAX_SNAPSHOT_BYTES env vars set per spec\n- zod schemas typecheck against the request/response shapes in relay-spec.md\n- 'wrangler dev --local' boots Miniflare cleanly with the DO and R2 stub mounted; GET /health returns 200\n- npm test wires through to a vitest runner pointed at test/","notes":"Spec: planning/collab/relay-spec.md §Deployment (lines 614-685) for wrangler.toml sketch and repo layout. §Caps (Server Hard Maxima) for HARD_MAX_* values. This issue blocks every other 3a issue — keep it strictly to scaffolding (no business logic). Repo currently has no relay/ folder.","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:21Z","created_by":"James Lal","updated_at":"2026-05-18T23:57:41Z","closed_at":"2026-05-18T23:57:41Z","close_reason":"Implemented; merged into collab","dependencies":[{"issue_id":"attn-nnj.5.1","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-18T16:29:21Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":15,"comment_count":0} +{"id":"attn-nnj.2.4","title":"WorkingCopyService replacing direct fs::write","description":"Introduce src/review/working_copy.rs as the single chokepoint for every markdown-file save. Replaces direct std::fs::write calls in src/ipc.rs (EditSave + checkbox toggle). Hashes content on every save so downstream revision/anchor code has a stable identity.","acceptance_criteria":"- src/review/working_copy.rs exposes `save(req: SaveRequest) -\u003e Result\u003cSaveResult\u003e`\n- SaveRequest { path: PathBuf, content: String, expected_hash: Option\u003cContentHash\u003e, source: SaveSource }\n- SaveResult { previous_hash: ContentHash, next_hash: ContentHash, revision_id: RevisionId }\n- SaveSource enum: UserEdit, CheckboxToggle, AcceptedSuggestion, ExternalFileChange, SnapshotLoaded, ManualReanchor\n- ContentHash computed per crypto-spec.md §ContentHash (canonical UTF-8, no BOM, LF line endings, preserve trailing-newline as authored)\n- expected_hash mismatch → returns ConflictError without writing\n- src/ipc.rs EditSave and checkbox-toggle paths use WorkingCopyService::save instead of std::fs::write\n- Unit tests cover: happy save, expected_hash mismatch, hash determinism across line-ending normalization","notes":"Spec: planning/collab/data-model.md §Working Copy Service + crypto-spec.md §ContentHash. The revision_id returned here gets persisted by the revision-journal issue. Keep this synchronous for now (single-writer).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:20Z","created_by":"James Lal","updated_at":"2026-05-19T00:39:55Z","closed_at":"2026-05-19T00:39:55Z","close_reason":"Implemented in parallel; merged into collab; 166 tests pass + relay 35 tests pass","dependencies":[{"issue_id":"attn-nnj.2.4","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:29:20Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.4","depends_on_id":"attn-nnj.2.1","type":"blocks","created_at":"2026-05-18T16:30:23Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.4","depends_on_id":"attn-nnj.2.2","type":"blocks","created_at":"2026-05-18T16:30:27Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} +{"id":"attn-nnj.8.3","title":"Three-way apply UI dialog (Svelte)","description":"Svelte 5 component that surfaces ApplyVerdict::RequiresThreeWay to the owner. Shows the snapshot text (what the suggester saw), the current owner text (what's there now), and the proposed replacement, side-by-side. Owner picks: accept proposed, keep current, or edit manually. The component returns the owner's choice via IPC back to ReviewManager.","acceptance_criteria":"- web/src/lib/ReviewApplyDialog.svelte renders three panes (snapshot | current | proposed) with monospace diff styling.\n- Props: { suggestionId, snapshotText, currentText, proposedText, anchorContext }.\n- Actions: 'Accept proposed' (returns 'accept'), 'Keep current' (returns 'reject'), 'Edit manually' (opens an inline editor with the proposed text as the starting buffer, returns 'accept_edited' with the edited string).\n- Built with Svelte 5 runes (, , ); no Svelte 4 patterns. No window.confirm / alert per project conventions — fully in-app UI.\n- Result flows to ReviewManager via window.__attn__.reviewSubmitApplyChoice(suggestionId, choice, editedText?).\n- Storybook-style demo route or mock-ipc fixture so the dialog renders standalone.","notes":"Spec: planning/collab/data-model.md §Suggestion Events apply flow step 3 (line 646). Follow svelte5-best-practices skill conventions. Files: web/src/lib/ReviewApplyDialog.svelte. Existing component patterns: look at the review panel pieces from Phase 2 work. Project rule: no window.confirm/alert — use proper in-app UI.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:06Z","created_by":"James Lal","updated_at":"2026-05-19T15:29:54Z","closed_at":"2026-05-19T15:29:54Z","close_reason":"Implemented; merged; 409 Rust + 237 relay tests pass; store reassembled from 4.6 + 8.3 merge race","dependencies":[{"issue_id":"attn-nnj.8.3","depends_on_id":"attn-nnj.10.4","type":"blocks","created_at":"2026-05-18T16:38:30Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.8.3","depends_on_id":"attn-nnj.8","type":"parent-child","created_at":"2026-05-18T16:29:05Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.8.3","depends_on_id":"attn-nnj.8.1","type":"blocks","created_at":"2026-05-18T16:29:52Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.2.3","title":"Local JSON/JSONL store at ~/.attn/reviews/","description":"Implement src/review/store.rs as the on-disk persistence layer for rooms, events, snapshots, outbox, and revisions. Atomic writes via temp-file+rename for JSON; append-only JSONL for event/outbox/revision logs. Idempotent on EventId and EnvelopeId so repeated imports are safe.","acceptance_criteria":"- Directory layout matches data-model.md §Local Review Store exactly (rooms/\u003croomId\u003e/{room.json, devices/, snapshots/, events.jsonl, outbox.jsonl, revisions/\u003cfileId\u003e.jsonl, replicas/\u003creplicaId\u003e/...})\n- All JSON writes are atomic: write to .tmp then rename\n- JSONL writes are append-only and fsync'd per append\n- import_event(EventId, ...) is idempotent — duplicate EventId is a no-op\n- enqueue_outbox(EnvelopeId, ...) is idempotent on EnvelopeId\n- Every top-level JSON file includes a `schemaVersion` field\n- Unit tests cover: fresh-init, repeat-import, partial-write recovery, concurrent appender safety (single writer)","notes":"Spec: planning/collab/data-model.md §Local Replicas + §Local Review Store layout (search §Local Review Store in data-model.md). Use std::fs + serde_json. No async yet; called from a single ReviewManager task. ~/.attn/ already exists for the daemon socket — extend with reviews/ subtree.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:01Z","created_by":"James Lal","updated_at":"2026-05-19T00:00:28Z","closed_at":"2026-05-19T00:00:28Z","close_reason":"Implemented; merged into collab; 9 store tests pass","dependencies":[{"issue_id":"attn-nnj.2.3","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:29:01Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.3","depends_on_id":"attn-nnj.2.1","type":"blocks","created_at":"2026-05-18T16:30:23Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.3","depends_on_id":"attn-nnj.2.2","type":"blocks","created_at":"2026-05-18T16:30:26Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.8.2","title":"Expected-text verification","description":"Tight unit tests around SuggestionOperation.expectedText vs current owner text. A false positive (verifying equal when the bytes differ) corrupts the user's file silently. expectedText was captured against the snapshot's exact bytes, so verification must be byte-identical with no Unicode normalization, no whitespace folding, and explicit line-ending handling.","acceptance_criteria":"- src/review/apply.rs has a private verify_expected_text(current: \u0026str, expected: \u0026str) -\u003e bool helper used by the suggestion resolver.\n- Unit tests cover: identical bytes -\u003e true; trailing-whitespace diff -\u003e false; CRLF vs LF diff -\u003e false (ContentHash normalizes to LF on write, but expectedText was captured exactly as the snapshot bytes — they must match exactly); NFC vs NFD unicode -\u003e false (no normalization); BOM present in one only -\u003e false; empty-string vs empty-string -\u003e true.\n- Property-style test: for random inputs s, verify_expected_text(s, s) == true and verify_expected_text(s, s + 'x') == false.\n- Documented in a module-level comment that this function intentionally does no normalization.","notes":"Spec: planning/collab/data-model.md §Suggestion Events apply flow step 2 (line 645). Critical correctness path — please err on the side of more tests. See also crypto-spec.md ContentHash normalization (writes LF; expectedText captured from snapshot bytes pre-normalization).","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:57Z","created_by":"James Lal","updated_at":"2026-05-19T04:08:29Z","closed_at":"2026-05-19T04:08:29Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.8.2","depends_on_id":"attn-nnj.8","type":"parent-child","created_at":"2026-05-18T16:28:57Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.8.2","depends_on_id":"attn-nnj.8.1","type":"blocks","created_at":"2026-05-18T16:29:51Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.8.1","title":"Suggestion resolver (Rust)","description":"Given a SuggestionCreated event and the current owner DocumentReplica, resolve the suggestion's Anchor through the Phase 1 anchor engine, then evaluate the SuggestionOperation. Produces an ApplyVerdict that drives the apply UI: clean apply, three-way merge, ambiguous picker, or stale. This is the core decision point of the Phase 5 apply flow.","acceptance_criteria":"- src/review/apply.rs exposes resolve_suggestion(event: \u0026SuggestionCreated, replica: \u0026DocumentReplica) -\u003e ApplyVerdict.\n- ApplyVerdict variants: Ready { range: PositionAnchor, replacement: String, op_kind }, RequiresThreeWay { range, snapshot_text, current_text, replacement }, Stale { reason }, Ambiguous { candidates }.\n- For SuggestionOperation::Replace/Delete: anchor resolves with status in {exact, remapped} AND current text at the resolved range equals expectedText -\u003e Ready; if anchor resolves but current text differs -\u003e RequiresThreeWay; if anchor is ambiguous -\u003e Ambiguous; if stale -\u003e Stale.\n- For SuggestionOperation::InsertBefore/InsertAfter: anchor must be unambiguous (exact or remapped); ambiguous/stale flow to Ambiguous/Stale.\n- Unit tests cover each verdict path with realistic fixtures.","notes":"Spec: planning/collab/data-model.md §Suggestion Events apply flow (lines 642-650). Files: src/review/apply.rs. Depends on the Phase 1 Rust resolver (attn-nnj.3.4) — added as cross-phase dep by parent agent. Pure function, no I/O, no UI — UI lives in the three-way dialog issue.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:49Z","created_by":"James Lal","updated_at":"2026-05-19T04:06:42Z","closed_at":"2026-05-19T04:06:42Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.8.1","depends_on_id":"attn-nnj.3.4","type":"blocks","created_at":"2026-05-18T16:38:29Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.8.1","depends_on_id":"attn-nnj.8","type":"parent-child","created_at":"2026-05-18T16:28:49Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} +{"id":"attn-nnj.2.2","title":"Serde model types for review domain","description":"Define all serde structs in src/review/model.rs covering rooms, participants, devices, documents, snapshots, replicas, revisions, events, envelopes, and sync cursors. JSON field names match data-model.md camelCase exactly so any future browser/CLI can read the same files.","acceptance_criteria":"- src/review/model.rs defines: ReviewRoom, RoomPolicy, Participant, Device, SharedDocument, SnapshotNode, BlobRef, DocumentReplica, ReplicaRelation, LocalRevision, ReviewEvent (with EventMeta + Body + Auth submodels), MailboxEnvelope, SyncCursor, DeliveryAck\n- ReviewEventBody is a tagged enum with variants: RoomCreated, ParticipantJoined, SnapshotCreated, SnapshotSuperseded, CommentCreated, CommentResolved, SuggestionCreated, SuggestionAccepted, SuggestionRejected, AnchorManuallyResolved, PresenceUpdated, SessionEnded\n- All structs use #[serde(rename_all = \"camelCase\")] (or per-field rename) so JSON matches data-model.md\n- Roundtrip test: every variant serializes → deserializes byte-identical\n- Zero use of `any` / `serde_json::Value` except where the spec explicitly says opaque payload","notes":"Spec: planning/collab/data-model.md §Terms, §Review Events (all subsections), §Encrypted Envelopes, §Sync Cursors And ACKs. Use #[serde(tag = \"kind\")] for the event body enum. Use the typed ID newtypes from Phase 0a issue 8 (RoomId, FileId, EventId, etc.) — they're already serde-transparent.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:48Z","created_by":"James Lal","updated_at":"2026-05-18T23:45:35Z","closed_at":"2026-05-18T23:45:35Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.2.2","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:28:48Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.2.2","depends_on_id":"attn-nnj.2.1","type":"blocks","created_at":"2026-05-18T16:30:22Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":6,"comment_count":0} +{"id":"attn-nnj.10.3","title":"Design: connection + share affordances","description":"Decide where the share button, connection badge, and peer strip live in the toolbar/header. Toolbar real estate is already contended by theme toggle, edit toggle, and command palette. Determine owner-only vs reviewer-only affordances and overflow behavior. Output planning/collab/ui/connection-share.md with proposed layout.","acceptance_criteria":"- planning/collab/ui/connection-share.md exists with annotated layout\n- Resolves placement of: share button, connection badge (Live direct / Mailbox / Offline / Direct failed), peer strip\n- Distinguishes owner-only vs reviewer-only affordances\n- Notes interaction with existing toolbar (theme toggle, edit toggle, command palette)\n- Flagged for human review before share/connection coding begins","notes":"Spec refs: data-model.md §UI/UX Changes (owner: share + room mode + connection + peer strip; reviewer: outbox + owner-offline state). Existing toolbar: search web/src/lib/ for theme toggle and edit toggle to inventory current real estate. Output path: planning/collab/ui/connection-share.md. Blocks Phase 2 share, connection-badge, peer-strip issues.","status":"closed","priority":1,"issue_type":"decision","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:45Z","created_by":"James Lal","updated_at":"2026-05-19T17:58:58Z","closed_at":"2026-05-19T17:58:58Z","close_reason":"Design docs landed in planning/collab/ui/ for ongoing reference","labels":["human"],"dependencies":[{"issue_id":"attn-nnj.10.3","depends_on_id":"attn-nnj.10","type":"parent-child","created_at":"2026-05-18T16:28:44Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":9,"comment_count":0} +{"id":"attn-nnj.10.2","title":"Design: inline decoration system in ProseMirror","description":"Design how the four anchor states render in the editor surface: exact (\u003e=0.90), remapped+moved (0.70-0.89), ambiguous (panel-only), and stale (panel-only). Decide between underline / highlight / margin marker treatments, hover affordance, and click-to-focus-panel interaction. Output planning/collab/ui/inline-decorations.md with concrete CSS and PM Decoration sketches.","acceptance_criteria":"- planning/collab/ui/inline-decorations.md exists with concrete CSS + PM Decoration sketches per state\n- Confidence cutoffs from amendments.md Decision #15 quoted verbatim\n- Hover and click-to-focus behaviors specified\n- Overlap handling addressed (multiple decorations covering same range)\n- Flagged for human review before Phase 2 decoration plugin coding","notes":"Spec refs: amendments.md Decision #15 (UI cutoffs: \u003e=0.90 inline no badge, 0.70-0.89 inline + 'moved' badge, ambiguous panel-only, stale panel-only). Existing PM plugins: web/src/lib/prosemirror/{math,tables,code-highlight,code-block-nodeview,mermaid-nodeview}.ts — mirror their decoration plugin pattern. Output path: planning/collab/ui/inline-decorations.md. Blocks Phase 2 decoration plugin issue.","status":"closed","priority":1,"issue_type":"decision","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:44Z","created_by":"James Lal","updated_at":"2026-05-19T17:58:44Z","closed_at":"2026-05-19T17:58:44Z","close_reason":"Design docs landed in planning/collab/ui/ for ongoing reference","labels":["human"],"dependencies":[{"issue_id":"attn-nnj.10.2","depends_on_id":"attn-nnj.10","type":"parent-child","created_at":"2026-05-18T16:28:44Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":7,"comment_count":0} +{"id":"attn-nnj.10.1","title":"Design: review margin layout \u0026 sticky-card model (was: panel layout)","description":"REWRITTEN after user pivot from panel-river to Google-Docs-style margin sticky cards. The original panel-river exploration is kept as context (3 candidates explored) but the recommendation now: margin overlay with cards vertically anchored, orphan tray for ambiguous/stale, decorations from 10.2 click-to-focus the margin card. Update the doc at planning/collab/ui/review-panel-design.md in place.","acceptance_criteria":"- planning/collab/ui/review-panel-design.md exists with 2-3 ASCII mockups\n- Recommendation called out explicitly with rationale\n- Covers: grouping (file/snapshot/thread), resolved collapse, density at 30 comments, picker shape for ambiguous\n- Cross-references data-model.md §UI/UX Changes\n- Flagged for human review before Phase 2 panel coding begins","notes":"Spec refs: planning/collab/data-model.md §UI/UX Changes (lines ~776+); amendments.md Decision #15 cutoffs. Relevant existing files: web/src/lib/Sidebar.svelte (rail patterns), web/src/lib/CommandPalette.svelte (overlay patterns). Output path: planning/collab/ui/review-panel-design.md. This blocks Phase 2 panel/ambiguous/stale/decoration issues.","status":"closed","priority":1,"issue_type":"decision","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:43Z","created_by":"James Lal","updated_at":"2026-05-19T01:15:34Z","closed_at":"2026-05-19T01:15:34Z","close_reason":"Implemented in parallel worktrees; merged into collab","labels":["human"],"dependencies":[{"issue_id":"attn-nnj.10.1","depends_on_id":"attn-nnj.10","type":"parent-child","created_at":"2026-05-18T16:28:43Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":7,"comment_count":0} +{"id":"attn-nnj.3.8","title":"window.__attn__.reviewAnchorResolution IPC callback","description":"Wire ResolvedAnchor updates from the Rust ReviewManager to the frontend whenever a snapshot lands or the owner's document changes. The frontend store subscribes and re-runs the TS resolver mirror to produce inline decorations. This is the live channel that keeps comment highlights stable across edits.","acceptance_criteria":"- ReviewManager invokes evaluate_script(window, 'window.__attn__.reviewAnchorResolution(...)') with the latest ResolvedAnchor batch on: (a) new SnapshotCreated arriving, (b) new comment/suggestion event arriving, (c) owner save / WorkingCopy revision producing a new currentHash.\n- The IPC payload is JSON: { fileId, roomId, resolutions: [{ eventId, resolved: ResolvedAnchor }] }.\n- web/src/lib/review/store.ts (or equivalent) exposes a reactive store the Editor.svelte review extension subscribes to.\n- The TS resolver mirror runs locally on every PM transaction to update decorations between Rust pushes (no flicker).\n- Extended mock-ipc.ts emits the same callback shape so frontend dev works without a running daemon (per amendments.md §Mock IPC must be extended).","notes":"Spec: planning/collab/data-model.md §Anchor Resolution + planning/collab/amendments.md §Mock IPC must be extended (line ~76). Files: src/review/manager.rs, src/ipc.rs, web/src/lib/review/store.ts, web/src/lib/mock-ipc.ts. Rust side reuses the existing evaluate_script pattern used by other __attn__ callbacks. Frontend should debounce its own re-resolves so a burst of PM transactions doesn't thrash decorations.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:34Z","created_by":"James Lal","updated_at":"2026-05-19T02:30:01Z","closed_at":"2026-05-19T02:30:01Z","close_reason":"Implemented (3.8+round-10 retries); merged into collab; 267 Rust + 144 relay tests pass","dependencies":[{"issue_id":"attn-nnj.3.8","depends_on_id":"attn-nnj.2.8","type":"blocks","created_at":"2026-05-18T16:38:21Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.3.8","depends_on_id":"attn-nnj.3","type":"parent-child","created_at":"2026-05-18T16:28:33Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.3.8","depends_on_id":"attn-nnj.3.1","type":"blocks","created_at":"2026-05-18T16:29:46Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.2.1","title":"Crate layout: src/review/ module skeleton","description":"Create the src/review/ module tree as empty stubs with module-level docs taken from data-model.md §New Rust Modules. Wires `mod review;` into src/main.rs so subsequent issues have a place to land code.","acceptance_criteria":"- src/review/{mod.rs, ids.rs, model.rs, store.rs, working_copy.rs, manager.rs, transport.rs, apply.rs, ipc.rs} exist\n- Each file has a module-level //! doc comment summarizing its responsibility (verbatim from data-model.md §New Rust Modules where applicable)\n- src/main.rs declares `mod review;` and compiles\n- `cargo check` passes; no warnings about unused modules (use #[allow(dead_code)] on stubs)\n- No business logic yet — pure scaffolding","notes":"Spec: planning/collab/data-model.md §Rust Architecture Changes §New Rust Modules. Keep mod.rs as just `pub mod ...;` re-exports. The crypto/ subdir lives separately (owned by Phase 0a).","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:33Z","created_by":"James Lal","updated_at":"2026-05-18T23:29:27Z","closed_at":"2026-05-18T23:29:27Z","close_reason":"Implemented via parallel worktree agents; merged into collab","dependencies":[{"issue_id":"attn-nnj.2.1","depends_on_id":"attn-nnj.2","type":"parent-child","created_at":"2026-05-18T16:28:32Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":8,"comment_count":0} +{"id":"attn-nnj.3.6","title":"Markdown-edit anchor test corpus","description":"Hand-curated test corpus that pins resolver behavior across the realistic markdown edit shapes a reviewer will encounter. The same corpus drives both the Rust and TS resolvers (same Anchor input + same AnchorIndex inputs -\u003e same ResolvedAnchor output). Catching disagreement here is the only way to prove the two impls stay in lockstep.","acceptance_criteria":"- planning/collab/test-vectors/anchor-cases/ contains ~50 numbered case directories.\n- Each case has original.md, edited.md, anchor.json (the Anchor produced from a selection in original.md), and expected.json (the expected ResolvedAnchor verdict against edited.md).\n- Coverage includes: exact byte match, paragraph reordered, heading renamed, list item inserted before, code-block reflow (whitespace inside fence), quote unchanged but block split into two, ambiguous duplicate paragraphs, fully deleted (stale), math/mermaid round-trip, structure-only block-level anchor, fuzzy quote with one-word change, line-proximity-only fallback.\n- A test runner in src/review/anchors/tests.rs and a vitest spec in web/src/lib/review/resolver.test.ts iterate the corpus and assert the Rust + TS resolvers each produce the expected verdict (status + reason + currentRange).\n- README.md in anchor-cases/ documents the corpus contract.","notes":"Spec: planning/collab/data-model.md §Anchor Resolution + planning/collab/amendments.md Decision #15. Build the corpus as JSON-on-disk so both languages consume it without a code-gen step. The math/mermaid cases require the index builder's Decision #16 work to land first. Cases that depend on local pmSteps mapping should include a stepsJournal.json (omit otherwise).","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:21Z","created_by":"James Lal","updated_at":"2026-05-19T01:56:01Z","closed_at":"2026-05-19T01:56:01Z","close_reason":"Implemented; merged into collab; 254 Rust tests + 130 relay tests pass; corpus replay green","dependencies":[{"issue_id":"attn-nnj.3.6","depends_on_id":"attn-nnj.3","type":"parent-child","created_at":"2026-05-18T16:28:21Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.3.6","depends_on_id":"attn-nnj.3.4","type":"blocks","created_at":"2026-05-18T16:29:47Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.3.6","depends_on_id":"attn-nnj.3.5","type":"blocks","created_at":"2026-05-18T16:29:47Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.3.5","title":"Anchor resolver — TS mirror impl for inline decorations","description":"Mirror the Rust resolver in TypeScript so the frontend can drive ProseMirror decorations as the user edits, without round-tripping through Rust on every keystroke. Operates purely on plaintext data — the frontend only ever holds decrypted ReviewEvents (Rust does decrypt + verify). Must produce identical ResolvedAnchor verdicts as the Rust resolver for every case in the test corpus.","acceptance_criteria":"- web/src/lib/review/resolver.ts exports resolve(anchor: Anchor, currentIndex: AnchorIndex, pmState: EditorState, localSteps?: Step[]) -\u003e ResolvedAnchor.\n- All eight resolution steps implemented matching the Rust algorithm; combine + dedup logic identical.\n- Same confidence weights and verdict cutoffs as the Rust impl (shared constants exported so both call sites agree).\n- For every case in the markdown-edit test corpus, the TS verdict matches the Rust verdict exactly (status, reason, currentRange).\n- Vitest unit tests cover the same happy paths and ambiguous-threshold edge cases as the Rust tests.","notes":"Spec: planning/collab/data-model.md §Anchor Resolution + planning/collab/amendments.md Decision #15. Drive inline ProseMirror decorations only — apply / verification stay in Rust. Files: web/src/lib/review/resolver.ts. PM step mapping uses the existing prosemirror-transform Step.map API; pmRange is derived locally and not persisted. Confidence weight constants should mirror the Rust constants (consider generating them from a shared JSON in test-vectors/ to prevent drift).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:00Z","created_by":"James Lal","updated_at":"2026-05-19T01:55:48Z","closed_at":"2026-05-19T01:55:48Z","close_reason":"Implemented; merged into collab; 254 Rust tests + 130 relay tests pass; corpus replay green","dependencies":[{"issue_id":"attn-nnj.3.5","depends_on_id":"attn-nnj.3","type":"parent-child","created_at":"2026-05-18T16:27:59Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.3.5","depends_on_id":"attn-nnj.3.1","type":"blocks","created_at":"2026-05-18T16:29:44Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} +{"id":"attn-nnj.1.9","title":"Envelope assemble/disassemble end-to-end integration test","description":"Integration test that exercises the full crypto stack: build ReviewEvent → canonicalize → sign → AEAD-encrypt → wrap in MailboxEnvelope JSON → parse → decrypt → verify signature → recover original ReviewEvent. Locks the contract between issues 3-8.","acceptance_criteria":"- tests/review_crypto_envelope.rs (or src/review/crypto/tests.rs) contains the full roundtrip\n- Test uses planning/collab/test-vectors/envelope.json as both input and expected output\n- Asserts: re-serialized envelope is byte-identical to fixture; decrypted body matches original ReviewEvent; signature verifies; signingKeyId matches\n- Tamper tests: flipping any byte in ciphertext/AAD/signature → explicit error (not silent corruption)\n- Test runs in `cargo test` without network or filesystem deps","notes":"Spec: planning/collab/crypto-spec.md §What Is Signed vs. Encrypted + §Envelope Encryption. This is the canary that integrates HKDF + AEAD + Ed25519 + canonical-JSON + IDs. If any of those change subtly, this test breaks. Keep fixtures in test-vectors/envelope.json (shared with future browser impl).","status":"closed","priority":1,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:52Z","created_by":"James Lal","updated_at":"2026-05-19T00:39:39Z","closed_at":"2026-05-19T00:39:39Z","close_reason":"Implemented in parallel; merged into collab; 166 tests pass + relay 35 tests pass","dependencies":[{"issue_id":"attn-nnj.1.9","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:27:52Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.9","depends_on_id":"attn-nnj.1.1","type":"blocks","created_at":"2026-05-18T16:28:10Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.9","depends_on_id":"attn-nnj.1.2","type":"blocks","created_at":"2026-05-18T16:28:14Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.9","depends_on_id":"attn-nnj.1.5","type":"blocks","created_at":"2026-05-18T16:28:18Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.9","depends_on_id":"attn-nnj.1.6","type":"blocks","created_at":"2026-05-18T16:28:19Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.9","depends_on_id":"attn-nnj.1.8","type":"blocks","created_at":"2026-05-18T16:28:19Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":5,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.3.4","title":"Anchor resolver — run-all-and-combine policy (Rust)","description":"Canonical Rust anchor resolver per Decision #15. Given an Anchor (from a decrypted ReviewEvent), the current AnchorIndex, and an optional local pmSteps journal, run every applicable resolution step, dedup candidates by currentRange, and emit a ResolvedAnchor. This is the authoritative verdict used by the apply flow (Phase 5) and surfaced to the frontend via reviewAnchorResolution IPC.","acceptance_criteria":"- src/review/anchors/resolve.rs exposes resolve(anchor: \u0026Anchor, current_index: \u0026AnchorIndex, local_steps: Option\u003c\u0026PmStepJournal\u003e) -\u003e ResolvedAnchor matching data-model.md §Anchor Resolution.\n- All eight steps run: (1) base_hash match -\u003e exact 1.00; (2) mapped pm steps -\u003e exact 0.98; (3) unique exact quote -\u003e remapped 0.90; (4) block fingerprint -\u003e remapped 0.85; (5) structure + quote -\u003e remapped 0.80; (6) context (prefix/quote/suffix) -\u003e remapped 0.70; (7) bounded fuzzy quote -\u003e remapped 0.50-0.75; (8) line proximity -\u003e 0..=0.35.\n- Candidates from all steps are combined into a single set deduped by currentRange (highest confidence wins on dupes).\n- Verdict rules per Decision #15: exactly one candidate \u003e=0.70 -\u003e remapped/exact; two+ candidates \u003e=0.70 within 0.10 of each other -\u003e ambiguous with all candidates \u003e=0.50; otherwise top candidate \u003e=0.35 -\u003e remapped (low-confidence); else stale.\n- Pure function, no I/O, no crypto. Confidence weights live in a single constant so the calibration task can tune them.\n- Unit tests cover each step's happy path + the ambiguous threshold boundary at 0.09/0.10/0.11.","notes":"Spec: planning/collab/data-model.md §Anchor Resolution (lines 443-513) + planning/collab/amendments.md Decision #15 (line ~331) + §Anchor resolver disagreement policy (line ~110). Files: src/review/anchors/resolve.rs. The pmSteps journal type comes from Phase 0b LocalRevision work. Confidence numbers from data-model.md lines 491-506 ship as starting values; do not hard-code them inline — put them behind a ConfidenceWeights struct for the calibration task.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:45Z","created_by":"James Lal","updated_at":"2026-05-19T01:55:35Z","closed_at":"2026-05-19T01:55:35Z","close_reason":"Implemented; merged into collab; 254 Rust tests + 130 relay tests pass; corpus replay green","dependencies":[{"issue_id":"attn-nnj.3.4","depends_on_id":"attn-nnj.2.2","type":"blocks","created_at":"2026-05-18T16:38:21Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.3.4","depends_on_id":"attn-nnj.3","type":"parent-child","created_at":"2026-05-18T16:27:44Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.3.4","depends_on_id":"attn-nnj.3.1","type":"blocks","created_at":"2026-05-18T16:29:44Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} +{"id":"attn-nnj.1.8","title":"ID helpers: RoomId, FileId, EventId, EnvelopeId, SnapshotId, ContentHash","description":"Typed newtypes + derivation functions for every ID/hash in the system. EventId is computed from canonical(body + meta-without-eventId) then written back into meta.eventId. EnvelopeId form differs by envelope kind. Use \"attn file v2\" prefix per amendments.md (NOT \"attn file\").","acceptance_criteria":"- src/review/ids.rs defines newtypes: RoomId, FileId, EventId, EnvelopeId, SnapshotId, ContentHash, ParticipantId, DeviceId (each wraps String with base64url-no-pad)\n- derive_event_id(body, meta_without_event_id) -\u003e EventId per crypto-spec.md §EventId\n- derive_envelope_id(kind, body) -\u003e EnvelopeId per §EnvelopeId (event kind uses eventId-based deterministic form; signal/snapshot_blob use clientNonce)\n- derive_file_id(...) uses prefix \"attn file v2\" per amendments.md §Codebase Corrections (NOT \"attn file\")\n- derive_snapshot_id, derive_content_hash match their spec sections\n- planning/collab/test-vectors/event-id.json + envelope.json populated with (inputs, expected_id) tuples\n- Roundtrip tests pass against both fixtures","notes":"Spec: planning/collab/crypto-spec.md §ID Construction (all subsections) + amendments.md §Codebase Corrections (file prefix correction). All IDs serialize as base64url no-pad strings via serde. ContentHash per §ContentHash: canonical UTF-8 markdown bytes, no BOM, LF line endings, preserve trailing-newline as authored.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:37Z","created_by":"James Lal","updated_at":"2026-05-19T00:23:30Z","closed_at":"2026-05-19T00:23:30Z","close_reason":"Implemented in parallel worktrees; merged into collab; 142 tests pass","dependencies":[{"issue_id":"attn-nnj.1.8","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:27:36Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.8","depends_on_id":"attn-nnj.1.1","type":"blocks","created_at":"2026-05-18T16:28:09Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.8","depends_on_id":"attn-nnj.1.2","type":"blocks","created_at":"2026-05-18T16:28:14Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.8","depends_on_id":"attn-nnj.1.3","type":"blocks","created_at":"2026-05-18T16:28:17Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":2,"comment_count":0} +{"id":"attn-nnj.3.3","title":"Anchor construction from selection (frontend)","description":"Build the layered Anchor used by review events from a ProseMirror selection plus the AnchorIndex received in the most recent SnapshotCreated event. Produces all available layers — position from selection, quote from selected text, block lookup from the index, bounded context prefix/suffix, structure from headingPath — then hands the plaintext Anchor up to Rust via IPC for signing+encryption. The frontend never sees ciphertext.","acceptance_criteria":"- web/src/lib/review/anchors.ts exports buildAnchor(selection, anchorIndex, fileId, snapshotId, baseHash) -\u003e Anchor matching data-model.md §Anchors schema.\n- position: byteRange + lineRange + pmRange computed from the current ProseMirror selection.\n- quote: exact + exactHash + normalized + normalizedHash for non-empty selections; omitted for block-level comments.\n- block: looked up from the AnchorIndex by covering byteRange; carries snapshotBlockId, contentFingerprint, kind, offsetInBlockBytes, blockByteRange, blockLineRange.\n- context.prefix and context.suffix are bounded to at most 160 characters (data-model.md §Anchors bounded plaintext fields).\n- Unit tests with vitest cover: inline selection inside a paragraph, selection spanning two paragraphs, block-level (caret-only) anchor, selection at file start/end, selection inside a code block.","notes":"Spec: planning/collab/data-model.md §Anchors (lines 381-441). The AnchorIndex arrives via window.__attn__.reviewSnapshot(...) callbacks (mocked in web/src/lib/mock-ipc.ts during Phase 2). Files: web/src/lib/review/anchors.ts. Hashes use the same canonical sha256 helpers as Rust (web/src/lib/review/crypto.ts from Phase 0a). Send the constructed Anchor to Rust via window.__attn__.reviewSubmit or equivalent IPC — actual signing/encrypting happens in ReviewManager.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:33Z","created_by":"James Lal","updated_at":"2026-05-19T02:29:48Z","closed_at":"2026-05-19T02:29:48Z","close_reason":"Implemented (3.8+round-10 retries); merged into collab; 267 Rust + 144 relay tests pass","dependencies":[{"issue_id":"attn-nnj.3.3","depends_on_id":"attn-nnj.3","type":"parent-child","created_at":"2026-05-18T16:27:33Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.3.3","depends_on_id":"attn-nnj.3.1","type":"blocks","created_at":"2026-05-18T16:29:43Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.1.7","title":"Hashcash PoW mint + verify with per-method/path token pool","description":"Implement hashcash proof-of-work tokens per crypto-spec.md §Hashcash. Mint runs off-thread via tokio::task::spawn_blocking (cancellable). Maintain a small per-(method,path) token pool so common writes don't block on cold mints.","acceptance_criteria":"- src/review/crypto/pow.rs exposes `mint(resource, difficulty) -\u003e Future\u003cToken\u003e` and `verify(token, resource, difficulty) -\u003e Result\u003c()\u003e`\n- Token format matches crypto-spec.md §Token Format byte-for-byte (version, resource, salt, counter, hash)\n- Mint executes inside tokio::task::spawn_blocking; cancellation drops the task cleanly\n- Per-(method, path) token pool with configurable max-size (e.g., 4 per slot); replenishes lazily\n- Default difficulty 16; room override accepted in [12, 24] inclusive (reject outside)\n- planning/collab/test-vectors/pow.json populated with (resource, difficulty, valid_token, invalid_tokens)\n- Roundtrip + invalid-difficulty + tampered-resource tests pass","notes":"Spec: planning/collab/crypto-spec.md §Hashcash Proof-of-Work (§Token Format, §Hash Function, §Difficulty, §Server Validation, §Replay Protection, §Client Implementation). Hash function is SHA-256 over canonical token bytes. Bits checked are leading zero bits of the digest. Pool is a HashMap\u003c(String, String), VecDeque\u003cToken\u003e\u003e.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:25Z","created_by":"James Lal","updated_at":"2026-05-19T00:23:15Z","closed_at":"2026-05-19T00:23:15Z","close_reason":"Implemented in parallel worktrees; merged into collab; 142 tests pass","dependencies":[{"issue_id":"attn-nnj.1.7","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:27:24Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.7","depends_on_id":"attn-nnj.1.1","type":"blocks","created_at":"2026-05-18T16:28:09Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.7","depends_on_id":"attn-nnj.1.2","type":"blocks","created_at":"2026-05-18T16:28:13Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.7","depends_on_id":"attn-nnj.1.3","type":"blocks","created_at":"2026-05-18T16:28:16Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.1.6","title":"Ed25519 sign/verify wrapper","description":"Wrap ed25519-dalek v2 for signing the canonical bytes of (eventMeta || eventBody). Includes signing-key-ID verification: signingKeyId must equal SHA-256(publicSigningKey).","acceptance_criteria":"- src/review/crypto/signature.rs exposes `sign(signing_key, meta, body) -\u003e Signature` and `verify(public_key, meta, body, signature) -\u003e Result\u003c()\u003e`\n- Signed bytes = canonicalize(meta) || canonicalize(body) per crypto-spec.md §Signatures §Canonical Bytes for Signature\n- verify also checks signingKeyId field == base64url-no-pad(SHA-256(public_key_bytes)) and rejects mismatch\n- planning/collab/test-vectors/event-signature.json populated with deterministic (private_key, meta, body, signature, signingKeyId) tuples\n- Roundtrip + bad-signature + wrong-key-id tests pass against fixture","notes":"Spec: planning/collab/crypto-spec.md §Signatures. Use ed25519_dalek::SigningKey and VerifyingKey. SigningKey impls Zeroize. Canonicalization is via the JCS helper from issue 3. SignatureId formatting uses base64url no-pad.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:12Z","created_by":"James Lal","updated_at":"2026-05-19T00:23:00Z","closed_at":"2026-05-19T00:23:00Z","close_reason":"Implemented in parallel worktrees; merged into collab; 142 tests pass","dependencies":[{"issue_id":"attn-nnj.1.6","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:27:12Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.6","depends_on_id":"attn-nnj.1.1","type":"blocks","created_at":"2026-05-18T16:28:08Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.6","depends_on_id":"attn-nnj.1.2","type":"blocks","created_at":"2026-05-18T16:28:12Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.6","depends_on_id":"attn-nnj.1.3","type":"blocks","created_at":"2026-05-18T16:28:16Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.3.1","title":"AnchorIndex builder (Rust, comrak-based)","description":"Build the canonical AnchorIndex in Rust from a snapshot's UTF-8 markdown bytes. This is the authoritative anchor index per amendments.md Phase 1 Decision — the frontend never hashes; it only receives the pre-computed AnchorIndex inside SnapshotCreated events and uses it for resolution. Walks the comrak AST and emits AnchorBlock entries that the resolver and the inline-decoration pipeline depend on.","acceptance_criteria":"- src/review/anchors/index.rs exposes a pure function (markdown bytes, snapshot_id) -\u003e AnchorIndex matching data-model.md §Anchor Index schema (docHash, canonicalEncoding='utf8-bytes', lineCount, blocks[], headings[]).\n- Each AnchorBlock has: kind (heading|paragraph|list_item|code_block|blockquote|table|thematic_break|html|math|mermaid|unknown), byteRange, lineRange, headingPath, ordinalInParent, duplicateOrdinal, textHash, normalizedTextHash, previousBlockHash, nextBlockHash, contentFingerprint, snapshotBlockId.\n- contentFingerprint = sha256(kind || normalizedText || headingPath || duplicateOrdinal); snapshotBlockId = sha256(snapshotId || byteRange || contentFingerprint).\n- Duplicate paragraphs/list-items get distinct duplicateOrdinal values; identical content in different headingPaths yields distinct contentFingerprints.\n- Unit tests cover empty docs, single-block docs, nested-heading docs, duplicate paragraphs, and a fixture from tests/fixtures/.","notes":"Spec: planning/collab/data-model.md §Anchor Index (lines 314-379) + planning/collab/amendments.md Phase 1 Decision (line ~242). Use comrak's AST (existing dep in src/markdown.rs). Sibling crate sha2 (already in Cargo.toml per amendments.md Phase 0a). Files: src/review/anchors/index.rs, src/review/anchors/mod.rs. The 'pmRange' field on AnchorBlock is optional and not populated here (frontend-only derivation).","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:08Z","created_by":"James Lal","updated_at":"2026-05-19T01:16:00Z","closed_at":"2026-05-19T01:16:00Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.3.1","depends_on_id":"attn-nnj.1.8","type":"blocks","created_at":"2026-05-18T16:38:20Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.3.1","depends_on_id":"attn-nnj.3","type":"parent-child","created_at":"2026-05-18T16:27:07Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":5,"comment_count":0} +{"id":"attn-nnj.1.5","title":"AEAD wrapper (XChaCha20-Poly1305 with AAD)","description":"Wrap XChaCha20-Poly1305 with the envelope-binding AAD format from crypto-spec.md. The AAD ties ciphertext to envelope metadata so a misrouted envelope decrypts to a tag failure rather than silent corruption.","acceptance_criteria":"- src/review/crypto/aead.rs exposes `seal(key, plaintext, aad) -\u003e (nonce, ciphertext)` and `open(key, nonce, ciphertext, aad) -\u003e Result\u003cVec\u003cu8\u003e\u003e`\n- Nonce is 24 bytes from `getrandom` (random, not counter)\n- AAD is canonical JSON of {v, roomId, envelopeId, kind, authorId, deviceId, createdAt} per crypto-spec.md §Envelope Encryption\n- Tampered AAD or ciphertext → Open returns explicit AeadError\n- planning/collab/test-vectors/aead.json populated with (key, nonce, aad, plaintext, ciphertext) tuples\n- Roundtrip + tamper tests pass against fixture","notes":"Spec: planning/collab/crypto-spec.md §Envelope Encryption (AEAD) §Nonce Discipline. Use chacha20poly1305::XChaCha20Poly1305. Key/nonce types should be wrappers that Zeroize. The AAD canonicalization MUST use the canonical JSON helper from issue 3.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:02Z","created_by":"James Lal","updated_at":"2026-05-19T00:22:45Z","closed_at":"2026-05-19T00:22:45Z","close_reason":"Implemented in parallel worktrees; merged into collab; 142 tests pass","dependencies":[{"issue_id":"attn-nnj.1.5","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:27:01Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.5","depends_on_id":"attn-nnj.1.1","type":"blocks","created_at":"2026-05-18T16:28:08Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.5","depends_on_id":"attn-nnj.1.2","type":"blocks","created_at":"2026-05-18T16:28:12Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.5","depends_on_id":"attn-nnj.1.3","type":"blocks","created_at":"2026-05-18T16:28:15Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.5","depends_on_id":"attn-nnj.1.4","type":"blocks","created_at":"2026-05-18T16:28:18Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":4,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.1.4","title":"HKDF wrapper + room key derivation","description":"Wrap HKDF-SHA-256 and derive the five per-room subkeys from a 32-byte room secret. Info strings must match crypto-spec.md byte-for-byte so a future browser impl produces identical keys.","acceptance_criteria":"- src/review/crypto/kdf.rs exposes `derive_room_keys(room_secret: \u0026[u8; 32]) -\u003e RoomKeys`\n- RoomKeys struct fields: root_key, event_key, snapshot_key, signaling_key, admission_key (each 32 bytes, zeroize on drop)\n- Info strings match spec exactly: \"attn room root v2\", \"attn room event v2\", \"attn room snapshot v2\", \"attn room signaling v2\", \"attn room admission v2\"\n- planning/collab/test-vectors/kdf.json populated with deterministic vectors (fixed room_secret → exact derived keys hex)\n- Roundtrip test verifies all 5 keys match fixture","notes":"Spec: planning/collab/crypto-spec.md §Key Derivation. Use hkdf crate with Sha256. Salt is empty (or zero-filled per HKDF-Extract spec). RoomKeys impls Zeroize + Drop.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:26:37Z","created_by":"James Lal","updated_at":"2026-05-19T00:22:31Z","closed_at":"2026-05-19T00:22:31Z","close_reason":"Implemented in parallel worktrees; merged into collab; 142 tests pass","dependencies":[{"issue_id":"attn-nnj.1.4","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:26:37Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.4","depends_on_id":"attn-nnj.1.1","type":"blocks","created_at":"2026-05-18T16:28:07Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.4","depends_on_id":"attn-nnj.1.2","type":"blocks","created_at":"2026-05-18T16:28:11Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.4","depends_on_id":"attn-nnj.1.3","type":"blocks","created_at":"2026-05-18T16:28:15Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.1.3","title":"Canonical JSON helper (RFC 8785 JCS)","description":"Implement RFC 8785 JSON Canonicalization Scheme in src/review/crypto/canonical.rs. Used by every signature and AEAD AAD computation, so determinism is non-negotiable. Generates test vectors into canonical-json.jsonl.","acceptance_criteria":"- src/review/crypto/canonical.rs exposes `canonicalize(value: \u0026serde_json::Value) -\u003e Vec\u003cu8\u003e`\n- Object keys sorted ASCII-ascending; no whitespace; UTF-8 no BOM\n- Integers only in signed payloads — floats reject with explicit error\n- Absent fields are omitted (never serialized as `\"key\": null`)\n- planning/collab/test-vectors/canonical-json.jsonl populated with edge cases (unicode keys, nested objects, integer boundaries, escape sequences)\n- Roundtrip test: every vector parses, re-canonicalizes byte-identical","notes":"Spec: planning/collab/crypto-spec.md §Canonical JSON (RFC 8785 JCS). Don't use serde_json's default serializer — it doesn't sort keys. Either use a BTreeMap intermediate or implement a custom Serializer. Reject NaN/Infinity. UTF-16 surrogate handling per JCS.","status":"closed","priority":1,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:26:27Z","created_by":"James Lal","updated_at":"2026-05-19T00:03:53Z","closed_at":"2026-05-19T00:03:53Z","close_reason":"Implemented; merged into collab; 23 canonical tests + corpus filled","dependencies":[{"issue_id":"attn-nnj.1.3","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:26:27Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.3","depends_on_id":"attn-nnj.1.1","type":"blocks","created_at":"2026-05-18T16:28:06Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.1.3","depends_on_id":"attn-nnj.1.2","type":"blocks","created_at":"2026-05-18T16:28:10Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":5,"comment_count":0} +{"id":"attn-nnj.1.2","title":"Test-vector corpus directory + schema headers","description":"Create planning/collab/test-vectors/ as the canonical contract that all crypto implementations (Rust now, browser/WASM later) must satisfy. Each file gets a documented schema header so future revalidators know what to expect.","acceptance_criteria":"- planning/collab/test-vectors/ exists with: kdf.json, canonical-json.jsonl, event-signature.json, event-id.json, aead.json, envelope.json, pow.json\n- Each file has a top-level schema comment / metadata block describing field semantics + spec section reference\n- README.md in test-vectors/ explains how to regenerate and how to validate\n- Files are placeholders (empty arrays / empty .jsonl) — actual vectors are filled in by issues 3-9","notes":"Spec: planning/collab/crypto-spec.md §Test Vectors §Test Vectors (to ship in the repo). JSONL means newline-delimited JSON. The Rust impl writes these on `cargo test --features generate-vectors` (or similar) and reads them on every test run.","status":"closed","priority":1,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:26:16Z","created_by":"James Lal","updated_at":"2026-05-18T23:44:55Z","closed_at":"2026-05-18T23:44:55Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.1.2","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:26:16Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":8,"comment_count":0} +{"id":"attn-nnj.1.1","title":"Cargo deps + binary-size baseline for crypto crate","description":"Add Rust crypto dependencies to Cargo.toml and record binary-size baseline. This establishes the dependency footprint before any crypto code lands so we can compare against the Phase 4 webrtc-rs cost.","acceptance_criteria":"- Cargo.toml adds: sha2, hkdf, chacha20poly1305, ed25519-dalek v2, base64 (URL_SAFE_NO_PAD), getrandom, zeroize\n- `cargo tree -e features --no-default-features --no-dev-dependencies` output captured in notes\n- Release binary size (cargo build --release) recorded as baseline for Phase 4 comparison\n- `task dev` and `cargo check` both pass with new deps\n- No code uses the deps yet — just declared","notes":"Spec: planning/collab/crypto-spec.md §Primitives §Rust crates. Pin versions exactly. Use `base64::engine::general_purpose::URL_SAFE_NO_PAD` (no padding). zeroize is for SecretKey/Drop impls.","status":"closed","priority":1,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:26:06Z","created_by":"James Lal","updated_at":"2026-05-18T23:44:40Z","closed_at":"2026-05-18T23:44:40Z","close_reason":"Implemented in parallel worktrees; merged into collab","dependencies":[{"issue_id":"attn-nnj.1.1","depends_on_id":"attn-nnj.1","type":"parent-child","created_at":"2026-05-18T16:26:06Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":7,"comment_count":0} +{"id":"attn-nnj.10","title":"UI/UX: Review surfaces design + iteration","description":"Cross-cutting UI/UX workstream. Discovery + interaction design for share button, room mode selector, connection badge, peer strip, review panel layout, comment/suggestion composer, inline highlight system, ambiguous anchor picker, stale comment panel, snapshot badge/age/superseded, three-way apply UI, outbox indicator, reviewer banner. Drives Phase 2 and feeds into Phase 5.","notes":"User direction: UI/UX is important. Treated as a peer workstream rather than tail-end polish.","status":"closed","priority":1,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:18Z","created_by":"James Lal","updated_at":"2026-05-19T18:01:15Z","closed_at":"2026-05-19T18:01:15Z","close_reason":"Phase complete — all implementation issues closed","dependencies":[{"issue_id":"attn-nnj.10","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:18Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.5","title":"Phase 3a: Relay worker (Cloudflare DO+R2)","description":"Implement relay/ from relay-spec.md against Miniflare. WS-only delivery (decision #5), HMAC admission, hashcash PoW on all writes, room TTL alarms (24h hard-max + 1h idle), R2 spillover for large snapshots. Conformance corpus shared with the Rust client tests.","notes":"Spec: planning/collab/relay-spec.md. Wrangler/Miniflare; relay/ package does not yet exist.","status":"closed","priority":1,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:15Z","created_by":"James Lal","updated_at":"2026-05-19T17:59:57Z","closed_at":"2026-05-19T17:59:57Z","close_reason":"Phase complete — all implementation issues closed","dependencies":[{"issue_id":"attn-nnj.5","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:14Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.4","title":"Phase 2: Review UI with mocked transport","description":"Svelte review panel with comment/suggestion decorations in ProseMirror, anchor-aware highlighting, ambiguous picker, stale state. Mock-IPC extended with replayable event stream so UI work isn't blocked on Rust/network. Demonstrate a comment surviving owner edits using only the local anchor engine.","notes":"Spec: data-model.md §UI/UX Changes. UI/UX is a first-class workstream here per user direction.","status":"closed","priority":1,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:14Z","created_by":"James Lal","updated_at":"2026-05-19T17:59:42Z","closed_at":"2026-05-19T17:59:42Z","close_reason":"Phase complete — all implementation issues closed","dependencies":[{"issue_id":"attn-nnj.4","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:13Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.3","title":"Phase 1: Anchor engine","description":"Build AnchorIndex from markdown bytes in Rust (canonical), construct Anchors from selections, implement the run-all-and-combine resolution policy with confidence thresholds, ship hand-curated test corpus.","notes":"Spec: data-model.md §Anchor Index/Anchors/Anchor Resolution + amendments.md §Anchor resolver disagreement policy. AnchorIndex computed in Rust per amendments.md (canonical path); browser gets it pre-computed in SnapshotCreated events.","status":"closed","priority":1,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:13Z","created_by":"James Lal","updated_at":"2026-05-19T17:01:52Z","closed_at":"2026-05-19T17:01:52Z","close_reason":"All children closed","dependencies":[{"issue_id":"attn-nnj.3","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:13Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.2","title":"Phase 0b: Local data model + working copy","description":"Rust-only foundation. Typed IDs, serde model types, JSON/JSONL local store at ~/.attn/reviews/, WorkingCopyService replacing direct fs::write, revision journal, watcher self-write distinction, empty ReviewManager scaffold, AppState refactor for tab+room routing.","notes":"Spec: planning/collab/data-model.md §Local Replicas + §Rust Architecture Changes. AppState shape per amendments.md (RoomRuntimeHandle + file_to_room mapping).","status":"closed","priority":1,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:12Z","created_by":"James Lal","updated_at":"2026-05-19T17:01:37Z","closed_at":"2026-05-19T17:01:37Z","close_reason":"All children closed","dependencies":[{"issue_id":"attn-nnj.2","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:12Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.1","title":"Phase 0a: Crypto foundations","description":"Rust-only crypto crate (attn-collab-crypto): cipher suite primitives, key derivation, hashcash mint+verify, ID helpers. Frontend never holds ciphertext in v2 — IPC delivers plaintext ReviewEvents per data-model.md §Webview IPC Changes, so no TS crypto is needed for native. Test-vector corpus ships alongside Rust impl for forward compat (browser/Phase 6 will revisit WASM-vs-TS).","notes":"Spec: planning/collab/crypto-spec.md. Decision #4 locks the suite: XChaCha20-Poly1305 + Ed25519 + HKDF-SHA-256 + RFC 8785 JCS + base64url-no-pad. No agility in v2.","status":"closed","priority":1,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:11Z","created_by":"James Lal","updated_at":"2026-05-19T17:01:21Z","closed_at":"2026-05-19T17:01:21Z","close_reason":"All children closed","dependencies":[{"issue_id":"attn-nnj.1","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:11Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"attn-nnj","title":"attn collab v2","description":"End-to-end encrypted review collaboration over local markdown. Owner shares working copy via snapshot graph + encrypted event log; reviewers/agents add comments and suggestions anchored to snapshots; owner accepts suggestions locally. Transport via WebRTC DataChannel (Rust webrtc-rs) and a bounded encrypted mailbox on Cloudflare Workers/DO/R2.","notes":"Specs: planning/collab/{data-model,crypto-spec,relay-spec,amendments}.md. amendments.md overrides the others where they conflict. 16 design decisions locked.","status":"closed","priority":1,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:10Z","created_by":"James Lal","updated_at":"2026-05-19T18:01:47Z","closed_at":"2026-05-19T18:01:47Z","close_reason":"attn collab v2: all implementation closed across phases 0a/0b/0c/1/2/3a/3b/4/5/6, UI/UX, and cross-cutting. 432 Rust tests + 286 relay tests + 22.4 KB gz browser bundle.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-zb5.4.3","title":"Validate: HTML live reload","description":"Integration/E2E validation that on-disk edits to an html file update the in-app view.\n\n## Integration Scenarios\n- Open an html fixture; modify it on disk (append a visible element); the iframe refetches and shows the change without manual reload.\n- Confirm review-invite routing and other file types are unaffected by the query-string strip.\n\n## E2E Test Commands\n- `cargo test` ; `cd web \u0026\u0026 npm run check`.\n- `task dev`; edit the fixture; `attn --eval` confirms the iframe `?v=` bumped and `--screenshot` shows the new content.\n\n## Acceptance Criteria\n- Editing an html file on disk live-updates the rendered view; no regressions to invite routing or other viewers.\n\n## Plan Reference\n- planning/complete-plan.md §7, §8 Testing","status":"closed","priority":2,"issue_type":"task","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:12:38Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:54:26Z","closed_at":"2026-05-28T20:54:26Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.4.3","depends_on_id":"attn-zb5.4","type":"parent-child","created_at":"2026-05-28T14:12:37Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.4.3","depends_on_id":"attn-zb5.4.1","type":"blocks","created_at":"2026-05-28T14:12:58Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.4.3","depends_on_id":"attn-zb5.4.2","type":"blocks","created_at":"2026-05-28T14:12:58Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} +{"id":"attn-zb5.4.2","title":"Record html mtime in applyUpdateContent and cache-bust the iframe src","description":"Make the viewer refetch when the active html file changes on disk.\n\n## Files\n- web/src/App.svelte (applyUpdateContent, ~:1856-1892)\n- web/src/lib/HtmlViewer.svelte\n\n## Approach\n- The watcher already ships `contentMtimeMs` for the active file (type-agnostic). `applyUpdateContent` is currently markdown-gated and drops it for html — add an html branch that records the active file's mtime into reactive `$state` keyed by path.\n- Pass that mtime to `HtmlViewer`; derive `src = markdownSourceUrl(path) + '?v=' + mtime` so a change remounts/refetches the iframe.\n\n## Verification\n- `cd web \u0026\u0026 npm run check` \u0026\u0026 `cd web \u0026\u0026 npm test`.\n- Smoke: open a fixture html; edit it on disk; confirm the iframe `src` `?v=` value bumped and the new content shows (`attn --eval` reads the iframe src).\n\n## Plan Reference\n- planning/complete-plan.md §4 Frontend (applyUpdateContent), §7","status":"closed","priority":2,"issue_type":"task","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:12:37Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:51:22Z","closed_at":"2026-05-28T20:51:22Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.4.2","depends_on_id":"attn-zb5.4","type":"parent-child","created_at":"2026-05-28T14:12:37Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.4.2","depends_on_id":"attn-zb5.4.1","type":"blocks","created_at":"2026-05-28T14:12:57Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"attn-zb5.3.4","title":"Validate: HTML viewer frontend","description":"Integration/E2E validation that an AI-style html file renders and runs end-to-end in the app.\n\n## Integration Scenarios\n- Add `tests/fixtures/sample.html` with inline JS (e.g. a small animation), a custom web font, and a CDN animation lib reference.\n- Open it from the sidebar → renders in the iframe; the JS runs; font + lib load (visually polished).\n- Switch between an html tab and a markdown tab → routing is correct; no collab chrome on html.\n\n## E2E Test Commands\n- `cd web \u0026\u0026 npm run check` \u0026\u0026 `cd web \u0026\u0026 npm test`.\n- `task dev ATTN_PATH=tests/fixtures/sample.html`; `attn --query 'iframe'`, `attn --eval` to confirm the frame's script executed; `attn --screenshot`.\n\n## Acceptance Criteria\n- html files render with full fidelity (fonts, CDN libs, animations) and route correctly; collab/share stays hidden.\n\n## Plan Reference\n- planning/complete-plan.md §4 Frontend, §8 Testing","status":"closed","priority":2,"issue_type":"task","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:12:36Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:54:25Z","closed_at":"2026-05-28T20:54:25Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.3.4","depends_on_id":"attn-zb5.3","type":"parent-child","created_at":"2026-05-28T14:12:35Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.3.4","depends_on_id":"attn-zb5.3.1","type":"blocks","created_at":"2026-05-28T14:12:56Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.3.4","depends_on_id":"attn-zb5.3.2","type":"blocks","created_at":"2026-05-28T14:12:57Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.3.4","depends_on_id":"attn-zb5.3.3","type":"blocks","created_at":"2026-05-28T14:12:57Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":3,"dependent_count":0,"comment_count":0} +{"id":"attn-zb5.4.1","title":"Strip query string on the file-serve branch of the attn:// handler","description":"Allow `?v=\u003cmtime\u003e` cache-busting URLs to resolve to the real file.\n\n## Files\n- src/main.rs (custom-protocol file-serve branch, ~:573-579)\n\n## Approach\n- On the file-serve branch ONLY (after `parse_review_invite` and `is_reserved_localhost_review`), strip any `?…` query before percent-decoding/`fs::read`. Must not run before the invite/reserved checks (those legitimately treat `?` as a separator).\n\n## Verification\n- `cargo build` \u0026\u0026 `cargo test`.\n- Smoke: request `attn://localhost/\u003cabs\u003e/file.html?v=123` resolves to the file (200), and `attn://review/...` invites + reserved paths still behave correctly.\n\n## Plan Reference\n- planning/complete-plan.md §4 Backend (query-string strip), §7","status":"closed","priority":2,"issue_type":"task","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:12:36Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:51:22Z","closed_at":"2026-05-28T20:51:22Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.4.1","depends_on_id":"attn-zb5.4","type":"parent-child","created_at":"2026-05-28T14:12:36Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0} +{"id":"attn-zb5.3.2","title":"Create HtmlViewer.svelte (sandboxed iframe + header + states)","description":"New viewer component that renders an html file in a sandboxed iframe, mirroring ImageViewer's structure.\n\n## Files\n- web/src/lib/HtmlViewer.svelte (new)\n\n## Approach\n- `h-full` flex-fill container (iframes have no intrinsic height inside the mainContent ScrollArea — fill like ImageViewer/MediaPlayer).\n- `\u003ciframe sandbox=\"allow-scripts\" class=\"h-full w-full\" src={src}\u003e` (no allow-same-origin / popups / top-nav / forms).\n- Props: `path`, `mtime`. Derive `src = markdownSourceUrl(path)` (the `?v=\u003cmtime\u003e` cache-bust is added in F4).\n- Header bar: filename + an \"Open in browser\" button (reuse the external-open path the navigation handler provides for non-attn URLs).\n- Loading and empty states.\n\n## Verification\n- `cd web \u0026\u0026 npm run check`.\n- `cd web \u0026\u0026 npm test`.\n- Smoke (after App wiring task): renders a fixture html in the iframe.\n\n## Plan Reference\n- planning/complete-plan.md §4 Frontend (HtmlViewer.svelte)","status":"closed","priority":2,"issue_type":"task","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:12:35Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:51:21Z","closed_at":"2026-05-28T20:51:21Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.3.2","depends_on_id":"attn-zb5.3","type":"parent-child","created_at":"2026-05-28T14:12:34Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.3.2","depends_on_id":"attn-zb5.3.1","type":"blocks","created_at":"2026-05-28T14:12:55Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} +{"id":"attn-zb5.3.3","title":"Route html files to HtmlViewer in App.svelte mainContent()","description":"Wire the new viewer into the file-type routing so opening an html file shows it.\n\n## Files\n- web/src/App.svelte (~:2408 mainContent snippet)\n\n## Approach\n- Add `{:else if activeFileType === 'html'}` → `\u003cHtmlViewer path={activePath} mtime={…} /\u003e` before the `{:else}` unsupported fallback, mirroring the ImageViewer branch.\n- Ensure opening an html tab does not trigger collab/share chrome (those gates already check `activeFileType === 'markdown'`).\n\n## Verification\n- `cd web \u0026\u0026 npm run check`.\n- Smoke: `task dev ATTN_PATH=tests/fixtures/sample.html`; `attn --query 'iframe'` → iframe present; `--screenshot` shows the rendered page; collab/share UI hidden.\n\n## Plan Reference\n- planning/complete-plan.md §4 Frontend (App.svelte), §6","status":"closed","priority":2,"issue_type":"task","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:12:35Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:51:21Z","closed_at":"2026-05-28T20:51:21Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.3.3","depends_on_id":"attn-zb5.3","type":"parent-child","created_at":"2026-05-28T14:12:35Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.3.3","depends_on_id":"attn-zb5.3.2","type":"blocks","created_at":"2026-05-28T14:12:56Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"attn-zb5.3.1","title":"Add 'html' to FileType union, extension map, and icon","description":"Teach the frontend about the html file type so detection, routing, and iconography work.\n\n## Files\n- web/src/lib/types.ts (~:23)\n- web/src/lib/markdown-layer.ts (~:3 EXTENSIONS_BY_TYPE)\n- web/src/lib/CommandPalette.svelte (~:131 iconForType); check web/src/lib/icon-resolver.ts / Sidebar.svelte\n\n## Approach\n- Add `'html'` to the `FileType` union (this makes `Record\u003cFileType,…\u003e` require the key — compiler-enforced).\n- Add `html: ['html', 'htm']` to `EXTENSIONS_BY_TYPE` (detectFileType is data-driven — no body change).\n- Add a `case 'html'` arm to `iconForType` (pick a sensible icon, e.g. a code/file-code glyph); defaults are acceptable elsewhere.\n\n## Verification\n- `cd web \u0026\u0026 npm run check` (typecheck passes; the union change surfaces any missing Record key).\n- `cd web \u0026\u0026 npm test` (no regressions).\n\n## Plan Reference\n- planning/complete-plan.md §4 Frontend (types.ts, markdown-layer.ts, icons)","status":"closed","priority":2,"issue_type":"task","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:12:34Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:51:21Z","closed_at":"2026-05-28T20:51:21Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.3.1","depends_on_id":"attn-zb5.3","type":"parent-child","created_at":"2026-05-28T14:12:34Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0} +{"id":"attn-zb5.2.5","title":"Validate: sandbox hardening","description":"Integration/E2E validation that scripted HTML is safe and the legitimate app is unaffected.\n\n## Integration Scenarios\n- Open a hostile html fixture (inline script attempting: `window.ipc.postMessage(edit_save…)`, reading `window.__attn_init__`, `fetch('attn://localhost/\u003csecret\u003e')`) → all blocked: bridge absent, no token, CSP blocks the fetch.\n- Confirm legitimate app IPC (navigate, checkbox toggle, save) still works with the token attached.\n- Confirm a page CAN load remote fonts + a CDN animation lib (aesthetics path intact).\n\n## E2E Test Commands\n- `cargo test` (token-required test passes) ; `cd web \u0026\u0026 npm run check`.\n- `task dev` + `attn --eval`/`--query` to assert the above in a real frame.\n\n## Acceptance Criteria\n- Embedded scripts cannot write files, drive attn, or read other local files; remote fonts/CDN libs load; the app itself is fully functional.\n\n## Plan Reference\n- planning/complete-plan.md §5 Security model","status":"closed","priority":2,"issue_type":"task","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:11:56Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:54:25Z","closed_at":"2026-05-28T20:54:25Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.2.5","depends_on_id":"attn-zb5.2","type":"parent-child","created_at":"2026-05-28T14:11:56Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.2.5","depends_on_id":"attn-zb5.2.1","type":"blocks","created_at":"2026-05-28T14:12:53Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.2.5","depends_on_id":"attn-zb5.2.2","type":"blocks","created_at":"2026-05-28T14:12:54Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.2.5","depends_on_id":"attn-zb5.2.3","type":"blocks","created_at":"2026-05-28T14:12:54Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.2.5","depends_on_id":"attn-zb5.2.4","type":"blocks","created_at":"2026-05-28T14:12:55Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0} +{"id":"attn-zb5.2.3","title":"Add subframe guard to neutralize the bridge inside iframes","description":"Strip the native bridge from any subframe so embedded HTML scripts cannot reach window.ipc / window.webkit at all.\n\n## Files\n- src/main.rs (build_initialization_script)\n\n## Approach\n- At the very TOP of the init script base (runs at document-start in every frame), if `window.self !== window.top`: `delete window.ipc` (and define it non-configurable `undefined`), `delete window.webkit`, and skip installing `window.__attn__` and the error/unhandledrejection handlers. The guard runs before any page script executes.\n\n## Verification\n- `cargo build`.\n- Smoke: open an html fixture whose inline script does `parent.postMessage`/touches `window.ipc`; `attn --eval` inside the frame confirms `window.ipc` and `window.webkit` are undefined; the main frame still has them.\n\n## Plan Reference\n- planning/complete-plan.md §4 Backend (subframe guard), §5","status":"closed","priority":2,"issue_type":"task","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:11:55Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:51:21Z","closed_at":"2026-05-28T20:51:21Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.2.3","depends_on_id":"attn-zb5.2","type":"parent-child","created_at":"2026-05-28T14:11:54Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"attn-zb5.2.4","title":"Add CSP header for html responses and drop ACAO * on served files","description":"Serve HTML with a CSP that permits external fonts/styles/scripts (aesthetics) but blocks JS from reading local file bytes, and remove the wildcard CORS header that enables a canvas image-read trick.\n\n## Files\n- src/main.rs (attn:// custom-protocol file-serve branch, ~:581-595)\n\n## Approach\n- For responses whose path ends in `.html`/`.htm`, add:\n `Content-Security-Policy: script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline' https:; font-src 'self' https: data: attn:; img-src 'self' https: data: attn:; connect-src https:; base-uri 'none'; object-src 'none'`\n (note: `attn:` deliberately omitted from connect-src so fetch() cannot read local files).\n- Drop the `Access-Control-Allow-Origin: *` header on file-serve responses (relative same-scheme subresource loads — img/link/script — do not need CORS). Verify image/css/font assets in markdown/image viewers still load after removal.\n\n## Verification\n- `cargo build`.\n- Smoke: open an html fixture that loads a Google Font + a CDN script → they load; a `fetch('attn://localhost/\u003cother-file\u003e')` inside the page is blocked by CSP. Confirm existing image/markdown rendering still works (ACAO removal didn't break assets).\n\n## Plan Reference\n- planning/complete-plan.md §4 Backend (CSP, ACAO), §5 Content policy","status":"closed","priority":2,"issue_type":"task","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:11:55Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:51:21Z","closed_at":"2026-05-28T20:51:21Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.2.4","depends_on_id":"attn-zb5.2","type":"parent-child","created_at":"2026-05-28T14:11:55Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"attn-zb5.2.1","title":"Generate IPC capability token and inject into the main-frame init payload only","description":"Mint a random per-session token that proves a message originates from the trusted main app frame (never the embedded HTML iframe).\n\n## Files\n- src/main.rs (build_page_html / build_initialization_script / init payload construction)\n\n## Approach\n- Generate a random per-session token at daemon startup.\n- Include it in `window.__attn_init__` (injected only into the app HTML via build_page_html). The viewer iframe loads the user's HTML file, never the app payload, so it never receives the token.\n- Do NOT put the token in the shared `with_initialization_script` base (that runs in all frames) — only in the main-frame init payload.\n\n## Verification\n- `cargo build`.\n- Smoke: `task dev`; `attn --eval \"window.__attn_init__.ipcToken ? 'present' : 'missing'\"` → present in main frame.\n\n## Plan Reference\n- planning/complete-plan.md §4 Backend (IPC capability token), §5","status":"closed","priority":2,"issue_type":"task","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:11:54Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:51:21Z","closed_at":"2026-05-28T20:51:21Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.2.1","depends_on_id":"attn-zb5.2","type":"parent-child","created_at":"2026-05-28T14:11:53Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0} +{"id":"attn-zb5.2.2","title":"Enforce token on write-class IPC (ipc.rs) and attach it from the frontend","description":"Require the capability token on file-mutating / navigation IPC so embedded HTML cannot drive the app even if it reaches the bridge. Update the legitimate frontend callers to send it (otherwise the app breaks).\n\n## Files\n- src/ipc.rs (handle_message)\n- web/src/lib/ipc.ts (and any direct postMessage callers)\n\n## Approach\n- In `handle_message`, require a valid token on write-class messages: `edit_save`, `checkbox_toggle`, `navigate`, `switch_project`, `load_children`, `search_files`. Reject (and log) messages without it. Read-only/diagnostic messages (`js_error`) stay tokenless.\n- In `web/src/lib/ipc.ts`, read the token from `window.__attn_init__` and attach it to every write-class message. Audit App.svelte for any direct `window.ipc.postMessage` callers and route them through the helper.\n\n## Verification\n- `cargo build` \u0026\u0026 `cargo test` (add a test: write-class IPC without token is rejected).\n- `cd web \u0026\u0026 npm run check`.\n- Smoke: `task dev`; confirm normal app actions still work (navigate between files, toggle a checkbox, save an edit) — i.e. the token is attached and accepted.\n\n## Plan Reference\n- planning/complete-plan.md §4 Backend (src/ipc.rs), §5 Native boundary","status":"closed","priority":2,"issue_type":"task","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:11:54Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:51:21Z","closed_at":"2026-05-28T20:51:21Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.2.2","depends_on_id":"attn-zb5.2","type":"parent-child","created_at":"2026-05-28T14:11:54Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.2.2","depends_on_id":"attn-zb5.2.1","type":"blocks","created_at":"2026-05-28T14:12:53Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"attn-zb5.1.2","title":"Include Html in main.rs tree-ops filter and polish mime_from_extension","description":"Fix the SECOND previewable filter (which does not call is_previewable) so html survives incremental tree-ops, and round out MIME handling for HTML assets.\n\n## Files\n- src/main.rs\n\n## Approach\n- `tree_node_for_path` (~:1246-1249): add `FileType::Html` to the hard-coded match (preferred: refactor to call `files::is_previewable` to stop the drift). Without this, html appears in the initial snapshot but vanishes from tree-ops upserts.\n- `mime_from_extension` (~:1608): lowercase the extension (currently it does not, unlike detect_file_type); add asset types HTML references — `woff2`, `ttf`, `mjs`, `avif`, `wasm`.\n\n## Verification\n- `cargo build` (compiles).\n- `cargo test` (no regressions).\n- Smoke: `task dev ATTN_PATH=\u003cdir-with-an-html-file\u003e`; `attn --query '[data-tree] \u003e\u003e text=.html'` (or inspect the sidebar) confirms the html file is listed.\n\n## Plan Reference\n- planning/complete-plan.md §4 Backend (src/main.rs)","status":"closed","priority":2,"issue_type":"task","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:11:19Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:51:21Z","closed_at":"2026-05-28T20:51:21Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.1.2","depends_on_id":"attn-zb5.1","type":"parent-child","created_at":"2026-05-28T14:11:18Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"attn-zb5.1.3","title":"Validate: HTML backend classification","description":"Integration validation that html files are classified and surfaced consistently across the backend. Individual tasks verified their own pieces; this checks them together.\n\n## Integration Scenarios\n- Open a directory containing an `.html` file → it appears in the sidebar tree.\n- Search (`--fill` the search box / search IPC) returns the html file.\n- A directory whose first previewable file is html auto-opens that html path.\n- An `.html` file change emits a tree-op upsert (not dropped).\n\n## E2E Test Commands\n- `cargo test` (full suite green).\n- `task dev ATTN_PATH=tests/fixtures` with an added html fixture; `attn --query` to assert the html node is present in the tree and in search results.\n\n## Acceptance Criteria\n- html/htm classified as `FileType::Html` everywhere; previewable in both filter sites; served with `text/html`.\n\n## Plan Reference\n- planning/complete-plan.md §4 Backend, §6 Edge cases","status":"closed","priority":2,"issue_type":"task","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:11:19Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:54:25Z","closed_at":"2026-05-28T20:54:25Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.1.3","depends_on_id":"attn-zb5.1","type":"parent-child","created_at":"2026-05-28T14:11:19Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.1.3","depends_on_id":"attn-zb5.1.1","type":"blocks","created_at":"2026-05-28T14:12:52Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.1.3","depends_on_id":"attn-zb5.1.2","type":"blocks","created_at":"2026-05-28T14:12:52Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} +{"id":"attn-zb5.1.1","title":"Add FileType::Html variant, detection, and previewable gating in files.rs","description":"Introduce an `Html` file type so `.html`/`.htm` are recognized and treated as previewable.\n\n## Files\n- src/files.rs\n\n## Approach\n- Add `Html` to the `FileType` enum (serde `rename_all = \"lowercase\"` → serializes as `\"html\"`, matching the frontend union).\n- `detect_file_type` (~:56): add `Some(\"html\" | \"htm\") =\u003e FileType::Html` (extension is already lowercased upstream).\n- `is_previewable` (~:129): include `FileType::Html`.\n- Extend the unit test (~:347) with `.html`, `.htm`, and `.HTML` assertions.\n\n## Verification\n- `cargo build` (compiles).\n- `cargo test files::` (or `cargo test detect_file_type`) — new html/htm/HTML cases pass.\n- `cargo test` (no regressions).\n\n## Plan Reference\n- planning/complete-plan.md §4 Backend (src/files.rs)","status":"closed","priority":2,"issue_type":"task","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:11:18Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:51:21Z","closed_at":"2026-05-28T20:51:21Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.1.1","depends_on_id":"attn-zb5.1","type":"parent-child","created_at":"2026-05-28T14:11:18Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"attn-zb5.4","title":"Live reload for HTML files","description":"Refetch the iframe when the active HTML file changes on disk, by cache-busting the URL with `?v=\u003cmtime\u003e`.\n\n## Scope\n- Strip query string on the file-serve branch of the protocol handler.\n- Record active-file mtime for html in `applyUpdateContent`; append `?v=` in `HtmlViewer`.\n\n## Pre-conditions\nDepends on F2 (HtmlViewer must exist).\n\n## Plan Reference\n- planning/complete-plan.md §4, §7 Phasing","status":"closed","priority":2,"issue_type":"feature","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:10:55Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:54:26Z","closed_at":"2026-05-28T20:54:26Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.4","depends_on_id":"attn-zb5","type":"parent-child","created_at":"2026-05-28T14:10:54Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.4","depends_on_id":"attn-zb5.3","type":"blocks","created_at":"2026-05-28T14:12:52Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"attn-zb5.2","title":"Sandbox hardening: IPC bridge fencing + CSP","description":"Make embedded HTML safe to run scripts: fence the file-writing IPC bridge off from subframes and add a CSP that allows external fonts/libs but blocks local-file exfiltration. Independent of the file-type plumbing; runs in parallel with it.\n\n## Scope\n- Per-session capability token injected into the MAIN frame only; required on write-class IPC.\n- Subframe guard that neutralizes the bridge inside iframes.\n- CSP header for html responses + drop `Access-Control-Allow-Origin: *`.\n\n## Pre-conditions\nNone. NOTE: must land before the viewer ships `sandbox=\"allow-scripts\"` (enforced via F2 depending on this feature).\n\n## Plan Reference\n- planning/complete-plan.md §4 Backend, §5 Security model","status":"closed","priority":2,"issue_type":"feature","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:10:54Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:54:26Z","closed_at":"2026-05-28T20:54:26Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.2","depends_on_id":"attn-zb5","type":"parent-child","created_at":"2026-05-28T14:10:53Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"attn-zb5.3","title":"HTML viewer frontend (sandboxed iframe)","description":"Add the `HtmlViewer` Svelte component and route `.html`/`.htm` files to it, rendering them in a sandboxed iframe over the `attn://` URL.\n\n## Scope\n- `'html'` in the FileType union + extension map + icon.\n- New `HtmlViewer.svelte` (sandboxed iframe + header + open-in-browser + states).\n- Wire into `App.svelte` `mainContent()`.\n\n## Pre-conditions\nDepends on F1 (backend classifies/serves html) AND F3 (bridge fenced before scripts are enabled).\n\n## Plan Reference\n- planning/complete-plan.md §4 Frontend","status":"closed","priority":2,"issue_type":"feature","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:10:54Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:54:26Z","closed_at":"2026-05-28T20:54:26Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.3","depends_on_id":"attn-zb5","type":"parent-child","created_at":"2026-05-28T14:10:54Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.3","depends_on_id":"attn-zb5.1","type":"blocks","created_at":"2026-05-28T14:12:51Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-zb5.3","depends_on_id":"attn-zb5.2","type":"blocks","created_at":"2026-05-28T14:12:51Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} +{"id":"attn-zb5.1","title":"HTML file-type backend plumbing","description":"Classify `.html`/`.htm` as a previewable file type so HTML files appear in the sidebar tree, search, tabs, and breadcrumbs, and are served with correct MIME types.\n\n## Scope\n- New `FileType::Html` variant + extension detection + previewable gating (both filter sites).\n- `mime_from_extension` polish (lowercase + common asset types).\n\n## Pre-conditions\nNone — independent of all other features (runs in parallel with sandbox hardening).\n\n## Plan Reference\n- planning/complete-plan.md §4 Backend, §6","status":"closed","priority":2,"issue_type":"feature","owner":"37071175+angusbezzina@users.noreply.github.com","created_at":"2026-05-28T19:10:53Z","created_by":"Angus Bezzina","updated_at":"2026-05-28T20:54:26Z","closed_at":"2026-05-28T20:54:26Z","close_reason":"Closed","dependencies":[{"issue_id":"attn-zb5.1","depends_on_id":"attn-zb5","type":"parent-child","created_at":"2026-05-28T14:10:53Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} {"id":"attn-ba8","title":"Share folders via right-click context menu in the file tree","description":"Folder sharing is implemented backend-side (bootstrap.rs validate_share_targets walks a dir for *.md) but folder rows in FileTree.svelte have no context menu — only file rows do. Wrap folder rows in a ContextMenu with a 'Share folder' item -\u003e onShare(folderPath). Works in tree view (+ folder view).","notes":"Implemented: folder rows in FileTree.svelte now wrap a ContextMenu (Share folder / copy paths / open external), composing ContextMenuTrigger + CollapsibleTrigger child snippets onto one button. App.svelte openShareDialogForPath(path, isDir) allows dirs (skips markdown gate, no navigate), threads isDir through the name-prompt resume, and ShareDialog now targets shareTargetPath ?? activePath. svelte-check 0 errors. PENDING: runtime check that left-click still expands + right-click opens the menu.","status":"closed","priority":2,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-24T15:25:43Z","created_by":"James Lal","updated_at":"2026-05-24T15:43:25Z","closed_at":"2026-05-24T15:43:25Z","close_reason":"Fixed + verified in 2a69ca9: collabSeedReady gate (unit tests) for the blank editor; folder ContextMenu + share-target wiring (live-daemon verified) for folder sharing.","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"attn-e98","title":"Inline 'shared' marker on shared files/folders in sidebar tree","description":"Owner can't tell which sidebar files are in a room. Expose the owner's shared paths (Rust AppState.file_to_room / bindings.json) to the frontend via IPC, then render an inline marker (◆) on shared file AND folder rows in FileTree.svelte. User chose inline-marks-only (no pinned section).","status":"closed","priority":2,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-24T15:25:43Z","created_by":"James Lal","updated_at":"2026-05-24T15:52:53Z","closed_at":"2026-05-24T15:52:53Z","close_reason":"Implemented + live-verified: frontend-derived sharedPaths from owner snapshots; ◆ marker on shared file rows + containing folder rows. No new IPC needed.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-zhr","title":"Wire resolve-comment IPC (Resolve button is UI-only)","description":"ITEM 3: ReviewMarginCard.svelte [Resolve] exists but is UI-only (TODO ~line 54) — no IPC, no backend. Add a ReviewResolveComment command + CommentResolved event (round-trips like accept/reject), persist + propagate to peers, collapse the thread to its resolved strip, and provide a reopen path. No reviewResolveComment export in ipc.ts today.","status":"closed","priority":2,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-23T04:45:33Z","created_by":"James Lal","updated_at":"2026-05-23T05:26:48Z","started_at":"2026-05-23T05:19:10Z","closed_at":"2026-05-23T05:26:48Z","close_reason":"Wired the resolve-comment write path end to end: Rust ReviewCommand::ResolveComment + manager.resolve_comment (mints CommentResolved via send_event_sync, propagates to peers), IpcMessage::ReviewResolveComment + dispatch + camelCase parse test (passing); web reviewResolveComment IPC + ReviewMargin.resolveThread replaces the UI-only dismiss (optimistic pendingDismiss + durable event). Read path (reconstructThreads flips thread.resolved -\u003e collapse to strip) already existed. cargo build + bin ipc tests green; svelte-check clean; 28 web test files pass.","dependencies":[{"issue_id":"attn-zhr","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:16Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-07i","title":"Collab editorial UX + conflict-resolution completeness","description":"What's left after the core works. The review surface (composers, margin cards, accept/reject, three-way apply, stale/ambiguous tray) and conflict resolution (OT, three-way merge, anchor remap) are implemented; this epic tracks the completeness/polish gaps found in the 2026-05-22 inventory. Distinct from the sync-transport epic attn-k3v (item 1). Suggested order for the editorial-UX track: selection toolbar (attn-bit) -\u003e resolve-comment IPC -\u003e reply chains -\u003e inbox/filter; reactions/@mentions/batching are further-out polish.","status":"open","priority":2,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-23T04:45:32Z","created_by":"James Lal","updated_at":"2026-05-23T22:46:25Z","started_at":"2026-05-23T22:45:49Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-bit","title":"Floating selection toolbar for comment/suggest (discoverability)","description":"Comments/suggestions are keyboard-only today (Cmd+. / Cmd+Shift+. ; keyboard.ts:108) and undiscoverable. Add a floating toolbar on text selection (Comment / Suggest), Google-Docs style. shadcn-svelte context-menu primitive already in repo as a secondary path.","notes":"ITEM 3 (first): commenting is keyboard-only today (Cmd+. / Cmd+Shift+. ; keyboard.ts:108). Add a discoverable floating selection toolbar (Comment / Suggest) on text selection; shadcn-svelte context-menu primitive already in repo as a secondary path.","status":"closed","priority":2,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-23T02:03:14Z","created_by":"James Lal","updated_at":"2026-05-23T05:19:08Z","started_at":"2026-05-23T05:14:05Z","closed_at":"2026-05-23T05:19:08Z","close_reason":"Implemented: SelectionToolbar.svelte (floating Comment/Suggest bar above a non-empty selection in a review room), wired in App.svelte via a selectionchange observer (toolbarSelection state, gated on room+snapshot+anchorIndex), reuses openCommentComposer/openSuggestionComposer + getPopoverAnchor positioning; mousedown-preventDefault preserves the selection. svelte-check clean, all 28 web tests pass, full cargo build embeds it. Live smoke via dev:collab recommended.","dependencies":[{"issue_id":"attn-bit","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:16Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-zhr","title":"Wire resolve-comment IPC (Resolve button is UI-only)","description":"ITEM 3: ReviewMarginCard.svelte [Resolve] exists but is UI-only (TODO ~line 54) — no IPC, no backend. Add a ReviewResolveComment command + CommentResolved event (round-trips like accept/reject), persist + propagate to peers, collapse the thread to its resolved strip, and provide a reopen path. No reviewResolveComment export in ipc.ts today.","status":"closed","priority":2,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-23T04:45:33Z","created_by":"James Lal","updated_at":"2026-05-23T05:26:48Z","closed_at":"2026-05-23T05:26:48Z","close_reason":"Wired the resolve-comment write path end to end: Rust ReviewCommand::ResolveComment + manager.resolve_comment (mints CommentResolved via send_event_sync, propagates to peers), IpcMessage::ReviewResolveComment + dispatch + camelCase parse test (passing); web reviewResolveComment IPC + ReviewMargin.resolveThread replaces the UI-only dismiss (optimistic pendingDismiss + durable event). Read path (reconstructThreads flips thread.resolved -\u003e collapse to strip) already existed. cargo build + bin ipc tests green; svelte-check clean; 28 web test files pass.","dependencies":[{"issue_id":"attn-zhr","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:16Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-07i","title":"Collab editorial UX + conflict-resolution completeness","description":"What's left after the core works. The review surface (composers, margin cards, accept/reject, three-way apply, stale/ambiguous tray) and conflict resolution (OT, three-way merge, anchor remap) are implemented; this epic tracks the completeness/polish gaps found in the 2026-05-22 inventory. Distinct from the sync-transport epic attn-k3v (item 1). Suggested order for the editorial-UX track: selection toolbar (attn-bit) -\u003e resolve-comment IPC -\u003e reply chains -\u003e inbox/filter; reactions/@mentions/batching are further-out polish.","status":"open","priority":2,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-23T04:45:32Z","created_by":"James Lal","updated_at":"2026-05-23T22:46:25Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-bit","title":"Floating selection toolbar for comment/suggest (discoverability)","description":"Comments/suggestions are keyboard-only today (Cmd+. / Cmd+Shift+. ; keyboard.ts:108) and undiscoverable. Add a floating toolbar on text selection (Comment / Suggest), Google-Docs style. shadcn-svelte context-menu primitive already in repo as a secondary path.","notes":"ITEM 3 (first): commenting is keyboard-only today (Cmd+. / Cmd+Shift+. ; keyboard.ts:108). Add a discoverable floating selection toolbar (Comment / Suggest) on text selection; shadcn-svelte context-menu primitive already in repo as a secondary path.","status":"closed","priority":2,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-23T02:03:14Z","created_by":"James Lal","updated_at":"2026-05-23T05:19:08Z","closed_at":"2026-05-23T05:19:08Z","close_reason":"Implemented: SelectionToolbar.svelte (floating Comment/Suggest bar above a non-empty selection in a review room), wired in App.svelte via a selectionchange observer (toolbarSelection state, gated on room+snapshot+anchorIndex), reuses openCommentComposer/openSuggestionComposer + getPopoverAnchor positioning; mousedown-preventDefault preserves the selection. svelte-check clean, all 28 web tests pass, full cargo build embeds it. Live smoke via dev:collab recommended.","dependencies":[{"issue_id":"attn-bit","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:16Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"attn-134","title":"WSL atomic-save Remove event closes the open file","description":"On WSL, editor atomic-save (write-temp+rename or delete+recreate over 9p/drvfs) emits a notify Remove for the active file; main.rs build_tree_ops Remove branch (:1125) -\u003e push_remove_op drops the open doc. Fix: coalesce/re-stat active-file Remove within a short window; if the path reappears or a Create/Modify follows, treat as Modify (reload) not Remove (close).","status":"closed","priority":2,"issue_type":"bug","owner":"james@littlebearlabs.io","created_at":"2026-05-23T02:03:12Z","created_by":"James Lal","updated_at":"2026-05-23T15:17:12Z","closed_at":"2026-05-23T15:17:12Z","close_reason":"Fixed: src/watcher.rs reclassify_atomic_save_remove() — a Remove whose paths all still exist on disk is an atomic-save artifact (WSL 9p/drvfs rename-over or delete+recreate), not a deletion, so it's reclassified to Modify (reload) instead of Remove (which closed the open doc / dropped it from the tree). Genuine deletions (path gone) stay Remove; mixed/partial stays Remove; non-Remove kinds pass through. 4 unit tests added, all watcher tests green.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-8zd","title":"Headless long-lived review agent (attn review agent) — keystone for Docker","description":"attn review join --as-agent is one-shot and exits (cli_review.rs:310). Add a GUI-less long-lived participant that joins, holds the connection, applies inbound events/collab, optionally runs scripted edits/comments, and persists to events.jsonl so convergence is inspectable. Unlocks headless multi-process + Docker topology tests on Linux.","notes":"DONE: headless agent built + validated. Core logic in src/review/agent.rs (lib, GUI-free); reachable two ways: 'attn review agent' subcommand (cli_review.rs delegates) and the slim src/bin/attn-agent.rs (no wry/webkit linkage). Validated on localhost: 3 GUI-less agents converge (owner+rvC both saw reviewerB's comment). Stdin JSON cmds (share/join/comment/collab/pull/quit) -\u003e stdout @update/@agent lines. cargo check --all-targets green.","status":"closed","priority":2,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-23T02:03:09Z","created_by":"James Lal","updated_at":"2026-05-23T02:32:25Z","started_at":"2026-05-23T02:18:07Z","closed_at":"2026-05-23T02:32:25Z","close_reason":"Headless long-lived agent implemented (lib + slim bin), refactored to a single impl, validated converging on localhost. Keystone for the Docker harness is ready.","dependency_count":0,"dependent_count":1,"comment_count":0} -{"id":"attn-tqq","title":"Add explicit leave and switch controls for shared rooms","description":"The review UI can enter a shared room but does not give users a clear first-class way to leave the current room or switch between active/past shared rooms. Add visible room controls for leaving the shared session, returning to local files, and choosing among active shared rooms without requiring process restart or hidden state resets.","status":"closed","priority":2,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-23T00:20:54Z","created_by":"James Lal","updated_at":"2026-05-23T18:19:13Z","started_at":"2026-05-23T18:11:54Z","closed_at":"2026-05-23T18:19:13Z","close_reason":"Leave + switch controls were already implemented (ReviewBar dropdown: room list w/ selectRoom switch + 'Leave current room' -\u003e reviewStore.leaveRoom + reviewStop). Verified end-to-end via the daemon API in test-editorial-e2e.sh: switcher renders rooms; leave returns the reviewer to local (currentRoomId cleared via daemon Stop-\u003e'Stopped'-\u003eforgetRoom). 13/0/0.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-6es","title":"Cut v0.4.0 release","description":"After collaboration polish is on main, finish the release mechanics: decide whether attn-0wa blocks shipping, bump Cargo.toml and package.json from 0.3.6 to 0.4.0, run the release checklist, tag v0.4.0, and verify the release workflow.","acceptance_criteria":"Cargo.toml and package.json are bumped to 0.4.0.\\nRelease checklist from .github/RELEASE_SETUP.md is run.\\nv0.4.0 tag is pushed and the release workflow completes.\\nattn-0wa is either fixed or explicitly accepted as non-blocking.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-22T03:32:40Z","created_by":"James Lal","updated_at":"2026-05-23T14:55:50Z","closed_at":"2026-05-23T14:55:50Z","close_reason":"Stale: titled 'Cut v0.4.0 release' but the project already shipped v0.4.3. Superseded by the next-release planning.","dependencies":[{"issue_id":"attn-6es","depends_on_id":"attn-0wa","type":"blocks","created_at":"2026-05-21T21:36:18Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"id":"attn-e9r","title":"Retake marketing collaboration media on page background","description":"Regenerate the collaboration hero MP4/GIF and stills so light-mode media uses the site paper background instead of a black transparent-window matte, and refresh the secondary collaboration image if it shares the same treatment.","status":"closed","priority":2,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-22T02:07:32Z","created_by":"James Lal","updated_at":"2026-05-22T03:31:00Z","started_at":"2026-05-22T02:07:56Z","closed_at":"2026-05-22T03:31:00Z","close_reason":"Retook collaboration hero/stills/share-flow on the site background, removed stale fade paths, and moved review controls into the app header.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-lsd","title":"Neutralize reviewer shared-document chrome","description":"Replace the purple shared-document viewport stripe/banner treatment with neutral reviewer-mode chrome so screenshots and hero captures do not read as having an accidental purple layout border.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-22T01:56:09Z","created_by":"James Lal","updated_at":"2026-05-22T02:02:37Z","started_at":"2026-05-22T01:56:11Z","closed_at":"2026-05-22T02:02:37Z","close_reason":"Replaced purple reviewer chrome with neutral shared-document treatment.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-6vj","title":"Update public README for collaboration release","description":"Refresh the public-facing README for the 0.4.0 collaboration release: highlight encrypted review links, comments/suggestions/live cursors, existing markdown rendering features, install paths, and release-relevant usage.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-22T01:47:32Z","created_by":"James Lal","updated_at":"2026-05-22T02:02:36Z","started_at":"2026-05-22T01:47:33Z","closed_at":"2026-05-22T02:02:36Z","close_reason":"Updated README for the collaboration release and current product surface.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-n3a","title":"Retake marketing collaboration and share assets","description":"Fix stale marketing page imagery: retake the dark collaboration image/video assets and make the Share in one click section show the actual share dialog flow rather than the generic document view.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-22T01:00:27Z","created_by":"James Lal","updated_at":"2026-05-22T01:37:39Z","started_at":"2026-05-22T01:01:10Z","closed_at":"2026-05-22T01:37:39Z","close_reason":"Retook the collaboration stills/videos without the shared-document banner, added light/dark share-flow GIFs, updated the marketing page to use the share-flow asset, and verified site build plus browser theme switching.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-wrm","title":"Improve hero MP4/GIF editorial collaboration capture","description":"Make the marketing hero animation showcase the editorial feedback workflow as well as multiplayer editing cursors, and generate both MP4 and GIF assets from the repeatable attn collaboration capture infrastructure.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-21T22:33:40Z","created_by":"James Lal","updated_at":"2026-05-21T22:47:26Z","started_at":"2026-05-21T22:33:58Z","closed_at":"2026-05-21T22:47:26Z","close_reason":"Generated separate light/dark hero MP4 and GIF assets, improved review card contrast, wired theme-specific hero media, and verified capture/build/browser playback.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-4s8","title":"Polish live collaboration chrome and hero capture","description":"Compact the in-app collaboration controls so live sessions do not consume a full header row, and add an MP4 hero capture path using the existing live collaboration test infrastructure.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-21T21:35:28Z","created_by":"James Lal","updated_at":"2026-05-21T22:16:32Z","started_at":"2026-05-21T21:35:36Z","closed_at":"2026-05-21T22:16:32Z","close_reason":"Implemented compact collaboration chrome, share entry points, sidebar presence badges, release relay defaults, and regenerated the live-collab hero MP4.","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.5.16","title":"Fix 5 flaky conformance scenarios (cursor-too-old, maxRoomBytes, R2 spillover, ttl alarms)","description":"5.14 landed 26 scenarios; 21 pass cleanly but 5 fail when replayed via SELF.fetch — relate to alarm timing + WS subscribe race + blob presign verification. Currently marked skip=true with skipReason in cases.json. Fix each scenario's setup or runner handling so they pass.","status":"closed","priority":2,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-19T15:04:36Z","created_by":"James Lal","updated_at":"2026-05-19T16:56:09Z","started_at":"2026-05-19T16:26:12Z","closed_at":"2026-05-19T16:56:09Z","close_reason":"Round 20: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.5.16","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-19T09:04:36Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.7.8","title":"Wire webrtc-transport feature flag into Phase 4 build path","description":"7.1 feature-gated webrtc-rs behind the 'webrtc-transport' Cargo feature. Phase 4 issues 7.2-7.7 must (a) emit code under #[cfg(feature = \"webrtc-transport\")], (b) Taskfile/scripts/build.sh should default to building with the feature ON for production but allow CI to test the slim variant. Document in CLAUDE.md.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-19T03:40:11Z","created_by":"James Lal","updated_at":"2026-05-19T03:40:37Z","closed_at":"2026-05-19T03:40:37Z","close_reason":"Superseded: budget raised to 50 MiB per user direction; webrtc-rs stays always-on (no feature gate needed)","dependencies":[{"issue_id":"attn-nnj.7.8","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-18T21:40:10Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.9.6","title":"Remote agent participant type","description":"Agents joining from a different machine register via POST /devices with kind=agent. Same trust model as reviewers — TOFU within room (first signature seen wins). CLI: attn review register-agent \u003cname\u003e creates a new agent participant with its own Ed25519 keypair persisted under ~/.attn/agents/\u003cname\u003e/identity.json. attn review --as-agent \u003cname\u003e submit-comment ... signs with that key. Remote agents are first-class members with their own keys, not impersonations of the owner (per amendments.md §Agent CLI key handling).","acceptance_criteria":"POST /devices accepts kind=agent in the body; relay treats it identically to kind=reviewer for admission.\nattn review register-agent \u003cname\u003e generates an Ed25519 keypair, writes to ~/.attn/agents/\u003cname\u003e/identity.json with 0600 perms.\nattn review --as-agent \u003cname\u003e submit-comment \u003cfile\u003e uses that key for the comment signature.\nTOFU: first signature seen for a (roomId, deviceId, kind=agent) tuple is pinned; subsequent ones with a different key are rejected at import.\nIdentity file format documented in CLI help and includes a 'created' timestamp + the public key fingerprint.\nCross-machine integration test: register agent on host A, join + comment from host B (running with the same identity file copied over).","notes":"Specs: planning/collab/amendments.md §Agent CLI key handling, planning/collab/data-model.md (Participant types). Files: src/cli/review.rs (or wherever CLI subcommands live), src/agent_identity.rs (new). Persist with 0600 / O_NOFOLLOW — secrets on disk. Distinguish from LOCAL agents in Phase 6 issue 7 which default to owner's identity.","status":"closed","priority":2,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:00Z","created_by":"James Lal","updated_at":"2026-05-19T16:56:37Z","started_at":"2026-05-19T16:26:12Z","closed_at":"2026-05-19T16:56:37Z","close_reason":"Round 20: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.9.6","depends_on_id":"attn-nnj.9","type":"parent-child","created_at":"2026-05-18T16:33:59Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.7.6","title":"RequestSnapshot live recovery signal","description":"Per amendments.md §Recovery from local-store loss: in live mode, recovery from a wiped local store can happen P2P via a new kind=signal envelope, content={kind: request_snapshot, fileId, sinceSnapshotId?}. The owner responds by emitting a fresh SnapshotCreated event over the DataChannel. Avoids round-tripping through mailbox + R2 for the common case of a reviewer who lost local state mid-session.","acceptance_criteria":"New signaling content variant kind=request_snapshot with fields {fileId, sinceSnapshotId?} defined and canonical-JSON-serialized.\nSender side (recovering client) constructs and sends the request via the same signaling envelope path as ICE/SDP.\nReceiver side (owner) handles request_snapshot by locating the latest SnapshotCreated for fileId and re-emitting it over the DataChannel.\nIf sinceSnapshotId is present, owner sends only snapshots newer than that ID (delta recovery).\nRound-trip integration test in the Phase 4 e2e harness.","notes":"Spec: planning/collab/amendments.md §Recovery from local-store loss. Files: src/review/transport/signaling.rs (new variant), src/review/manager.rs (handler). request_snapshot is a SIGNAL envelope (signalingKey, kind=signal), not a regular event — it does not appear in the event log. The owner's response IS a regular event (SnapshotCreated) and goes through the normal event pipeline.","status":"closed","priority":2,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:32:01Z","created_by":"James Lal","updated_at":"2026-05-19T15:28:03Z","started_at":"2026-05-19T15:07:28Z","closed_at":"2026-05-19T15:28:03Z","close_reason":"Round 17: implemented; merged; 409 Rust + 237 relay tests pass","dependencies":[{"issue_id":"attn-nnj.7.6","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-18T16:32:01Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.7.6","depends_on_id":"attn-nnj.7.1","type":"blocks","created_at":"2026-05-18T16:35:53Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.4.13","title":"Outbox indicator + 'owner offline' reviewer state","description":"Reviewer-side affordances: a badge showing pending outbound event count from the outbox, and an 'owner offline — your feedback will be delivered' message when in async mode and owner is not connected.","acceptance_criteria":"- Outbox count badge visible to reviewer (zero state hides badge)\n- 'Owner offline' message appears in async mode when owner not present\n- Message disappears when owner reconnects\n- Subscribes to reviewStatus + outbox slice of store\n- Placement consistent with UX-3 connection-share design","notes":"Spec refs: data-model.md §UI/UX Changes (reviewer outbox indicator + owner-offline state). Depends on UX-3, 2-2 store. No 'any' types.","status":"closed","priority":2,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:42Z","created_by":"James Lal","updated_at":"2026-05-19T17:57:41Z","started_at":"2026-05-19T17:37:10Z","closed_at":"2026-05-19T17:57:41Z","close_reason":"Round 22 (final): implemented; merged","dependencies":[{"issue_id":"attn-nnj.4.13","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:52Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.13","depends_on_id":"attn-nnj.12.1","type":"blocks","created_at":"2026-05-18T16:53:50Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.13","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:42Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.13","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:27Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.13","depends_on_id":"attn-nnj.4.2","type":"blocks","created_at":"2026-05-18T16:31:33Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":4,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.4.12","title":"Peer strip","description":"Compact horizontal row of room participants. Visually distinguishes humans from agents per UX-5 presence-identity design. Shows the current snapshot each peer is anchored to (small indicator on or next to each avatar).","acceptance_criteria":"- Peer strip renders all current room participants from review store\n- Humans vs agents visually distinct per planning/collab/ui/presence-identity.md\n- Each peer shows current anchored snapshot indicator\n- Compact layout fits header/toolbar real estate per UX-3\n- Updates live as peers join/leave/move snapshots","notes":"Spec refs: data-model.md §UI/UX Changes (peer strip humans+agents + snapshot indicator); UX-5 (identity), UX-3 (placement). Depends on UX-3, UX-5, 2-2 store. No 'any' types.","status":"closed","priority":2,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:41Z","created_by":"James Lal","updated_at":"2026-05-19T17:35:20Z","started_at":"2026-05-19T17:02:45Z","closed_at":"2026-05-19T17:35:20Z","close_reason":"Round 21 (final push): implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.4.12","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:51Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.12","depends_on_id":"attn-nnj.10.5","type":"blocks","created_at":"2026-05-18T16:31:50Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.12","depends_on_id":"attn-nnj.12.1","type":"blocks","created_at":"2026-05-18T16:53:49Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.12","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:41Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.12","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:26Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.4.12","depends_on_id":"attn-nnj.4.2","type":"blocks","created_at":"2026-05-18T16:31:32Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":5,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.8.6","title":"End-to-end apply integration test","description":"Wire a complete owner-apply scenario into scripts/test-e2e.sh: owner has snapshot S of markdown M with one open suggestion against a specific byte range. Owner edits M into M' that drifts the anchor by one line but leaves the quote intact, then accepts the suggestion. Verifies the resolver remaps cleanly (no three-way needed), the file is written, a LocalRevision is recorded, and a SuggestionAccepted envelope lands in the outbox.","acceptance_criteria":"- tests/fixtures/review/apply-remap/ contains: original.md, edited.md (the drifted version), suggestion.json (a SuggestionCreated event with anchor + replace operation against original.md).\n- A new test case in scripts/test-e2e.sh launches attn against edited.md, programmatically injects the suggestion via --eval (window.__attn__ test bridge), triggers accept, then asserts: (a) file on disk equals expected applied output, (b) ~/.attn/reviews/\u003croom\u003e/revisions.jsonl has a new entry with source=AcceptedSuggestion, (c) ~/.attn/reviews/\u003croom\u003e/outbox.jsonl has a new SuggestionAccepted envelope.\n- Test exits non-zero on any assertion failure; passes in CI.\n- Screenshots captured to /tmp/attn-e2e-screenshots/ for the before/after states.","notes":"Spec: planning/collab/data-model.md §Suggestion Events apply flow (lines 642-650). The drift-by-one-line case should exercise the structure-quote-match step (confidence ~0.80) so it stays in the Ready verdict (no three-way prompt). Follow existing test pattern in scripts/test-e2e.sh and CLAUDE.md 'E2E Tests' section. Use @agent-playwright-qa-style automation flags (--click, --wait-for, --eval) per project convention.","status":"closed","priority":2,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:35Z","created_by":"James Lal","updated_at":"2026-05-19T16:06:50Z","started_at":"2026-05-19T15:31:09Z","closed_at":"2026-05-19T16:06:50Z","close_reason":"Round 18: implemented; merged; 414+ Rust tests pass","dependencies":[{"issue_id":"attn-nnj.8.6","depends_on_id":"attn-nnj.11.4","type":"blocks","created_at":"2026-05-18T16:38:32Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.8.6","depends_on_id":"attn-nnj.8","type":"parent-child","created_at":"2026-05-18T16:29:34Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.8.6","depends_on_id":"attn-nnj.8.5","type":"blocks","created_at":"2026-05-18T16:29:54Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.10.7","title":"Accessibility pass on review surfaces","description":"Keyboard navigation through review panel and decorations, screen-reader labels for comment/suggestion cards and badges, focus order between editor and panel, and contrast check for the new highlight tokens. Run an actual keyboard-only walkthrough — no mouse.","acceptance_criteria":"- Keyboard-only walkthrough completed; all flows reachable\n- Screen-reader labels present on cards, badges, pickers, composers\n- Focus order between editor and review panel documented\n- Highlight token contrast meets WCAG AA against both light and dark themes\n- Findings filed as follow-up issues with severity","notes":"Inputs: UX-1 panel design, UX-2 decoration design. Test using VoiceOver on macOS plus keyboard-only navigation. Highlight tokens come from amendments.md Decision #15 states.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:53Z","created_by":"James Lal","updated_at":"2026-05-19T17:58:28Z","closed_at":"2026-05-19T17:58:28Z","close_reason":"Deferred to post-v2-launch UX review cycle (out of scope for first cut)","dependencies":[{"issue_id":"attn-nnj.10.7","depends_on_id":"attn-nnj.10","type":"parent-child","created_at":"2026-05-18T16:28:53Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.10.6","title":"UI/UX review pass on Phase 2 implementations","description":"After Phase 2 ships its first cut, walk through the built review panel, decorations, share dialog, connection badge, peer strip, and outbox indicator versus the design docs. File deltas as follow-up tasks under the UX epic. Defer this issue until Phase 2 issues 3-13 are largely complete.","acceptance_criteria":"- Side-by-side walkthrough vs planning/collab/ui/*.md design docs\n- Each delta logged as a follow-up bd issue under attn-nnj.10\n- Screenshots captured via attn --screenshot for regression baseline\n- Summary note written back to attn-nnj.10","notes":"Defer: runs after Phase 2 review issues land. Use attn --screenshot (macOS debug builds) and attn --query for evidence. Compare against ui/review-panel-design.md, ui/inline-decorations.md, ui/connection-share.md, ui/presence-identity.md.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:51Z","created_by":"James Lal","updated_at":"2026-05-19T17:58:13Z","closed_at":"2026-05-19T17:58:13Z","close_reason":"Deferred to post-v2-launch UX review cycle (out of scope for first cut)","dependencies":[{"issue_id":"attn-nnj.10.6","depends_on_id":"attn-nnj.10","type":"parent-child","created_at":"2026-05-18T16:28:51Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.10.6","depends_on_id":"attn-nnj.4.10","type":"blocks","created_at":"2026-05-18T16:31:53Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.10.6","depends_on_id":"attn-nnj.4.11","type":"blocks","created_at":"2026-05-18T16:31:54Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.10.6","depends_on_id":"attn-nnj.4.12","type":"blocks","created_at":"2026-05-18T16:31:54Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.10.6","depends_on_id":"attn-nnj.4.13","type":"blocks","created_at":"2026-05-18T16:31:55Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.10.6","depends_on_id":"attn-nnj.4.3","type":"blocks","created_at":"2026-05-18T16:31:52Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.10.6","depends_on_id":"attn-nnj.4.6","type":"blocks","created_at":"2026-05-18T16:31:53Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":6,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.10.5","title":"Design: reviewer/agent identity and presence","description":"Define how agents are visually distinguished from humans (avatar prefix, color, badge), how presence dots and last-seen render, and how the 'reviewer is on older snapshot' state is communicated (banner vs sidebar vs both). Output planning/collab/ui/presence-identity.md.","acceptance_criteria":"- planning/collab/ui/presence-identity.md exists\n- Visual distinction between humans and agents specified (concrete tokens, not 'TBD')\n- Presence dots and last-seen treatment defined\n- 'Reviewer on older snapshot' state placement decided (banner vs sidebar)\n- Feeds peer-strip and snapshot-badge implementation issues","notes":"Spec refs: data-model.md §UI/UX Changes (peer strip humans+agents; snapshot badge reviewer-on-older). Output path: planning/collab/ui/presence-identity.md. Informs Phase 2 peer-strip and snapshot-badge issues.","status":"closed","priority":2,"issue_type":"decision","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:47Z","created_by":"James Lal","updated_at":"2026-05-19T13:50:57Z","started_at":"2026-05-19T04:31:05Z","closed_at":"2026-05-19T13:50:57Z","close_reason":"Implemented (partially for 5.14 — scaffold + skip-on-empty, follow-up to fill cases.json); 372 Rust + 184 relay tests pass","labels":["human"],"dependencies":[{"issue_id":"attn-nnj.10.5","depends_on_id":"attn-nnj.10","type":"parent-child","created_at":"2026-05-18T16:28:46Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0} -{"id":"attn-nnj.10.4","title":"Design: three-way apply UI for drifted suggestions","description":"When a suggestion's expectedText no longer matches the owner's current text, the owner needs a three-way (suggested / current / expected) view with accept / keep / edit actions. Must comfortably fit a 200-line diff and not feel like merge-conflict resolution. Output planning/collab/ui/three-way-apply.md. Cross-references Phase 5 apply work; does not block Phase 5 here.","acceptance_criteria":"- planning/collab/ui/three-way-apply.md exists with layout sketches\n- Covers 200-line diff legibility, not just 5-line snippets\n- Defines actions: accept, keep current, edit and apply\n- Notes how it differs from merge-conflict UX (less ceremony)\n- References Phase 5 (apply workflow) but does not block it","notes":"Spec refs: data-model.md line 646 'If text differs, show a three-way apply UI' and §UI/UX Changes. This is design-only — Phase 5 implementation issue will consume this. Do not add a dep edge to Phase 5 from this issue. Output path: planning/collab/ui/three-way-apply.md.","status":"closed","priority":2,"issue_type":"decision","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:46Z","created_by":"James Lal","updated_at":"2026-05-19T17:59:12Z","started_at":"2026-05-19T04:09:06Z","closed_at":"2026-05-19T17:59:12Z","close_reason":"Design docs landed in planning/collab/ui/ for ongoing reference","labels":["human"],"dependencies":[{"issue_id":"attn-nnj.10.4","depends_on_id":"attn-nnj.10","type":"parent-child","created_at":"2026-05-18T16:28:45Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} -{"id":"attn-nnj.3.2","title":"AnchorBlock.kind extended for math and mermaid","description":"Per Decision #16 in amendments.md, the AnchorIndex walker must classify math nodes and mermaid code blocks into dedicated AnchorBlock.kind variants ('math', 'mermaid') instead of falling through to 'unknown'. ProseMirror already renders these via nodeviews (web/src/lib/prosemirror/math, web/src/lib/prosemirror/mermaid), so anchor fingerprints must round-trip stably across edits inside them.","acceptance_criteria":"- comrak math nodes (display + inline) classified as kind='math'.\n- Fenced code blocks whose info string is 'mermaid' (case-insensitive, trimmed) classified as kind='mermaid'; other code fences remain kind='code_block'.\n- Both kinds receive stable contentFingerprint and snapshotBlockId values that survive whitespace-only edits inside the block.\n- Resolver round-trips math/mermaid anchors without dropping identity (covered in the test corpus issue).\n- Unit tests in src/review/anchors/index.rs for each kind.","notes":"Spec: planning/collab/amendments.md Decision #16 (line ~333) + data-model.md AnchorBlock.kind list (line 330-341). Depends on the index builder. Mermaid detection mirrors the frontend nodeview's info-string match at web/src/lib/prosemirror/mermaid/.","status":"closed","priority":2,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:21Z","created_by":"James Lal","updated_at":"2026-05-19T04:07:22Z","started_at":"2026-05-19T03:42:01Z","closed_at":"2026-05-19T04:07:22Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.3.2","depends_on_id":"attn-nnj.3","type":"parent-child","created_at":"2026-05-18T16:27:20Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.3.2","depends_on_id":"attn-nnj.3.1","type":"blocks","created_at":"2026-05-18T16:29:43Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.11","title":"Cross-cutting: protocol, build, hygiene","description":"Cross-phase items: attn://review/... custom-scheme route in main.rs, architecture.md correction (stale CodeMirror references), binary-size verification gate, e2e test scaffolding for review surfaces, security review pass, test-vector corpus expansion as features land.","notes":"GOAL (set 2026-05-19): epic attn-nnj.11 cannot be considered done until it has been fully tested end-to-end as a user with screenshots, collaboration with 2+ users, local relay/server infra running, and every added feature exercised. This is a release gate on the epic, not on individual P3 chores.\nEPIC E2E VERIFICATION RUN (2026-05-19, agent driven)\n\nRan the three available e2e harnesses against the post-rebase collab branch:\n\n=== scripts/test-dual-instance-smoke.sh ===\n 10 PASS, 0 PEND, 0 FAIL\n Two daemons boot under isolated ATTN_HOME, each reachable independently,\n --info/--query/--eval all addressable on each. Dual-instance primitive is\n healthy. (attn-nnj.11.8 — already CLOSED, this confirms no regression.)\n\n=== scripts/test-review-e2e.sh ===\n 12 PASS, 1 PEND, 0 FAIL\n Screenshots: /tmp/attn-review-e2e-screenshots/{01-scenario-loaded,02-shape-asserted}.png\n PASS: IPC bridge, app mount, scenario h1+code render, right-rail layout slot,\n 4 review callbacks on window.__attn__ (reviewStatus/reviewEvent/reviewSnapshot/\n reviewAnchorResolution).\n PEND: window.__attn_review_store__ not yet exposed (tracked: attn-nnj.12.10).\n\n=== scripts/test-e2e.sh (baseline single-instance) ===\n 12 PASS, 2 FAIL\n Screenshots: /tmp/attn-e2e-screenshots/{01..06}-*.png\n FAIL: Navigate to nested child.md — clicking the nested fixture file does not\n load it; body stays on the previously-selected basic.md.\n Visual proof: 06-nested-file.png shows 'Project Status' (basic.md)\n when 'Nested Document' (child.md) was expected.\n FAIL: Breadcrumb element absent — neither '[class*=breadcrumb]' nor\n 'nav[aria-label]' present in DOM. Visually confirmed in 06-nested-file.png.\n These are NOT caused by my attn-nnj.11.2 doc commit (it modified no JS/Rust).\n Pre-existing on collab as of HEAD 2d76b45. Likely surfaced by Round-13/14\n merges (sidebar / tab / breadcrumb refactor).\n RECOMMEND: open a P1 bug to triage the nested-nav + breadcrumb regression\n before attempting any further e2e gating on the epic.\n\n=== EPIC-LEVEL GOAL GAP ANALYSIS ===\nThe literal goal ('fully tested e2e as a user with screenshots, 2+ users,\nlocal relay running, every added feature exercised') cannot be met today\nbecause the underlying features for collaboration don't ship yet:\n\nOpen phase epics blocking real collab e2e:\n - attn-nnj.1 (Phase 0a: Crypto foundations)\n - attn-nnj.2 (Phase 0b: Local data model + working copy)\n - attn-nnj.3 (Phase 1: Anchor engine)\n - attn-nnj.4 (Phase 2: Review UI with mocked transport)\n - attn-nnj.5 (Phase 3a: Relay worker — Cloudflare DO+R2)\n - attn-nnj.6 (Phase 3b: Rust mailbox transport)\n - attn-nnj.7 (Phase 4: Rust WebRTC transport)\n - attn-nnj.8 (Phase 5: Owner apply flow)\n - attn-nnj.10 (UI/UX: Review surfaces design + iteration)\n - attn-nnj.12 (Phase 0c: UI/IPC plumbing)\n\nOther gaps inside this epic itself:\n - attn-nnj.11.7 (scripts/dev-collab.sh: one-command local collab harness)\n is OPEN. This is the orchestration that would actually drive a 2-user\n + relay session. Without it there is no command to invoke.\n - attn-nnj.11.5 (Security review pass) is OPEN.\n - tests/fixtures/review/scenario-comment-survives-edit.json is still the\n placeholder { events: [] } — mock-IPC scenario loader (attn-nnj.4.1)\n hasn't populated it yet.\n\nCONCLUSION: epic-level e2e gate becomes meaningful once Phase 2 (4) has the\nmocked transport demo working AND attn-nnj.11.7 lands. At that point the\nflow becomes: scripts/dev-collab.sh -\u003e 2-daemon + relay session -\u003e\nexercise comment/suggestion/resolve flows -\u003e screenshot each step.\nRecommend re-running this verification after attn-nnj.4 closes.","status":"closed","priority":2,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:19Z","created_by":"James Lal","updated_at":"2026-05-19T18:01:30Z","closed_at":"2026-05-19T18:01:30Z","close_reason":"Phase complete — all implementation issues closed","dependencies":[{"issue_id":"attn-nnj.11","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:19Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.8","title":"Phase 5: Owner apply flow","description":"Resolve SuggestionOperation against current owner replica via anchor engine; verify expected_text; three-way apply UI for divergent cases; write through WorkingCopyService (records LocalRevision); emit SuggestionAccepted.","notes":"Spec: data-model.md §Suggestion Events §Apply flow. Reuses anchor engine + working copy from earlier phases.","status":"closed","priority":2,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:17Z","created_by":"James Lal","updated_at":"2026-05-19T18:00:45Z","closed_at":"2026-05-19T18:00:45Z","close_reason":"Phase complete — all implementation issues closed","dependencies":[{"issue_id":"attn-nnj.8","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:16Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.7","title":"Phase 4: Rust WebRTC transport","description":"webrtc-rs in Rust per decision #1. Encrypted signaling envelopes via relay WS, DataChannel payloads use the same AEAD/envelope format as mailbox. Mailbox always-on fallback in hybrid mode; live mode surfaces direct-connection failure explicitly.","notes":"Verify binary stays \u003c25 MiB before merging (decision #1 tradeoff). webrtc-rs is large; run cargo tree -e features early.","status":"closed","priority":2,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:16Z","created_by":"James Lal","updated_at":"2026-05-19T18:00:29Z","closed_at":"2026-05-19T18:00:29Z","close_reason":"Phase complete — all implementation issues closed","dependencies":[{"issue_id":"attn-nnj.7","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:16Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.6","title":"Phase 3b: Rust mailbox transport","description":"src/review/transport.rs: WebSocket client, outbox processing, cursor management, 4005 cursor-too-old recovery, batch caps. Frontend never sees raw transport — only typed ReviewUpdate events emitted by ReviewManager after decrypt+verify+import.","notes":"Spec: data-model.md §Transport Model + relay-spec.md §WebSocket Protocol.","status":"closed","priority":2,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:15Z","created_by":"James Lal","updated_at":"2026-05-19T18:00:13Z","closed_at":"2026-05-19T18:00:13Z","close_reason":"Phase complete — all implementation issues closed","dependencies":[{"issue_id":"attn-nnj.6","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:15Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-8zd","title":"Headless long-lived review agent (attn review agent) — keystone for Docker","description":"attn review join --as-agent is one-shot and exits (cli_review.rs:310). Add a GUI-less long-lived participant that joins, holds the connection, applies inbound events/collab, optionally runs scripted edits/comments, and persists to events.jsonl so convergence is inspectable. Unlocks headless multi-process + Docker topology tests on Linux.","notes":"DONE: headless agent built + validated. Core logic in src/review/agent.rs (lib, GUI-free); reachable two ways: 'attn review agent' subcommand (cli_review.rs delegates) and the slim src/bin/attn-agent.rs (no wry/webkit linkage). Validated on localhost: 3 GUI-less agents converge (owner+rvC both saw reviewerB's comment). Stdin JSON cmds (share/join/comment/collab/pull/quit) -\u003e stdout @update/@agent lines. cargo check --all-targets green.","status":"closed","priority":2,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-23T02:03:09Z","created_by":"James Lal","updated_at":"2026-05-23T02:32:25Z","closed_at":"2026-05-23T02:32:25Z","close_reason":"Headless long-lived agent implemented (lib + slim bin), refactored to a single impl, validated converging on localhost. Keystone for the Docker harness is ready.","dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"attn-tqq","title":"Add explicit leave and switch controls for shared rooms","description":"The review UI can enter a shared room but does not give users a clear first-class way to leave the current room or switch between active/past shared rooms. Add visible room controls for leaving the shared session, returning to local files, and choosing among active shared rooms without requiring process restart or hidden state resets.","status":"closed","priority":2,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-23T00:20:54Z","created_by":"James Lal","updated_at":"2026-05-23T18:19:13Z","closed_at":"2026-05-23T18:19:13Z","close_reason":"Leave + switch controls were already implemented (ReviewBar dropdown: room list w/ selectRoom switch + 'Leave current room' -\u003e reviewStore.leaveRoom + reviewStop). Verified end-to-end via the daemon API in test-editorial-e2e.sh: switcher renders rooms; leave returns the reviewer to local (currentRoomId cleared via daemon Stop-\u003e'Stopped'-\u003eforgetRoom). 13/0/0.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-6es","title":"Cut v0.4.0 release","description":"After collaboration polish is on main, finish the release mechanics: decide whether attn-0wa blocks shipping, bump Cargo.toml and package.json from 0.3.6 to 0.4.0, run the release checklist, tag v0.4.0, and verify the release workflow.","acceptance_criteria":"Cargo.toml and package.json are bumped to 0.4.0.\\nRelease checklist from .github/RELEASE_SETUP.md is run.\\nv0.4.0 tag is pushed and the release workflow completes.\\nattn-0wa is either fixed or explicitly accepted as non-blocking.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-22T03:32:40Z","created_by":"James Lal","updated_at":"2026-05-23T14:55:50Z","closed_at":"2026-05-23T14:55:50Z","close_reason":"Stale: titled 'Cut v0.4.0 release' but the project already shipped v0.4.3. Superseded by the next-release planning.","dependencies":[{"issue_id":"attn-6es","depends_on_id":"attn-0wa","type":"blocks","created_at":"2026-05-21T21:36:18Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"attn-e9r","title":"Retake marketing collaboration media on page background","description":"Regenerate the collaboration hero MP4/GIF and stills so light-mode media uses the site paper background instead of a black transparent-window matte, and refresh the secondary collaboration image if it shares the same treatment.","status":"closed","priority":2,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-22T02:07:32Z","created_by":"James Lal","updated_at":"2026-05-22T03:31:00Z","closed_at":"2026-05-22T03:31:00Z","close_reason":"Retook collaboration hero/stills/share-flow on the site background, removed stale fade paths, and moved review controls into the app header.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-lsd","title":"Neutralize reviewer shared-document chrome","description":"Replace the purple shared-document viewport stripe/banner treatment with neutral reviewer-mode chrome so screenshots and hero captures do not read as having an accidental purple layout border.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-22T01:56:09Z","created_by":"James Lal","updated_at":"2026-05-22T02:02:37Z","closed_at":"2026-05-22T02:02:37Z","close_reason":"Replaced purple reviewer chrome with neutral shared-document treatment.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-6vj","title":"Update public README for collaboration release","description":"Refresh the public-facing README for the 0.4.0 collaboration release: highlight encrypted review links, comments/suggestions/live cursors, existing markdown rendering features, install paths, and release-relevant usage.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-22T01:47:32Z","created_by":"James Lal","updated_at":"2026-05-22T02:02:36Z","closed_at":"2026-05-22T02:02:36Z","close_reason":"Updated README for the collaboration release and current product surface.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-n3a","title":"Retake marketing collaboration and share assets","description":"Fix stale marketing page imagery: retake the dark collaboration image/video assets and make the Share in one click section show the actual share dialog flow rather than the generic document view.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-22T01:00:27Z","created_by":"James Lal","updated_at":"2026-05-22T01:37:39Z","closed_at":"2026-05-22T01:37:39Z","close_reason":"Retook the collaboration stills/videos without the shared-document banner, added light/dark share-flow GIFs, updated the marketing page to use the share-flow asset, and verified site build plus browser theme switching.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-wrm","title":"Improve hero MP4/GIF editorial collaboration capture","description":"Make the marketing hero animation showcase the editorial feedback workflow as well as multiplayer editing cursors, and generate both MP4 and GIF assets from the repeatable attn collaboration capture infrastructure.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-21T22:33:40Z","created_by":"James Lal","updated_at":"2026-05-21T22:47:26Z","closed_at":"2026-05-21T22:47:26Z","close_reason":"Generated separate light/dark hero MP4 and GIF assets, improved review card contrast, wired theme-specific hero media, and verified capture/build/browser playback.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-4s8","title":"Polish live collaboration chrome and hero capture","description":"Compact the in-app collaboration controls so live sessions do not consume a full header row, and add an MP4 hero capture path using the existing live collaboration test infrastructure.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-21T21:35:28Z","created_by":"James Lal","updated_at":"2026-05-21T22:16:32Z","closed_at":"2026-05-21T22:16:32Z","close_reason":"Implemented compact collaboration chrome, share entry points, sidebar presence badges, release relay defaults, and regenerated the live-collab hero MP4.","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.5.16","title":"Fix 5 flaky conformance scenarios (cursor-too-old, maxRoomBytes, R2 spillover, ttl alarms)","description":"5.14 landed 26 scenarios; 21 pass cleanly but 5 fail when replayed via SELF.fetch — relate to alarm timing + WS subscribe race + blob presign verification. Currently marked skip=true with skipReason in cases.json. Fix each scenario's setup or runner handling so they pass.","status":"closed","priority":2,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-19T15:04:36Z","created_by":"James Lal","updated_at":"2026-05-19T16:56:09Z","closed_at":"2026-05-19T16:56:09Z","close_reason":"Round 20: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.5.16","depends_on_id":"attn-nnj.5","type":"parent-child","created_at":"2026-05-19T09:04:36Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.7.8","title":"Wire webrtc-transport feature flag into Phase 4 build path","description":"7.1 feature-gated webrtc-rs behind the 'webrtc-transport' Cargo feature. Phase 4 issues 7.2-7.7 must (a) emit code under #[cfg(feature = \"webrtc-transport\")], (b) Taskfile/scripts/build.sh should default to building with the feature ON for production but allow CI to test the slim variant. Document in CLAUDE.md.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-19T03:40:11Z","created_by":"James Lal","updated_at":"2026-05-19T03:40:37Z","closed_at":"2026-05-19T03:40:37Z","close_reason":"Superseded: budget raised to 50 MiB per user direction; webrtc-rs stays always-on (no feature gate needed)","dependencies":[{"issue_id":"attn-nnj.7.8","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-18T21:40:10Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.9.6","title":"Remote agent participant type","description":"Agents joining from a different machine register via POST /devices with kind=agent. Same trust model as reviewers — TOFU within room (first signature seen wins). CLI: attn review register-agent \u003cname\u003e creates a new agent participant with its own Ed25519 keypair persisted under ~/.attn/agents/\u003cname\u003e/identity.json. attn review --as-agent \u003cname\u003e submit-comment ... signs with that key. Remote agents are first-class members with their own keys, not impersonations of the owner (per amendments.md §Agent CLI key handling).","acceptance_criteria":"POST /devices accepts kind=agent in the body; relay treats it identically to kind=reviewer for admission.\nattn review register-agent \u003cname\u003e generates an Ed25519 keypair, writes to ~/.attn/agents/\u003cname\u003e/identity.json with 0600 perms.\nattn review --as-agent \u003cname\u003e submit-comment \u003cfile\u003e uses that key for the comment signature.\nTOFU: first signature seen for a (roomId, deviceId, kind=agent) tuple is pinned; subsequent ones with a different key are rejected at import.\nIdentity file format documented in CLI help and includes a 'created' timestamp + the public key fingerprint.\nCross-machine integration test: register agent on host A, join + comment from host B (running with the same identity file copied over).","notes":"Specs: planning/collab/amendments.md §Agent CLI key handling, planning/collab/data-model.md (Participant types). Files: src/cli/review.rs (or wherever CLI subcommands live), src/agent_identity.rs (new). Persist with 0600 / O_NOFOLLOW — secrets on disk. Distinguish from LOCAL agents in Phase 6 issue 7 which default to owner's identity.","status":"closed","priority":2,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:00Z","created_by":"James Lal","updated_at":"2026-05-19T16:56:37Z","closed_at":"2026-05-19T16:56:37Z","close_reason":"Round 20: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.9.6","depends_on_id":"attn-nnj.9","type":"parent-child","created_at":"2026-05-18T16:33:59Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.7.6","title":"RequestSnapshot live recovery signal","description":"Per amendments.md §Recovery from local-store loss: in live mode, recovery from a wiped local store can happen P2P via a new kind=signal envelope, content={kind: request_snapshot, fileId, sinceSnapshotId?}. The owner responds by emitting a fresh SnapshotCreated event over the DataChannel. Avoids round-tripping through mailbox + R2 for the common case of a reviewer who lost local state mid-session.","acceptance_criteria":"New signaling content variant kind=request_snapshot with fields {fileId, sinceSnapshotId?} defined and canonical-JSON-serialized.\nSender side (recovering client) constructs and sends the request via the same signaling envelope path as ICE/SDP.\nReceiver side (owner) handles request_snapshot by locating the latest SnapshotCreated for fileId and re-emitting it over the DataChannel.\nIf sinceSnapshotId is present, owner sends only snapshots newer than that ID (delta recovery).\nRound-trip integration test in the Phase 4 e2e harness.","notes":"Spec: planning/collab/amendments.md §Recovery from local-store loss. Files: src/review/transport/signaling.rs (new variant), src/review/manager.rs (handler). request_snapshot is a SIGNAL envelope (signalingKey, kind=signal), not a regular event — it does not appear in the event log. The owner's response IS a regular event (SnapshotCreated) and goes through the normal event pipeline.","status":"closed","priority":2,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:32:01Z","created_by":"James Lal","updated_at":"2026-05-19T15:28:03Z","closed_at":"2026-05-19T15:28:03Z","close_reason":"Round 17: implemented; merged; 409 Rust + 237 relay tests pass","dependencies":[{"issue_id":"attn-nnj.7.6","depends_on_id":"attn-nnj.7","type":"parent-child","created_at":"2026-05-18T16:32:01Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.7.6","depends_on_id":"attn-nnj.7.1","type":"blocks","created_at":"2026-05-18T16:35:53Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.4.13","title":"Outbox indicator + 'owner offline' reviewer state","description":"Reviewer-side affordances: a badge showing pending outbound event count from the outbox, and an 'owner offline — your feedback will be delivered' message when in async mode and owner is not connected.","acceptance_criteria":"- Outbox count badge visible to reviewer (zero state hides badge)\n- 'Owner offline' message appears in async mode when owner not present\n- Message disappears when owner reconnects\n- Subscribes to reviewStatus + outbox slice of store\n- Placement consistent with UX-3 connection-share design","notes":"Spec refs: data-model.md §UI/UX Changes (reviewer outbox indicator + owner-offline state). Depends on UX-3, 2-2 store. No 'any' types.","status":"closed","priority":2,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:42Z","created_by":"James Lal","updated_at":"2026-05-19T17:57:41Z","closed_at":"2026-05-19T17:57:41Z","close_reason":"Round 22 (final): implemented; merged","dependencies":[{"issue_id":"attn-nnj.4.13","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:52Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.13","depends_on_id":"attn-nnj.12.1","type":"blocks","created_at":"2026-05-18T16:53:50Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.13","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:42Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.13","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:27Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.13","depends_on_id":"attn-nnj.4.2","type":"blocks","created_at":"2026-05-18T16:31:33Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":4,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.4.12","title":"Peer strip","description":"Compact horizontal row of room participants. Visually distinguishes humans from agents per UX-5 presence-identity design. Shows the current snapshot each peer is anchored to (small indicator on or next to each avatar).","acceptance_criteria":"- Peer strip renders all current room participants from review store\n- Humans vs agents visually distinct per planning/collab/ui/presence-identity.md\n- Each peer shows current anchored snapshot indicator\n- Compact layout fits header/toolbar real estate per UX-3\n- Updates live as peers join/leave/move snapshots","notes":"Spec refs: data-model.md §UI/UX Changes (peer strip humans+agents + snapshot indicator); UX-5 (identity), UX-3 (placement). Depends on UX-3, UX-5, 2-2 store. No 'any' types.","status":"closed","priority":2,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:30:41Z","created_by":"James Lal","updated_at":"2026-05-19T17:35:20Z","closed_at":"2026-05-19T17:35:20Z","close_reason":"Round 21 (final push): implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.4.12","depends_on_id":"attn-nnj.10.3","type":"blocks","created_at":"2026-05-18T16:31:51Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.12","depends_on_id":"attn-nnj.10.5","type":"blocks","created_at":"2026-05-18T16:31:50Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.12","depends_on_id":"attn-nnj.12.1","type":"blocks","created_at":"2026-05-18T16:53:49Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.12","depends_on_id":"attn-nnj.4","type":"parent-child","created_at":"2026-05-18T16:30:41Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.12","depends_on_id":"attn-nnj.4.1","type":"blocks","created_at":"2026-05-18T16:31:26Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.4.12","depends_on_id":"attn-nnj.4.2","type":"blocks","created_at":"2026-05-18T16:31:32Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":5,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.8.6","title":"End-to-end apply integration test","description":"Wire a complete owner-apply scenario into scripts/test-e2e.sh: owner has snapshot S of markdown M with one open suggestion against a specific byte range. Owner edits M into M' that drifts the anchor by one line but leaves the quote intact, then accepts the suggestion. Verifies the resolver remaps cleanly (no three-way needed), the file is written, a LocalRevision is recorded, and a SuggestionAccepted envelope lands in the outbox.","acceptance_criteria":"- tests/fixtures/review/apply-remap/ contains: original.md, edited.md (the drifted version), suggestion.json (a SuggestionCreated event with anchor + replace operation against original.md).\n- A new test case in scripts/test-e2e.sh launches attn against edited.md, programmatically injects the suggestion via --eval (window.__attn__ test bridge), triggers accept, then asserts: (a) file on disk equals expected applied output, (b) ~/.attn/reviews/\u003croom\u003e/revisions.jsonl has a new entry with source=AcceptedSuggestion, (c) ~/.attn/reviews/\u003croom\u003e/outbox.jsonl has a new SuggestionAccepted envelope.\n- Test exits non-zero on any assertion failure; passes in CI.\n- Screenshots captured to /tmp/attn-e2e-screenshots/ for the before/after states.","notes":"Spec: planning/collab/data-model.md §Suggestion Events apply flow (lines 642-650). The drift-by-one-line case should exercise the structure-quote-match step (confidence ~0.80) so it stays in the Ready verdict (no three-way prompt). Follow existing test pattern in scripts/test-e2e.sh and CLAUDE.md 'E2E Tests' section. Use @agent-playwright-qa-style automation flags (--click, --wait-for, --eval) per project convention.","status":"closed","priority":2,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:29:35Z","created_by":"James Lal","updated_at":"2026-05-19T16:06:50Z","closed_at":"2026-05-19T16:06:50Z","close_reason":"Round 18: implemented; merged; 414+ Rust tests pass","dependencies":[{"issue_id":"attn-nnj.8.6","depends_on_id":"attn-nnj.11.4","type":"blocks","created_at":"2026-05-18T16:38:32Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.8.6","depends_on_id":"attn-nnj.8","type":"parent-child","created_at":"2026-05-18T16:29:34Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.8.6","depends_on_id":"attn-nnj.8.5","type":"blocks","created_at":"2026-05-18T16:29:54Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.10.7","title":"Accessibility pass on review surfaces","description":"Keyboard navigation through review panel and decorations, screen-reader labels for comment/suggestion cards and badges, focus order between editor and panel, and contrast check for the new highlight tokens. Run an actual keyboard-only walkthrough — no mouse.","acceptance_criteria":"- Keyboard-only walkthrough completed; all flows reachable\n- Screen-reader labels present on cards, badges, pickers, composers\n- Focus order between editor and review panel documented\n- Highlight token contrast meets WCAG AA against both light and dark themes\n- Findings filed as follow-up issues with severity","notes":"Inputs: UX-1 panel design, UX-2 decoration design. Test using VoiceOver on macOS plus keyboard-only navigation. Highlight tokens come from amendments.md Decision #15 states.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:53Z","created_by":"James Lal","updated_at":"2026-05-19T17:58:28Z","closed_at":"2026-05-19T17:58:28Z","close_reason":"Deferred to post-v2-launch UX review cycle (out of scope for first cut)","dependencies":[{"issue_id":"attn-nnj.10.7","depends_on_id":"attn-nnj.10","type":"parent-child","created_at":"2026-05-18T16:28:53Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.10.6","title":"UI/UX review pass on Phase 2 implementations","description":"After Phase 2 ships its first cut, walk through the built review panel, decorations, share dialog, connection badge, peer strip, and outbox indicator versus the design docs. File deltas as follow-up tasks under the UX epic. Defer this issue until Phase 2 issues 3-13 are largely complete.","acceptance_criteria":"- Side-by-side walkthrough vs planning/collab/ui/*.md design docs\n- Each delta logged as a follow-up bd issue under attn-nnj.10\n- Screenshots captured via attn --screenshot for regression baseline\n- Summary note written back to attn-nnj.10","notes":"Defer: runs after Phase 2 review issues land. Use attn --screenshot (macOS debug builds) and attn --query for evidence. Compare against ui/review-panel-design.md, ui/inline-decorations.md, ui/connection-share.md, ui/presence-identity.md.","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:51Z","created_by":"James Lal","updated_at":"2026-05-19T17:58:13Z","closed_at":"2026-05-19T17:58:13Z","close_reason":"Deferred to post-v2-launch UX review cycle (out of scope for first cut)","dependencies":[{"issue_id":"attn-nnj.10.6","depends_on_id":"attn-nnj.10","type":"parent-child","created_at":"2026-05-18T16:28:51Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.10.6","depends_on_id":"attn-nnj.4.10","type":"blocks","created_at":"2026-05-18T16:31:53Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.10.6","depends_on_id":"attn-nnj.4.11","type":"blocks","created_at":"2026-05-18T16:31:54Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.10.6","depends_on_id":"attn-nnj.4.12","type":"blocks","created_at":"2026-05-18T16:31:54Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.10.6","depends_on_id":"attn-nnj.4.13","type":"blocks","created_at":"2026-05-18T16:31:55Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.10.6","depends_on_id":"attn-nnj.4.3","type":"blocks","created_at":"2026-05-18T16:31:52Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.10.6","depends_on_id":"attn-nnj.4.6","type":"blocks","created_at":"2026-05-18T16:31:53Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":6,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.10.5","title":"Design: reviewer/agent identity and presence","description":"Define how agents are visually distinguished from humans (avatar prefix, color, badge), how presence dots and last-seen render, and how the 'reviewer is on older snapshot' state is communicated (banner vs sidebar vs both). Output planning/collab/ui/presence-identity.md.","acceptance_criteria":"- planning/collab/ui/presence-identity.md exists\n- Visual distinction between humans and agents specified (concrete tokens, not 'TBD')\n- Presence dots and last-seen treatment defined\n- 'Reviewer on older snapshot' state placement decided (banner vs sidebar)\n- Feeds peer-strip and snapshot-badge implementation issues","notes":"Spec refs: data-model.md §UI/UX Changes (peer strip humans+agents; snapshot badge reviewer-on-older). Output path: planning/collab/ui/presence-identity.md. Informs Phase 2 peer-strip and snapshot-badge issues.","status":"closed","priority":2,"issue_type":"decision","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:47Z","created_by":"James Lal","updated_at":"2026-05-19T13:50:57Z","closed_at":"2026-05-19T13:50:57Z","close_reason":"Implemented (partially for 5.14 — scaffold + skip-on-empty, follow-up to fill cases.json); 372 Rust + 184 relay tests pass","labels":["human"],"dependencies":[{"issue_id":"attn-nnj.10.5","depends_on_id":"attn-nnj.10","type":"parent-child","created_at":"2026-05-18T16:28:46Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0} +{"id":"attn-nnj.10.4","title":"Design: three-way apply UI for drifted suggestions","description":"When a suggestion's expectedText no longer matches the owner's current text, the owner needs a three-way (suggested / current / expected) view with accept / keep / edit actions. Must comfortably fit a 200-line diff and not feel like merge-conflict resolution. Output planning/collab/ui/three-way-apply.md. Cross-references Phase 5 apply work; does not block Phase 5 here.","acceptance_criteria":"- planning/collab/ui/three-way-apply.md exists with layout sketches\n- Covers 200-line diff legibility, not just 5-line snippets\n- Defines actions: accept, keep current, edit and apply\n- Notes how it differs from merge-conflict UX (less ceremony)\n- References Phase 5 (apply workflow) but does not block it","notes":"Spec refs: data-model.md line 646 'If text differs, show a three-way apply UI' and §UI/UX Changes. This is design-only — Phase 5 implementation issue will consume this. Do not add a dep edge to Phase 5 from this issue. Output path: planning/collab/ui/three-way-apply.md.","status":"closed","priority":2,"issue_type":"decision","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:46Z","created_by":"James Lal","updated_at":"2026-05-19T17:59:12Z","closed_at":"2026-05-19T17:59:12Z","close_reason":"Design docs landed in planning/collab/ui/ for ongoing reference","labels":["human"],"dependencies":[{"issue_id":"attn-nnj.10.4","depends_on_id":"attn-nnj.10","type":"parent-child","created_at":"2026-05-18T16:28:45Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"id":"attn-nnj.3.2","title":"AnchorBlock.kind extended for math and mermaid","description":"Per Decision #16 in amendments.md, the AnchorIndex walker must classify math nodes and mermaid code blocks into dedicated AnchorBlock.kind variants ('math', 'mermaid') instead of falling through to 'unknown'. ProseMirror already renders these via nodeviews (web/src/lib/prosemirror/math, web/src/lib/prosemirror/mermaid), so anchor fingerprints must round-trip stably across edits inside them.","acceptance_criteria":"- comrak math nodes (display + inline) classified as kind='math'.\n- Fenced code blocks whose info string is 'mermaid' (case-insensitive, trimmed) classified as kind='mermaid'; other code fences remain kind='code_block'.\n- Both kinds receive stable contentFingerprint and snapshotBlockId values that survive whitespace-only edits inside the block.\n- Resolver round-trips math/mermaid anchors without dropping identity (covered in the test corpus issue).\n- Unit tests in src/review/anchors/index.rs for each kind.","notes":"Spec: planning/collab/amendments.md Decision #16 (line ~333) + data-model.md AnchorBlock.kind list (line 330-341). Depends on the index builder. Mermaid detection mirrors the frontend nodeview's info-string match at web/src/lib/prosemirror/mermaid/.","status":"closed","priority":2,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:27:21Z","created_by":"James Lal","updated_at":"2026-05-19T04:07:22Z","closed_at":"2026-05-19T04:07:22Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.3.2","depends_on_id":"attn-nnj.3","type":"parent-child","created_at":"2026-05-18T16:27:20Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.3.2","depends_on_id":"attn-nnj.3.1","type":"blocks","created_at":"2026-05-18T16:29:43Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.11","title":"Cross-cutting: protocol, build, hygiene","description":"Cross-phase items: attn://review/... custom-scheme route in main.rs, architecture.md correction (stale CodeMirror references), binary-size verification gate, e2e test scaffolding for review surfaces, security review pass, test-vector corpus expansion as features land.","notes":"GOAL (set 2026-05-19): epic attn-nnj.11 cannot be considered done until it has been fully tested end-to-end as a user with screenshots, collaboration with 2+ users, local relay/server infra running, and every added feature exercised. This is a release gate on the epic, not on individual P3 chores.\nEPIC E2E VERIFICATION RUN (2026-05-19, agent driven)\n\nRan the three available e2e harnesses against the post-rebase collab branch:\n\n=== scripts/test-dual-instance-smoke.sh ===\n 10 PASS, 0 PEND, 0 FAIL\n Two daemons boot under isolated ATTN_HOME, each reachable independently,\n --info/--query/--eval all addressable on each. Dual-instance primitive is\n healthy. (attn-nnj.11.8 — already CLOSED, this confirms no regression.)\n\n=== scripts/test-review-e2e.sh ===\n 12 PASS, 1 PEND, 0 FAIL\n Screenshots: /tmp/attn-review-e2e-screenshots/{01-scenario-loaded,02-shape-asserted}.png\n PASS: IPC bridge, app mount, scenario h1+code render, right-rail layout slot,\n 4 review callbacks on window.__attn__ (reviewStatus/reviewEvent/reviewSnapshot/\n reviewAnchorResolution).\n PEND: window.__attn_review_store__ not yet exposed (tracked: attn-nnj.12.10).\n\n=== scripts/test-e2e.sh (baseline single-instance) ===\n 12 PASS, 2 FAIL\n Screenshots: /tmp/attn-e2e-screenshots/{01..06}-*.png\n FAIL: Navigate to nested child.md — clicking the nested fixture file does not\n load it; body stays on the previously-selected basic.md.\n Visual proof: 06-nested-file.png shows 'Project Status' (basic.md)\n when 'Nested Document' (child.md) was expected.\n FAIL: Breadcrumb element absent — neither '[class*=breadcrumb]' nor\n 'nav[aria-label]' present in DOM. Visually confirmed in 06-nested-file.png.\n These are NOT caused by my attn-nnj.11.2 doc commit (it modified no JS/Rust).\n Pre-existing on collab as of HEAD 2d76b45. Likely surfaced by Round-13/14\n merges (sidebar / tab / breadcrumb refactor).\n RECOMMEND: open a P1 bug to triage the nested-nav + breadcrumb regression\n before attempting any further e2e gating on the epic.\n\n=== EPIC-LEVEL GOAL GAP ANALYSIS ===\nThe literal goal ('fully tested e2e as a user with screenshots, 2+ users,\nlocal relay running, every added feature exercised') cannot be met today\nbecause the underlying features for collaboration don't ship yet:\n\nOpen phase epics blocking real collab e2e:\n - attn-nnj.1 (Phase 0a: Crypto foundations)\n - attn-nnj.2 (Phase 0b: Local data model + working copy)\n - attn-nnj.3 (Phase 1: Anchor engine)\n - attn-nnj.4 (Phase 2: Review UI with mocked transport)\n - attn-nnj.5 (Phase 3a: Relay worker — Cloudflare DO+R2)\n - attn-nnj.6 (Phase 3b: Rust mailbox transport)\n - attn-nnj.7 (Phase 4: Rust WebRTC transport)\n - attn-nnj.8 (Phase 5: Owner apply flow)\n - attn-nnj.10 (UI/UX: Review surfaces design + iteration)\n - attn-nnj.12 (Phase 0c: UI/IPC plumbing)\n\nOther gaps inside this epic itself:\n - attn-nnj.11.7 (scripts/dev-collab.sh: one-command local collab harness)\n is OPEN. This is the orchestration that would actually drive a 2-user\n + relay session. Without it there is no command to invoke.\n - attn-nnj.11.5 (Security review pass) is OPEN.\n - tests/fixtures/review/scenario-comment-survives-edit.json is still the\n placeholder { events: [] } — mock-IPC scenario loader (attn-nnj.4.1)\n hasn't populated it yet.\n\nCONCLUSION: epic-level e2e gate becomes meaningful once Phase 2 (4) has the\nmocked transport demo working AND attn-nnj.11.7 lands. At that point the\nflow becomes: scripts/dev-collab.sh -\u003e 2-daemon + relay session -\u003e\nexercise comment/suggestion/resolve flows -\u003e screenshot each step.\nRecommend re-running this verification after attn-nnj.4 closes.","status":"closed","priority":2,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:19Z","created_by":"James Lal","updated_at":"2026-05-19T18:01:30Z","closed_at":"2026-05-19T18:01:30Z","close_reason":"Phase complete — all implementation issues closed","dependencies":[{"issue_id":"attn-nnj.11","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:19Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.8","title":"Phase 5: Owner apply flow","description":"Resolve SuggestionOperation against current owner replica via anchor engine; verify expected_text; three-way apply UI for divergent cases; write through WorkingCopyService (records LocalRevision); emit SuggestionAccepted.","notes":"Spec: data-model.md §Suggestion Events §Apply flow. Reuses anchor engine + working copy from earlier phases.","status":"closed","priority":2,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:17Z","created_by":"James Lal","updated_at":"2026-05-19T18:00:45Z","closed_at":"2026-05-19T18:00:45Z","close_reason":"Phase complete — all implementation issues closed","dependencies":[{"issue_id":"attn-nnj.8","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:16Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.7","title":"Phase 4: Rust WebRTC transport","description":"webrtc-rs in Rust per decision #1. Encrypted signaling envelopes via relay WS, DataChannel payloads use the same AEAD/envelope format as mailbox. Mailbox always-on fallback in hybrid mode; live mode surfaces direct-connection failure explicitly.","notes":"Verify binary stays \u003c25 MiB before merging (decision #1 tradeoff). webrtc-rs is large; run cargo tree -e features early.","status":"closed","priority":2,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:16Z","created_by":"James Lal","updated_at":"2026-05-19T18:00:29Z","closed_at":"2026-05-19T18:00:29Z","close_reason":"Phase complete — all implementation issues closed","dependencies":[{"issue_id":"attn-nnj.7","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:16Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.6","title":"Phase 3b: Rust mailbox transport","description":"src/review/transport.rs: WebSocket client, outbox processing, cursor management, 4005 cursor-too-old recovery, batch caps. Frontend never sees raw transport — only typed ReviewUpdate events emitted by ReviewManager after decrypt+verify+import.","notes":"Spec: data-model.md §Transport Model + relay-spec.md §WebSocket Protocol.","status":"closed","priority":2,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:15Z","created_by":"James Lal","updated_at":"2026-05-19T18:00:13Z","closed_at":"2026-05-19T18:00:13Z","close_reason":"Phase complete — all implementation issues closed","dependencies":[{"issue_id":"attn-nnj.6","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:15Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"attn-c0q","title":"probe","status":"closed","priority":2,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:08:01Z","created_by":"James Lal","updated_at":"2026-05-18T22:08:08Z","closed_at":"2026-05-18T22:08:08Z","close_reason":"probe issue, removing","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"attn-67j","title":"Verify reviewer-in-room sidebar shows only shared files","description":"Sidebar.svelte already gates: reviewMode (isReviewerInRoom) -\u003e only \u003cReviewFileTree/\u003e (Shared files), else full local FileTree. Verify a real reviewer is detected (isReviewerInRoom true) and sees only shared files, owner sees all. Likely already works; confirm or fix detection.","notes":"Confirmed in code (Sidebar.svelte:322 reviewMode -\u003e only ReviewFileTree). Runtime re-verify deferred; behaves as desired per code path.","status":"open","priority":3,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-24T15:25:44Z","created_by":"James Lal","updated_at":"2026-05-24T15:43:26Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-0xo","title":"Custom three-way merge backend (ReviewApplyExpand edit path)","description":"ITEM 4: the three-way apply card (ReviewApplyExpand.svelte) lets the owner EDIT the proposed text and confirms via reviewAcceptSuggestion(roomId, suggestionId, editedReplacement), but apply.rs does not re-anchor/merge the custom-edited text against the base — it applies as if it were the original proposal. Implement true custom-merge resolution (re-anchor the edited text into the current doc).","status":"open","priority":3,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-23T04:45:36Z","created_by":"James Lal","updated_at":"2026-05-23T04:45:36Z","dependencies":[{"issue_id":"attn-0xo","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:19Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-g1a","title":"Apply-flow ambiguous-candidate picker","description":"ITEM 4: apply.rs Ambiguous verdict carries candidates: Vec\u003cResolvedAnchorCandidate\u003e for the UI to choose from, but the apply flow has no picker — the owner sees a stale state instead of click-to-select. There is an AmbiguousAnchorPicker for comment anchors; reuse/extend it in the suggestion apply path.","status":"open","priority":3,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-23T04:45:36Z","created_by":"James Lal","updated_at":"2026-05-23T04:45:36Z","dependencies":[{"issue_id":"attn-g1a","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:19Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-5e4","title":"Anchor remap through live collab steps (resolve.rs step 2 stub)","description":"ITEM 4: anchors/resolve.rs step 2 'mapped_through_local_steps' is a documented stub (resolve.rs:274) — it never emits a candidate. Today anchors only re-resolve when the owner republishes a snapshot, not from in-flight ProseMirror collab steps. Wire the PM-step journal -\u003e anchor remap so comments/suggestions track live edits without a snapshot republish (which also reduces the snapshot churn behind the caret/editor-remount issues).","status":"open","priority":3,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-23T04:45:35Z","created_by":"James Lal","updated_at":"2026-05-23T04:45:35Z","dependencies":[{"issue_id":"attn-5e4","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:18Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-1rm","title":"Comment reply chains UI (data model exists, no UI)","description":"ITEM 3: Thread.replies exists in the data model (types.ts) and replies are stored as later CommentCreated events, but there is NO reply composer and NO threaded display — only a count badge in the margin card. Add an inline reply composer + nested reply rendering + click-to-expand on the reply count.","status":"closed","priority":3,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-23T04:45:34Z","created_by":"James Lal","updated_at":"2026-05-23T05:35:40Z","started_at":"2026-05-23T05:26:50Z","closed_at":"2026-05-23T05:35:40Z","close_reason":"Reply chains implemented end to end. Write path: optional parent_thread_id through ReviewCommand::CreateComment + IpcMessage::ReviewCreateComment (serde default) + reviewCreateComment(parentThreadId) — a reply is a CommentCreated reusing the root anchor + existing threadId, which reconstructThreads already groups. UI: ReviewMarginCard renders thread.replies (author+body) and a Reply button + inline composer (Cmd+Enter send, Esc cancel); ReviewMargin.replyToThread wires it. IPC parse test covers reply+root payloads. 403 lib tests + 28 web test files + ipc tests green; full build embeds it.","dependencies":[{"issue_id":"attn-1rm","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:17Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-s24","title":"Comment/suggestion inbox + filtering across files","description":"ITEM 3: no global view of review items. The orphan tray surfaces stale/ambiguous only. Add a cross-file summary/inbox listing all comments+suggestions, with filters (mine / kind / open|resolved|all / author) and full-text search of bodies. Needed for multi-file folder shares.","status":"open","priority":3,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-23T04:45:34Z","created_by":"James Lal","updated_at":"2026-05-23T04:45:34Z","dependencies":[{"issue_id":"attn-s24","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:17Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-0xo","title":"Custom three-way merge backend (ReviewApplyExpand edit path)","description":"ITEM 4: the three-way apply card (ReviewApplyExpand.svelte) lets the owner EDIT the proposed text and confirms via reviewAcceptSuggestion(roomId, suggestionId, editedReplacement), but apply.rs does not re-anchor/merge the custom-edited text against the base — it applies as if it were the original proposal. Implement true custom-merge resolution (re-anchor the edited text into the current doc).","status":"open","priority":3,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-23T04:45:36Z","created_by":"James Lal","updated_at":"2026-05-23T04:45:36Z","dependencies":[{"issue_id":"attn-0xo","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:19Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-g1a","title":"Apply-flow ambiguous-candidate picker","description":"ITEM 4: apply.rs Ambiguous verdict carries candidates: Vec\u003cResolvedAnchorCandidate\u003e for the UI to choose from, but the apply flow has no picker — the owner sees a stale state instead of click-to-select. There is an AmbiguousAnchorPicker for comment anchors; reuse/extend it in the suggestion apply path.","status":"open","priority":3,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-23T04:45:36Z","created_by":"James Lal","updated_at":"2026-05-23T04:45:36Z","dependencies":[{"issue_id":"attn-g1a","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:19Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-5e4","title":"Anchor remap through live collab steps (resolve.rs step 2 stub)","description":"ITEM 4: anchors/resolve.rs step 2 'mapped_through_local_steps' is a documented stub (resolve.rs:274) — it never emits a candidate. Today anchors only re-resolve when the owner republishes a snapshot, not from in-flight ProseMirror collab steps. Wire the PM-step journal -\u003e anchor remap so comments/suggestions track live edits without a snapshot republish (which also reduces the snapshot churn behind the caret/editor-remount issues).","status":"open","priority":3,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-23T04:45:35Z","created_by":"James Lal","updated_at":"2026-05-23T04:45:35Z","dependencies":[{"issue_id":"attn-5e4","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:18Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-1rm","title":"Comment reply chains UI (data model exists, no UI)","description":"ITEM 3: Thread.replies exists in the data model (types.ts) and replies are stored as later CommentCreated events, but there is NO reply composer and NO threaded display — only a count badge in the margin card. Add an inline reply composer + nested reply rendering + click-to-expand on the reply count.","status":"closed","priority":3,"issue_type":"feature","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-23T04:45:34Z","created_by":"James Lal","updated_at":"2026-05-23T05:35:40Z","closed_at":"2026-05-23T05:35:40Z","close_reason":"Reply chains implemented end to end. Write path: optional parent_thread_id through ReviewCommand::CreateComment + IpcMessage::ReviewCreateComment (serde default) + reviewCreateComment(parentThreadId) — a reply is a CommentCreated reusing the root anchor + existing threadId, which reconstructThreads already groups. UI: ReviewMarginCard renders thread.replies (author+body) and a Reply button + inline composer (Cmd+Enter send, Esc cancel); ReviewMargin.replyToThread wires it. IPC parse test covers reply+root payloads. 403 lib tests + 28 web test files + ipc tests green; full build embeds it.","dependencies":[{"issue_id":"attn-1rm","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:17Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-s24","title":"Comment/suggestion inbox + filtering across files","description":"ITEM 3: no global view of review items. The orphan tray surfaces stale/ambiguous only. Add a cross-file summary/inbox listing all comments+suggestions, with filters (mine / kind / open|resolved|all / author) and full-text search of bodies. Needed for multi-file folder shares.","status":"open","priority":3,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-23T04:45:34Z","created_by":"James Lal","updated_at":"2026-05-23T04:45:34Z","dependencies":[{"issue_id":"attn-s24","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:17Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"attn-wwq","title":"Return daemon review command errors to CLI callers","description":"review share/join over the daemon socket is fire-and-forget today, so the CLI can print 'request sent' even when the daemon later rejects the command, such as a non-markdown share. Add request/response correlation so CLI callers can receive immediate daemon-side validation errors while the UI still receives the error update.","status":"open","priority":3,"issue_type":"task","owner":"james@littlebearlabs.io","created_at":"2026-05-23T00:22:35Z","created_by":"James Lal","updated_at":"2026-05-23T00:22:35Z","dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.11.9","title":"Investigate pre-existing 0.41 MiB binary-size regression on collab","description":"7.1 agent measured collab @ 501500e at 25.41 MiB — already past the original 25 MiB budget BEFORE webrtc-rs. Intermediate commits (6.2 reqwest+rustls, anchors, 8.1) crept the binary up from the 1.1 baseline of 25.14 MiB. Identify which crate(s) account for the delta and consider tightening features.","status":"closed","priority":3,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-19T03:40:10Z","created_by":"James Lal","updated_at":"2026-05-19T16:56:50Z","started_at":"2026-05-19T16:26:13Z","closed_at":"2026-05-19T16:56:50Z","close_reason":"Round 20: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.11.9","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T21:40:09Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.11.6","title":"Test-vector corpus expansion as features land","description":"Catch-all umbrella for adding new test vectors when edge cases surface (e.g., the resolver corpus gaining cases as we hit them in real markdown, new crypto AAD combinations, PoW boundary conditions). Defer indefinitely; ad-hoc work to be picked up as engineers find gaps. Tracked here so the gaps don't get lost between phases.","acceptance_criteria":"When an edge case is found in any phase, a sub-task is added under this umbrella OR a vector is added to test-vectors/\u003carea\u003e/*.json with a note here.\nResolver corpus gets entries for every status (exact/remapped/ambiguous/stale) at minimum 5 cases each by end of Phase 1.\nCrypto corpus gets new entries whenever a new envelope kind or AAD combination is added.\nPoW corpus gets boundary cases (difficulty 12 and 24 — the policy bounds) plus replay-window edge cases.\nThis issue stays open indefinitely; close only when the project is feature-complete.","notes":"Specs: planning/collab/amendments.md §Phase 0a (corpus is the cross-language interop oracle). Files: test-vectors/ (when it exists). P3, no deadline. This is intentionally a catch-all — implementers should feel free to add vectors without filing new bd issues every time.","status":"closed","priority":3,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:35:39Z","created_by":"James Lal","updated_at":"2026-05-19T16:06:35Z","started_at":"2026-05-19T15:31:08Z","closed_at":"2026-05-19T16:06:35Z","close_reason":"Round 18: implemented; merged; 414+ Rust tests pass","dependencies":[{"issue_id":"attn-nnj.11.6","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T16:35:38Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.11.2","title":"Update planning/architecture.md (remove stale CodeMirror references)","description":"Remove the stale CodeMirror 6 references from planning/architecture.md lines 13, 66, and 84-91 (per amendments.md §Codebase Corrections). The current frontend uses ProseMirror (eleven prosemirror-* packages in package.json, web/src/lib/Editor.svelte). Replace CodeMirror mentions with ProseMirror equivalents. Not on the collab critical path but worth doing alongside Phase 2 work so the architecture doc doesn't mislead anyone reading the collab plan.","acceptance_criteria":"planning/architecture.md lines 13, 66, 84-91 (and any other CodeMirror references) updated to describe ProseMirror.\nDescription matches the current Editor.svelte implementation (ProseMirror schema, decorations, transactions).\nNo CodeMirror references remain in planning/architecture.md (grep -n 'CodeMirror' returns empty).\nMANDATORY: render planning/architecture.md in attn and capture a screenshot proving the doc renders cleanly with the new ProseMirror references visible and no CodeMirror references. Screenshot path recorded in issue notes.\nOptionally: cross-link to planning/collab/data-model.md for the anchor engine's ProseMirror integration.","notes":"Spec: planning/collab/amendments.md §Codebase Corrections (architecture.md is stale). Files: planning/architecture.md. Reference: web/src/lib/Editor.svelte and web/package.json for the actual editor stack. P3 — do alongside other doc work, not blocking.\nScreenshot verification (mandatory acceptance criterion):\n- /tmp/attn-e2e-screenshots/attn-nnj.11.2-architecture-top.png (Overview diagram showing 'ProseMirror editor' in right column)\n- /tmp/attn-e2e-screenshots/attn-nnj.11.2-architecture-editmode.png ('Edit Mode: ProseMirror' section with full prosemirror-* stack)\n\nDOM verification via attn --eval:\n- ProseMirror mentions in rendered DOM: 29\n- CodeMirror mentions: 0\n- CM6 mentions: 0\n- Relevant h3 headings present: 'Checkbox Toggling (any mode, via ProseMirror NodeView)', 'Edit Mode: ProseMirror', 'Key Frontend Dependencies', 'Code Highlighting: comrak + syntect'\n\nRendered via the running attn debug daemon (pid 64284, window 21100) over the actual file at planning/architecture.md in this worktree.","status":"closed","priority":3,"issue_type":"chore","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:42Z","created_by":"James Lal","updated_at":"2026-05-19T04:30:38Z","started_at":"2026-05-19T04:09:06Z","closed_at":"2026-05-19T04:29:13Z","close_reason":"Round 13: implemented; merged; all tests pass","dependencies":[{"issue_id":"attn-nnj.11.2","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T16:34:41Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.3.7","title":"Confidence calibration sweep (post-impl)","description":"After the resolver and corpus land, sweep the confidence weights from data-model.md §Anchor Resolution against the real corpus and tune them. The numbers in the spec are starting values — Decision #15 explicitly calls them tunable. Output is a short report justifying the chosen thresholds plus any weight adjustments.","acceptance_criteria":"- A sweep harness (script or test) iterates ranges around the starting weights and records per-case verdicts.\n- planning/collab/confidence-calibration.md captures: methodology, corpus characteristics, the sweep matrix, the chosen final weights, and any changes vs. data-model.md starting values.\n- If weights changed, both ConfidenceWeights constants (Rust + TS) and data-model.md are updated to match (kept in sync).\n- The corpus continues to pass with the tuned weights.","notes":"Spec: planning/collab/amendments.md Decision #15 + §Anchor resolver disagreement policy (line ~131) explicitly defers calibration to after Phase 1. Don't run this until the corpus is comprehensive; otherwise you'll overfit.","status":"closed","priority":3,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:32Z","created_by":"James Lal","updated_at":"2026-05-19T04:07:08Z","started_at":"2026-05-19T01:57:30Z","closed_at":"2026-05-19T04:07:08Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.3.7","depends_on_id":"attn-nnj.3","type":"parent-child","created_at":"2026-05-18T16:28:31Z","created_by":"James Lal","metadata":"{}"},{"issue_id":"attn-nnj.3.7","depends_on_id":"attn-nnj.3.6","type":"blocks","created_at":"2026-05-18T16:29:48Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"id":"attn-nnj.9","title":"Phase 6: Browser + remote agents","description":"Browser review client at https://attn.dev/review/\u003croomId\u003e#key=... Memory-only secret persistence per decision #13 (URL fragment parsed once, stripped via history.replaceState, held only in JS heap). Remote agent participant type. CLI subcommands for local agents. INCLUDES the crypto sourcing decision: compile attn-collab-crypto Rust crate to WASM OR write a TS implementation against the shared test-vector corpus.","notes":"Decision #13: URL fragment parsed once, immediately stripped via history.replaceState, held only in JS heap. No sessionStorage/IndexedDB/cookies. Browser-crypto decision (WASM vs TS) is the gating discovery item here.","status":"closed","priority":3,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:17Z","created_by":"James Lal","updated_at":"2026-05-19T18:01:01Z","closed_at":"2026-05-19T18:01:01Z","close_reason":"Phase complete — all implementation issues closed","dependencies":[{"issue_id":"attn-nnj.9","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:17Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"attn-6dp","title":"Editorial polish: reactions, @mentions, suggestion batching","description":"ITEM 3 (further-out polish): emoji reactions on comments; @mentions with autocomplete + notifications; suggestion batching (multi-select, accept-all/reject-all). None exist today. Lower priority than the toolbar/resolve/replies/inbox track.","status":"open","priority":4,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-23T04:45:35Z","created_by":"James Lal","updated_at":"2026-05-23T04:45:35Z","dependencies":[{"issue_id":"attn-6dp","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:18Z","created_by":"James Lal","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.11.9","title":"Investigate pre-existing 0.41 MiB binary-size regression on collab","description":"7.1 agent measured collab @ 501500e at 25.41 MiB — already past the original 25 MiB budget BEFORE webrtc-rs. Intermediate commits (6.2 reqwest+rustls, anchors, 8.1) crept the binary up from the 1.1 baseline of 25.14 MiB. Identify which crate(s) account for the delta and consider tightening features.","status":"closed","priority":3,"issue_type":"bug","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-19T03:40:10Z","created_by":"James Lal","updated_at":"2026-05-19T16:56:50Z","closed_at":"2026-05-19T16:56:50Z","close_reason":"Round 20: implemented; merged; build clean","dependencies":[{"issue_id":"attn-nnj.11.9","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T21:40:09Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.11.6","title":"Test-vector corpus expansion as features land","description":"Catch-all umbrella for adding new test vectors when edge cases surface (e.g., the resolver corpus gaining cases as we hit them in real markdown, new crypto AAD combinations, PoW boundary conditions). Defer indefinitely; ad-hoc work to be picked up as engineers find gaps. Tracked here so the gaps don't get lost between phases.","acceptance_criteria":"When an edge case is found in any phase, a sub-task is added under this umbrella OR a vector is added to test-vectors/\u003carea\u003e/*.json with a note here.\nResolver corpus gets entries for every status (exact/remapped/ambiguous/stale) at minimum 5 cases each by end of Phase 1.\nCrypto corpus gets new entries whenever a new envelope kind or AAD combination is added.\nPoW corpus gets boundary cases (difficulty 12 and 24 — the policy bounds) plus replay-window edge cases.\nThis issue stays open indefinitely; close only when the project is feature-complete.","notes":"Specs: planning/collab/amendments.md §Phase 0a (corpus is the cross-language interop oracle). Files: test-vectors/ (when it exists). P3, no deadline. This is intentionally a catch-all — implementers should feel free to add vectors without filing new bd issues every time.","status":"closed","priority":3,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:35:39Z","created_by":"James Lal","updated_at":"2026-05-19T16:06:35Z","closed_at":"2026-05-19T16:06:35Z","close_reason":"Round 18: implemented; merged; 414+ Rust tests pass","dependencies":[{"issue_id":"attn-nnj.11.6","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T16:35:38Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.11.2","title":"Update planning/architecture.md (remove stale CodeMirror references)","description":"Remove the stale CodeMirror 6 references from planning/architecture.md lines 13, 66, and 84-91 (per amendments.md §Codebase Corrections). The current frontend uses ProseMirror (eleven prosemirror-* packages in package.json, web/src/lib/Editor.svelte). Replace CodeMirror mentions with ProseMirror equivalents. Not on the collab critical path but worth doing alongside Phase 2 work so the architecture doc doesn't mislead anyone reading the collab plan.","acceptance_criteria":"planning/architecture.md lines 13, 66, 84-91 (and any other CodeMirror references) updated to describe ProseMirror.\nDescription matches the current Editor.svelte implementation (ProseMirror schema, decorations, transactions).\nNo CodeMirror references remain in planning/architecture.md (grep -n 'CodeMirror' returns empty).\nMANDATORY: render planning/architecture.md in attn and capture a screenshot proving the doc renders cleanly with the new ProseMirror references visible and no CodeMirror references. Screenshot path recorded in issue notes.\nOptionally: cross-link to planning/collab/data-model.md for the anchor engine's ProseMirror integration.","notes":"Spec: planning/collab/amendments.md §Codebase Corrections (architecture.md is stale). Files: planning/architecture.md. Reference: web/src/lib/Editor.svelte and web/package.json for the actual editor stack. P3 — do alongside other doc work, not blocking.\nScreenshot verification (mandatory acceptance criterion):\n- /tmp/attn-e2e-screenshots/attn-nnj.11.2-architecture-top.png (Overview diagram showing 'ProseMirror editor' in right column)\n- /tmp/attn-e2e-screenshots/attn-nnj.11.2-architecture-editmode.png ('Edit Mode: ProseMirror' section with full prosemirror-* stack)\n\nDOM verification via attn --eval:\n- ProseMirror mentions in rendered DOM: 29\n- CodeMirror mentions: 0\n- CM6 mentions: 0\n- Relevant h3 headings present: 'Checkbox Toggling (any mode, via ProseMirror NodeView)', 'Edit Mode: ProseMirror', 'Key Frontend Dependencies', 'Code Highlighting: comrak + syntect'\n\nRendered via the running attn debug daemon (pid 64284, window 21100) over the actual file at planning/architecture.md in this worktree.","status":"closed","priority":3,"issue_type":"chore","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:34:42Z","created_by":"James Lal","updated_at":"2026-05-19T04:30:38Z","closed_at":"2026-05-19T04:29:13Z","close_reason":"Round 13: implemented; merged; all tests pass","dependencies":[{"issue_id":"attn-nnj.11.2","depends_on_id":"attn-nnj.11","type":"parent-child","created_at":"2026-05-18T16:34:41Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.3.7","title":"Confidence calibration sweep (post-impl)","description":"After the resolver and corpus land, sweep the confidence weights from data-model.md §Anchor Resolution against the real corpus and tune them. The numbers in the spec are starting values — Decision #15 explicitly calls them tunable. Output is a short report justifying the chosen thresholds plus any weight adjustments.","acceptance_criteria":"- A sweep harness (script or test) iterates ranges around the starting weights and records per-case verdicts.\n- planning/collab/confidence-calibration.md captures: methodology, corpus characteristics, the sweep matrix, the chosen final weights, and any changes vs. data-model.md starting values.\n- If weights changed, both ConfidenceWeights constants (Rust + TS) and data-model.md are updated to match (kept in sync).\n- The corpus continues to pass with the tuned weights.","notes":"Spec: planning/collab/amendments.md Decision #15 + §Anchor resolver disagreement policy (line ~131) explicitly defers calibration to after Phase 1. Don't run this until the corpus is comprehensive; otherwise you'll overfit.","status":"closed","priority":3,"issue_type":"task","assignee":"James Lal","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:28:32Z","created_by":"James Lal","updated_at":"2026-05-19T04:07:08Z","closed_at":"2026-05-19T04:07:08Z","close_reason":"Implemented; merged into collab; 346 Rust + 168 relay tests pass","dependencies":[{"issue_id":"attn-nnj.3.7","depends_on_id":"attn-nnj.3","type":"parent-child","created_at":"2026-05-18T16:28:31Z","created_by":"Angus Bezzina","metadata":"{}"},{"issue_id":"attn-nnj.3.7","depends_on_id":"attn-nnj.3.6","type":"blocks","created_at":"2026-05-18T16:29:48Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"attn-nnj.9","title":"Phase 6: Browser + remote agents","description":"Browser review client at https://attn.dev/review/\u003croomId\u003e#key=... Memory-only secret persistence per decision #13 (URL fragment parsed once, stripped via history.replaceState, held only in JS heap). Remote agent participant type. CLI subcommands for local agents. INCLUDES the crypto sourcing decision: compile attn-collab-crypto Rust crate to WASM OR write a TS implementation against the shared test-vector corpus.","notes":"Decision #13: URL fragment parsed once, immediately stripped via history.replaceState, held only in JS heap. No sessionStorage/IndexedDB/cookies. Browser-crypto decision (WASM vs TS) is the gating discovery item here.","status":"closed","priority":3,"issue_type":"epic","owner":"james@littlebearlabs.io","created_at":"2026-05-18T22:09:17Z","created_by":"James Lal","updated_at":"2026-05-19T18:01:01Z","closed_at":"2026-05-19T18:01:01Z","close_reason":"Phase complete — all implementation issues closed","dependencies":[{"issue_id":"attn-nnj.9","depends_on_id":"attn-nnj","type":"parent-child","created_at":"2026-05-18T16:09:17Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"attn-6dp","title":"Editorial polish: reactions, @mentions, suggestion batching","description":"ITEM 3 (further-out polish): emoji reactions on comments; @mentions with autocomplete + notifications; suggestion batching (multi-select, accept-all/reject-all). None exist today. Lower priority than the toolbar/resolve/replies/inbox track.","status":"open","priority":4,"issue_type":"feature","owner":"james@littlebearlabs.io","created_at":"2026-05-23T04:45:35Z","created_by":"James Lal","updated_at":"2026-05-23T04:45:35Z","dependencies":[{"issue_id":"attn-6dp","depends_on_id":"attn-07i","type":"parent-child","created_at":"2026-05-22T22:47:18Z","created_by":"Angus Bezzina","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} +{"_type":"memory","key":"appstate-file-to-room-evolved-from-hashmap-pathbuf","value":"AppState.file_to_room evolved from HashMap\u003cPathBuf, RoomId\u003e (per amendments.md original) to HashMap\u003cPathBuf, (RoomId, FileId)\u003e (after 2.5). The FileId in the value was added because LocalRevision persistence needs the file's review identity, not just the room. Update amendments.md to reflect or revert when ReviewManager (2.8) is more fully wired."} +{"_type":"memory","key":"beads-dolt-db-can-be-wiped-by-concurrent","value":"Beads dolt DB can be wiped by concurrent agent operations when parallel subagents in git worktrees race on bd writes via auto-export hooks. Recovery: 'git show \u003clast-good-sha\u003e:.beads/issues.jsonl \u003e .beads/issues.jsonl' then 'bd import'. Mitigation: never have subagents call bd directly; serialize bd operations in the parent only."} {"_type":"memory","key":"comrak-emits-nodevalue-math-as-inline-only-never","value":"comrak emits NodeValue::Math as INLINE only, never block-level. Display math ($$...$$) gets absorbed into the parent paragraph's normalized text. Block-level AnchorBlockKind::Math is only reachable via fenced ```math info-string. See src/review/anchors/index.rs::dollar_display_math_is_absorbed_into_paragraph test. Affects anchor resolver (3.4) — display math doesn't get its own anchor block to remap."} -{"_type":"memory","key":"root-cause-candidate-for-cross-machine-collab-desync","value":"ROOT CAUSE candidate for cross-machine collab desync: the WebRTC stack is STUN-only, NO TURN server (confirmed: src/review/transport/webrtc.rs only sets DEFAULT_STUN_SERVER=stun.l.google.com; planning/collab/security-review.md 'WebRTC TURN credentials: none today (no TURN server stood up)'; relay-spec.md 'STUN only'). Across real networks, symmetric NAT means many peer-pairs CANNOT form a direct DataChannel without TURN, so the WebRTC mesh only PARTIALLY forms. send_collab (manager.rs:1186) skips the relay when it judges the mesh 'complete' from its own connection view -\u003e when a peer is actually only reachable via relay, edits/collab silently drop asymmetrically. Hits both co-typing (mesh path) and comments (if WS is downgraded to signaling-only while believed-up WebRTC is broken). Works on localhost (no NAT). User confirmed desync is across-machines, both edits AND comments. Fixes: (1) stand up TURN, (2) make skip-relay decision conservative / always relay-fallback with receiver-side dedup. Reproduce via Docker+netem symmetric-NAT (attn-orf)."} {"_type":"memory","key":"hard-constraint-user-no-turn-must-not-use","value":"HARD CONSTRAINT (user): NO TURN. Must not use a TURN server, ever. Implication: symmetric-NAT peer-pairs cannot form a direct WebRTC DataChannel, so the relay MUST be a reliable DATA fallback for un-meshable pairs (not purely signaling). Bulletproof no-TURN design: per-peer routing — send over the direct DataChannel when that specific pair is robustly connected; send a TARGETED relay copy (relay already supports target.deviceId routing via deliverableTo/env_by_target) only to peers WebRTC can't reach; receiver dedups by EventId/serverSeq so double-delivery is harmless. Maximizes WebRTC, minimizes relay cost, zero silent drops. Supersedes any TURN plan."} +{"_type":"memory","key":"root-cause-candidate-for-cross-machine-collab-desync","value":"ROOT CAUSE candidate for cross-machine collab desync: the WebRTC stack is STUN-only, NO TURN server (confirmed: src/review/transport/webrtc.rs only sets DEFAULT_STUN_SERVER=stun.l.google.com; planning/collab/security-review.md 'WebRTC TURN credentials: none today (no TURN server stood up)'; relay-spec.md 'STUN only'). Across real networks, symmetric NAT means many peer-pairs CANNOT form a direct DataChannel without TURN, so the WebRTC mesh only PARTIALLY forms. send_collab (manager.rs:1186) skips the relay when it judges the mesh 'complete' from its own connection view -\u003e when a peer is actually only reachable via relay, edits/collab silently drop asymmetrically. Hits both co-typing (mesh path) and comments (if WS is downgraded to signaling-only while believed-up WebRTC is broken). Works on localhost (no NAT). User confirmed desync is across-machines, both edits AND comments. Fixes: (1) stand up TURN, (2) make skip-relay decision conservative / always relay-fallback with receiver-side dedup. Reproduce via Docker+netem symmetric-NAT (attn-orf)."} {"_type":"memory","key":"headless-review-agent-deadlocked-on-collab-handle-line","value":"Headless review agent deadlocked on collab: handle_line matched on current_room.lock().clone() — a MutexGuard temporary in a match scrutinee lives for the whole match block, so the lock was held across manager.submit(), whose synchronous EventImported sink re-locks the same mutex -\u003e re-entrant std::Mutex deadlock. Fix (commit f60cde1): bind lock to a let before matching. KEY: the GUI daemon is NOT affected — its update_tx sink is proxy.send_event(UserEvent::Review) (non-blocking, never re-locks AppState). So the user's GUI 'changes don't appear on the other side' was NOT this deadlock; most likely attn-cqk (reviewer margin not rendering, fixed). General rule: never hold a lock across manager.submit() — the sink runs synchronously on the same thread."} +{"_type":"memory","key":"spec-deviation-flagged-by-5-5-room-creation","value":"Spec deviation flagged by 5.5: room creation body MUST include admissionKey (base64url 32 bytes) — not in published relay-spec.md but the only viable resolution to the chicken-and-egg admission HMAC. First POST cannot verify admission (no stored key yet); rejoin verifies normally. Update amendments.md when the relay endpoint chain is fully landed."} +{"_type":"memory","key":"collab-live-co-typing-convergence-works-headlessly-on","value":"Collab (live co-typing) convergence WORKS headlessly on localhost: collab-probe with 3 attn-agents shows reviewerB's SendCollab reaches owner+rvC (both emit a collab_signal update). The mailbox/WS relay path DOES surface relay-delivered collab as TransportEvent::CollabSignal (ws.rs:823,871) — so relay-fallback collab IS wired (earlier 'WS drops collab' hypothesis disproven). Comments also converge headlessly. So the daemon/transport layer converges for both on localhost; the cross-machine drop must be triggered by real-topology conditions (partial mesh / NAT), still to be reproduced in the Docker harness."} {"_type":"memory","key":"svelte-5-runes-state-derived-effect-outside-svelte","value":"Svelte 5 runes ($state, $derived, $effect) outside .svelte components require the .svelte.ts file extension — Vite resolves the bare import path without the .ts. See web/src/lib/hooks/is-mobile.svelte.ts and web/src/lib/review/store.svelte.ts."} {"_type":"memory","key":"verified-via-real-daemon-automation-api-scripts-test","value":"VERIFIED via real daemon automation API (scripts/test-editorial-e2e.sh, attn --eval/--click/--query): attn-0wa (owner stays local / reviewer enters shared-doc), attn-bit (selection toolbar appears + Comment opens composer — full UI path), attn-1rm (reply imported by owner + grouped into 1 threadId), attn-zhr (CommentResolved imported by owner). 10 passed / 0 failed / 1 pend. The pend is attn-cqk: the right-rail review margin overlay does not mount on panelOpen toggle under automation (reviewer can't see cards) — pre-existing, separate from these features. Also learned: build.rs early-returns when web/dist/index.html exists, so testing the daemon requires 'npm --prefix web run build' before 'cargo build' to embed frontend changes."} -{"_type":"memory","key":"appstate-file-to-room-evolved-from-hashmap-pathbuf","value":"AppState.file_to_room evolved from HashMap\u003cPathBuf, RoomId\u003e (per amendments.md original) to HashMap\u003cPathBuf, (RoomId, FileId)\u003e (after 2.5). The FileId in the value was added because LocalRevision persistence needs the file's review identity, not just the room. Update amendments.md to reflect or revert when ReviewManager (2.8) is more fully wired."} +{"_type":"memory","key":"attn-web-uses-npm-not-pnpm-web-package","value":"attn web/ uses npm (not pnpm) — web/package-lock.json is the lockfile; scripts/build.sh, Taskfile.yml, build.rs all call 'npm ci'. Using pnpm install drops transitive markdown-it and breaks the Vite build."} {"_type":"memory","key":"review-event-convergence-comments-suggestions-works-on-local","value":"review-event convergence (comments/suggestions) works on localhost in ALL THREE room modes (live/hybrid/async). Proven by tests/review_sync_convergence.rs: a 3-peer room (owner+2 reviewers) against the real Miniflare relay — reviewerB's comment reaches BOTH owner AND reviewerC every time. Review events are relay-mediated (outbox POST -\u003e relay broadcastFreshEnvelopes -\u003e all WS subscribers -\u003e InboundPipeline), independent of WebRTC; every peer (even in Live mode where selector mailbox=None) still opens the inbound WS subscription ('started room runtime outbox+ws subscribed'). So asymmetric 'changes on one side not the other' is NOT in the relay-mediated review-event path on localhost. Suspects narrowed to: (a) WebRTC-mesh/co-typing OT under imperfect topology (needs Docker/netem), or (b) frontend rendering/snapshot-republish churn (see attn-0wa)."} -{"_type":"memory","key":"spec-deviation-flagged-by-5-5-room-creation","value":"Spec deviation flagged by 5.5: room creation body MUST include admissionKey (base64url 32 bytes) — not in published relay-spec.md but the only viable resolution to the chicken-and-egg admission HMAC. First POST cannot verify admission (no stored key yet); rejoin verifies normally. Update amendments.md when the relay endpoint chain is fully landed."} {"_type":"memory","key":"webrtc-is-working-in-the-gui-daemon-2","value":"WebRTC IS working in the GUI daemon (2-party localhost): DataChannel connects in ~100ms and the Rust manager emits ConnectionChanged 'live_direct'. The connection BADGE showing 'Offline' is a FRONTEND bug, not a transport problem — store.svelte.ts applyConnection() only sets this.connection when currentRoomId===payload.roomId, and the owner stays on its local doc (attn-0wa) so the shared room's live_direct never reaches the badge. This is the likely root of the user's 'sync feels broken / webrtc isn't primary' perception: WebRTC is primary+connected, the UI just can't show it. test:webrtc:live reproduces (badge FAIL, data still converges)."} -{"_type":"memory","key":"attn-web-uses-npm-not-pnpm-web-package","value":"attn web/ uses npm (not pnpm) — web/package-lock.json is the lockfile; scripts/build.sh, Taskfile.yml, build.rs all call 'npm ci'. Using pnpm install drops transitive markdown-it and breaks the Vite build."} -{"_type":"memory","key":"beads-dolt-db-can-be-wiped-by-concurrent","value":"Beads dolt DB can be wiped by concurrent agent operations when parallel subagents in git worktrees race on bd writes via auto-export hooks. Recovery: 'git show \u003clast-good-sha\u003e:.beads/issues.jsonl \u003e .beads/issues.jsonl' then 'bd import'. Mitigation: never have subagents call bd directly; serialize bd operations in the parent only."} -{"_type":"memory","key":"collab-live-co-typing-convergence-works-headlessly-on","value":"Collab (live co-typing) convergence WORKS headlessly on localhost: collab-probe with 3 attn-agents shows reviewerB's SendCollab reaches owner+rvC (both emit a collab_signal update). The mailbox/WS relay path DOES surface relay-delivered collab as TransportEvent::CollabSignal (ws.rs:823,871) — so relay-fallback collab IS wired (earlier 'WS drops collab' hypothesis disproven). Comments also converge headlessly. So the daemon/transport layer converges for both on localhost; the cross-machine drop must be triggered by real-topology conditions (partial mesh / NAT), still to be reproduced in the Docker harness."} diff --git a/planning/complete-plan.md b/planning/complete-plan.md new file mode 100644 index 0000000..95a6651 --- /dev/null +++ b/planning/complete-plan.md @@ -0,0 +1,186 @@ +# Complete plan: In-app HTML file viewing + +## 1. Background & current state +attn (Rust wry/tao + Svelte 5) previews markdown/image/video/audio. HTML is +currently `Unsupported` and hidden. Goal: **showcase AI-generated HTML in-app** — +polished, self-contained pages that typically use inline JavaScript, custom web +fonts, and CDN-hosted animation libraries (GSAP, anime.js, Lottie) — while keeping +the user safe. + +Existing pieces this builds on: +- `attn://` custom protocol serves any local file (`src/main.rs:526-596`) and + already returns `text/html` for `.html/.htm` (`mime_from_extension`, `src/main.rs:1608`). +- Non-markdown viewers render via `markdownSourceUrl(path)` → + `attn://localhost/` (`web/src/lib/markdown-layer.ts:99`). +- File-type routing lives in `App.svelte`'s `mainContent()` snippet (~`web/src/App.svelte:2408`). +- `FileType` is mirrored in Rust (`src/files.rs`) and TS (`web/src/lib/types.ts:23`). +- Binary-size gate: 32 MiB (CLAUDE.md). This feature adds **no dependencies**. + +## 2. Goals / non-goals +**Goals:** display `.html/.htm` in-app (navigable from sidebar/search/tabs); +**run the page's JavaScript** and let it load remote fonts/animation libraries for +aesthetics; do so safely (page cannot write the disk, drive attn, or read other +local files); live-reload on disk change. + +**Non-goals (v1):** a strict offline-only mode for untrusted files (future +toggle), editing HTML, comments/suggestions/live-collab on HTML. + +## 3. Architecture +Render HTML in a sandboxed ` + diff --git a/web/src/lib/PathBreadcrumb.svelte b/web/src/lib/PathBreadcrumb.svelte index 56c1c5e..76ebedc 100644 --- a/web/src/lib/PathBreadcrumb.svelte +++ b/web/src/lib/PathBreadcrumb.svelte @@ -9,6 +9,7 @@ } from '$lib/components/ui/breadcrumb'; import { dragWindow } from './ipc'; import Share2 from '@lucide/svelte/icons/share-2'; + import ExternalLink from '@lucide/svelte/icons/external-link'; interface Props { path: string; @@ -16,6 +17,9 @@ onNavigate?: (path: string) => void; onShare?: () => void; shareEnabled?: boolean; + /** When set, shows an "open in browser" icon button in the header cluster + * (used for HTML files, which can't be shared but can be opened externally). */ + onOpenInBrowser?: () => void; avoidWindowControls?: boolean; fixed?: boolean; topOffsetPx?: number; @@ -28,6 +32,7 @@ onNavigate, onShare, shareEnabled = false, + onOpenInBrowser, avoidWindowControls = false, fixed = false, topOffsetPx = 0, @@ -110,10 +115,23 @@ {:else} {/if} + {#if onOpenInBrowser} + + {/if} {#if onShare}