@agent-native/core 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +1 -0
  2. package/dist/action.d.ts +8 -1
  3. package/dist/action.d.ts.map +1 -1
  4. package/dist/action.js +20 -10
  5. package/dist/action.js.map +1 -1
  6. package/dist/cli/app-skill.d.ts +3 -1
  7. package/dist/cli/app-skill.d.ts.map +1 -1
  8. package/dist/cli/app-skill.js +50 -8
  9. package/dist/cli/app-skill.js.map +1 -1
  10. package/dist/cli/connect.d.ts.map +1 -1
  11. package/dist/cli/connect.js +39 -5
  12. package/dist/cli/connect.js.map +1 -1
  13. package/dist/cli/create.d.ts.map +1 -1
  14. package/dist/cli/create.js +9 -7
  15. package/dist/cli/create.js.map +1 -1
  16. package/dist/cli/index.js +42 -10
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cli/mcp-config-writers.d.ts +10 -0
  19. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  20. package/dist/cli/mcp-config-writers.js +60 -6
  21. package/dist/cli/mcp-config-writers.js.map +1 -1
  22. package/dist/cli/mcp.d.ts.map +1 -1
  23. package/dist/cli/mcp.js +4 -6
  24. package/dist/cli/mcp.js.map +1 -1
  25. package/dist/cli/plan-local.d.ts.map +1 -1
  26. package/dist/cli/plan-local.js +15 -2
  27. package/dist/cli/plan-local.js.map +1 -1
  28. package/dist/cli/plan-publish-store.d.ts +17 -7
  29. package/dist/cli/plan-publish-store.d.ts.map +1 -1
  30. package/dist/cli/plan-publish-store.js +33 -8
  31. package/dist/cli/plan-publish-store.js.map +1 -1
  32. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  33. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  34. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  35. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  36. package/dist/cli/recap.d.ts +63 -5
  37. package/dist/cli/recap.d.ts.map +1 -1
  38. package/dist/cli/recap.js +641 -48
  39. package/dist/cli/recap.js.map +1 -1
  40. package/dist/cli/skills.d.ts +26 -11
  41. package/dist/cli/skills.d.ts.map +1 -1
  42. package/dist/cli/skills.js +644 -972
  43. package/dist/cli/skills.js.map +1 -1
  44. package/dist/cli/templates-meta.d.ts.map +1 -1
  45. package/dist/cli/templates-meta.js +3 -2
  46. package/dist/cli/templates-meta.js.map +1 -1
  47. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
  48. package/dist/client/blocks/library/AnnotatedCodeBlock.js +37 -9
  49. package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
  50. package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
  51. package/dist/client/blocks/library/DiffBlock.js +44 -12
  52. package/dist/client/blocks/library/DiffBlock.js.map +1 -1
  53. package/dist/client/blocks/library/annotation-rail.d.ts +12 -3
  54. package/dist/client/blocks/library/annotation-rail.d.ts.map +1 -1
  55. package/dist/client/blocks/library/annotation-rail.js +29 -3
  56. package/dist/client/blocks/library/annotation-rail.js.map +1 -1
  57. package/dist/client/blocks/library/html.d.ts.map +1 -1
  58. package/dist/client/blocks/library/html.js +3 -1
  59. package/dist/client/blocks/library/html.js.map +1 -1
  60. package/dist/client/blocks/library/question-form.d.ts.map +1 -1
  61. package/dist/client/blocks/library/question-form.js +4 -1
  62. package/dist/client/blocks/library/question-form.js.map +1 -1
  63. package/dist/client/components/LiveCursorOverlay.d.ts +46 -0
  64. package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -0
  65. package/dist/client/components/LiveCursorOverlay.js +137 -0
  66. package/dist/client/components/LiveCursorOverlay.js.map +1 -0
  67. package/dist/client/components/PresenceBar.d.ts +11 -1
  68. package/dist/client/components/PresenceBar.d.ts.map +1 -1
  69. package/dist/client/components/PresenceBar.js +39 -7
  70. package/dist/client/components/PresenceBar.js.map +1 -1
  71. package/dist/client/components/RemoteSelectionRings.d.ts +43 -0
  72. package/dist/client/components/RemoteSelectionRings.d.ts.map +1 -0
  73. package/dist/client/components/RemoteSelectionRings.js +116 -0
  74. package/dist/client/components/RemoteSelectionRings.js.map +1 -0
  75. package/dist/client/index.d.ts +4 -0
  76. package/dist/client/index.d.ts.map +1 -1
  77. package/dist/client/index.js +5 -0
  78. package/dist/client/index.js.map +1 -1
  79. package/dist/collab/awareness.d.ts +25 -0
  80. package/dist/collab/awareness.d.ts.map +1 -1
  81. package/dist/collab/awareness.js +42 -5
  82. package/dist/collab/awareness.js.map +1 -1
  83. package/dist/collab/client.d.ts +19 -1
  84. package/dist/collab/client.d.ts.map +1 -1
  85. package/dist/collab/client.js +362 -57
  86. package/dist/collab/client.js.map +1 -1
  87. package/dist/collab/follow-mode.d.ts +56 -0
  88. package/dist/collab/follow-mode.d.ts.map +1 -0
  89. package/dist/collab/follow-mode.js +54 -0
  90. package/dist/collab/follow-mode.js.map +1 -0
  91. package/dist/collab/index.d.ts +3 -1
  92. package/dist/collab/index.d.ts.map +1 -1
  93. package/dist/collab/index.js +5 -1
  94. package/dist/collab/index.js.map +1 -1
  95. package/dist/collab/presence.d.ts +56 -0
  96. package/dist/collab/presence.d.ts.map +1 -0
  97. package/dist/collab/presence.js +98 -0
  98. package/dist/collab/presence.js.map +1 -0
  99. package/dist/collab/routes.d.ts.map +1 -1
  100. package/dist/collab/routes.js +33 -6
  101. package/dist/collab/routes.js.map +1 -1
  102. package/dist/collab/struct-routes.d.ts.map +1 -1
  103. package/dist/collab/struct-routes.js +24 -4
  104. package/dist/collab/struct-routes.js.map +1 -1
  105. package/dist/collab/ydoc-manager.d.ts +13 -0
  106. package/dist/collab/ydoc-manager.d.ts.map +1 -1
  107. package/dist/collab/ydoc-manager.js +51 -15
  108. package/dist/collab/ydoc-manager.js.map +1 -1
  109. package/dist/db/migrations.d.ts.map +1 -1
  110. package/dist/db/migrations.js +2 -1
  111. package/dist/db/migrations.js.map +1 -1
  112. package/dist/extensions/routes.d.ts +18 -0
  113. package/dist/extensions/routes.d.ts.map +1 -1
  114. package/dist/extensions/routes.js +30 -8
  115. package/dist/extensions/routes.js.map +1 -1
  116. package/dist/oauth-tokens/store.d.ts.map +1 -1
  117. package/dist/oauth-tokens/store.js +42 -5
  118. package/dist/oauth-tokens/store.js.map +1 -1
  119. package/dist/scripts/db/index.d.ts.map +1 -1
  120. package/dist/scripts/db/index.js +1 -0
  121. package/dist/scripts/db/index.js.map +1 -1
  122. package/dist/scripts/db/migrate-encrypt-oauth-tokens.d.ts +28 -0
  123. package/dist/scripts/db/migrate-encrypt-oauth-tokens.d.ts.map +1 -0
  124. package/dist/scripts/db/migrate-encrypt-oauth-tokens.js +164 -0
  125. package/dist/scripts/db/migrate-encrypt-oauth-tokens.js.map +1 -0
  126. package/dist/scripts/db/scoping.d.ts.map +1 -1
  127. package/dist/scripts/db/scoping.js +7 -5
  128. package/dist/scripts/db/scoping.js.map +1 -1
  129. package/dist/secrets/index.d.ts +1 -0
  130. package/dist/secrets/index.d.ts.map +1 -1
  131. package/dist/secrets/index.js +4 -0
  132. package/dist/secrets/index.js.map +1 -1
  133. package/dist/server/collab-plugin.d.ts +6 -0
  134. package/dist/server/collab-plugin.d.ts.map +1 -1
  135. package/dist/server/collab-plugin.js +105 -5
  136. package/dist/server/collab-plugin.js.map +1 -1
  137. package/dist/server/poll-events.d.ts +5 -0
  138. package/dist/server/poll-events.d.ts.map +1 -1
  139. package/dist/server/poll-events.js +27 -4
  140. package/dist/server/poll-events.js.map +1 -1
  141. package/dist/sharing/actions/set-resource-visibility.d.ts.map +1 -1
  142. package/dist/sharing/actions/set-resource-visibility.js +4 -1
  143. package/dist/sharing/actions/set-resource-visibility.js.map +1 -1
  144. package/dist/templates/default/.agents/skills/real-time-collab/SKILL.md +185 -37
  145. package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +12 -2
  146. package/dist/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +185 -37
  147. package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +12 -2
  148. package/docs/content/plan-plugin.md +21 -6
  149. package/docs/content/pr-visual-recap.md +52 -3
  150. package/docs/content/real-time-collaboration.md +481 -97
  151. package/docs/content/skills-guide.md +13 -0
  152. package/docs/content/template-plan.md +18 -7
  153. package/package.json +5 -1
  154. package/src/templates/default/.agents/skills/real-time-collab/SKILL.md +185 -37
  155. package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +12 -2
  156. package/src/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +185 -37
  157. package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +12 -2
@@ -1,9 +1,10 @@
1
1
  ---
2
2
  name: real-time-collab
3
3
  description: >-
4
- Multi-user collaborative editing with Yjs CRDT and live cursors. Use when
5
- adding real-time collaborative editing to a template, debugging sync issues,
6
- or understanding how the agent and humans edit documents simultaneously.
4
+ Multi-user collaborative editing with Yjs CRDT, SSE fast-path transport, and
5
+ granular server-side merge. Use when adding real-time collaborative editing to
6
+ a template, debugging sync issues, or understanding how the agent and humans
7
+ edit documents simultaneously.
7
8
  metadata:
8
9
  internal: true
9
10
  ---
@@ -12,39 +13,57 @@ metadata:
12
13
 
13
14
  ## Rule
14
15
 
15
- Collaborative editing uses Yjs CRDT via TipTap. The agent and human users are equal participants — both edit the same Y.Doc and changes merge cleanly without conflicts.
16
+ Collaborative editing uses Yjs CRDT via TipTap. The agent and human users are
17
+ equal participants — both edit the same Y.Doc and changes merge cleanly without
18
+ conflicts. Always set `resourceType` on `createCollabPlugin`.
16
19
 
17
20
  ## How It Works
18
21
 
19
22
  - **`Y.Doc`** stores the document as a `Y.XmlFragment` (ProseMirror node tree)
20
- - **TipTap's Collaboration extension** binds the editor to the Y.XmlFragment via `ySyncPlugin`
21
- - **CollaborationCaret extension** renders remote users' cursors with names and colors
22
- - **Polling** (every 2s) syncs Y.Doc updates and awareness state between clients and server
23
- - **SQL `_collab_docs` table** persists Yjs state as base64-encoded binary (works across SQLite/Postgres)
23
+ - **TipTap's Collaboration extension** binds the editor to the Y.XmlFragment
24
+ via `ySyncPlugin`
25
+ - **CollaborationCaret extension** renders remote users' cursors with names and
26
+ colors
27
+ - **SSE fast-path** — `/_agent-native/poll-events` `EventSource` delivers collab
28
+ events push-style; while SSE is healthy the collab poll interval relaxes to
29
+ ~12 s
30
+ - **Polling fallback** — `/_agent-native/poll` is polled every 2 s when SSE is
31
+ unavailable; this is the universal serverless fallback
32
+ - **Update batching** — local Yjs updates are debounced ~80 ms and coalesced
33
+ with `Y.mergeUpdates` before sending; flushed immediately on
34
+ `visibilitychange` / `pagehide`
35
+ - **SQL `_collab_docs` table** persists Yjs state as base64 (SQLite/Postgres
36
+ compatible). Tombstone compaction fires automatically when the stored blob
37
+ exceeds 4× the fresh encoded size.
24
38
 
25
39
  ## Agent + Human Editing
26
40
 
27
41
  1. **Human edits** → TipTap → ySyncPlugin → Y.XmlFragment → `POST /_agent-native/collab/:docId/update`
28
42
  2. **Agent edits** → action edits canonical SQL content + bumps `updatedAt` → change-sync refetch → the open editor reconciles the new content into the live Y.Doc (see below) → poll update → all clients
29
43
 
30
- Both produce Yjs operations that merge cleanly. Agent edits appear without destroying cursor position, selection, or undo history.
44
+ Both produce Yjs operations that merge cleanly. Agent edits appear without
45
+ destroying cursor position, selection, or undo history.
31
46
 
32
- This is how content (documents) and slides now work. The agent does **not** push edits into Yjs in-process, and it does **not** call any `findCollabOrigin()` / localhost probe — that approach silently no-op'd on serverless (the action runs in a different process), so agent edits didn't show up live until the user navigated away and back. Nor does it search-and-replace inside existing Y.XmlText nodes, which could never create new block structure (lists, headings, tables). The peer-editor model below replaces both.
47
+ The agent does **not** push edits into Yjs in-process and does **not** call any
48
+ localhost probe — those approaches silently no-op on serverless (the action runs
49
+ in a different process). The peer-editor model below replaced them.
33
50
 
34
51
  ## Agent Edits As A Real-Time Peer Editor
35
52
 
36
- The agent edits documents the same way a human collaborator does: its change lands in the shared Y.Doc, propagates to every connected client, and persists. It gets there without any in-process Yjs push from the action.
53
+ **SQL is the durable source of truth for document body content.** The agent
54
+ action edits the canonical content column and bumps `updatedAt`. No localhost
55
+ calls, no in-process Yjs mutation.
37
56
 
38
- **SQL is the durable source of truth for document body content.** The agent action edits the canonical content (e.g. `documents.content`) and bumps `updatedAt`. That's the whole server side — no localhost calls, no Yjs mutation from the action.
39
-
40
- **The open editor reconciles authoritative external content into the live Y.Doc.** The action's `updatedAt` bump flows through the change-sync system (see `real-time-sync`), which refetches the record. The editor applies the new content through its real markdown/HTML pipeline via `setContent`, so new block structure (lists, headings, tables) renders correctly and merges with concurrent human edits through the Yjs CRDT diff. The result: the agent's edit propagates to every connected client and persists, exactly like a human collaborator's edit.
57
+ **The open editor reconciles authoritative external content into the live
58
+ Y.Doc.** The `updatedAt` bump flows through change-sync, which refetches the
59
+ record. The lead client applies the new content via `setContent`, producing Yjs
60
+ operations that merge with concurrent human edits. Every connected client
61
+ receives the result through normal Yjs sync.
41
62
 
42
63
  ### The `updatedAt` gate
43
64
 
44
- The editor only adopts content that is genuinely **newer** than what it already reflects. An older-or-equal `updatedAt` is a lagging poll or a stale snapshot and is **ignored**.
45
-
46
65
  ```ts
47
- // Pseudocode in the editor's reconcile effect
66
+ // In the editor's reconcile effect
48
67
  if (loaded.updatedAt > lastAppliedUpdatedAt.current) {
49
68
  applyAuthoritativeContent(loaded.content); // adopt
50
69
  lastAppliedUpdatedAt.current = loaded.updatedAt;
@@ -52,56 +71,107 @@ if (loaded.updatedAt > lastAppliedUpdatedAt.current) {
52
71
  // else: lagging poll / stale snapshot → ignore
53
72
  ```
54
73
 
55
- **Why:** without the gate, a slightly-behind poll response re-applies old content right after the agent's edit, so the edit "reverts on the next poll" / "doesn't show until refresh" — the whack-a-mole we kept hitting. A **fresh mount or doc-switch has no baseline**, so it always adopts whatever content it loaded — which is why a manual refresh is always correct.
74
+ Without the gate, a slightly-behind poll response re-applies old content and
75
+ the edit "reverts on next poll". A fresh mount always adopts whatever content
76
+ it loaded.
56
77
 
57
78
  ### Lead-client election
58
79
 
59
- Exactly ONE connected client applies an authoritative snapshot into the shared Y.Doc; the rest receive it through normal Yjs sync. The lead is the present client with the lowest Yjs `clientID`, decided by the core helper:
80
+ Exactly ONE connected client applies the authoritative snapshot; the rest
81
+ receive it through Yjs sync:
60
82
 
61
83
  ```ts
62
84
  import { isReconcileLeadClient } from "@agent-native/core/client";
63
85
 
64
86
  if (
65
87
  loaded.updatedAt > lastAppliedUpdatedAt.current &&
66
- isReconcileLeadClient(provider.awareness, ydoc.clientID)
88
+ isReconcileLeadClient(awareness, ydoc.clientID)
67
89
  ) {
68
90
  applyAuthoritativeContent(loaded.content);
69
91
  }
70
92
  ```
71
93
 
72
- **Why:** if every open editor independently diffed the same snapshot into the CRDT, each would insert the changed region at the same position, duplicating it N times (concurrent inserts → duplicated text). Electing one lead avoids that. The agent's awareness id (`AGENT_CLIENT_ID`, max int) can never win, and a client editing alone is always the lead. The election is deterministic across clients with no coordination round-trip.
94
+ The agent's awareness entry (`AGENT_CLIENT_ID`, max int) can never be the
95
+ lead. A sole client is always the lead. The election is deterministic with no
96
+ coordination round-trip.
73
97
 
74
98
  ### v1 limitation
75
99
 
76
- A full-content reconcile is **last-writer-wins for the rare case** where a human has unsaved edits in the exact region the agent simultaneously rewrites — the agent's snapshot can clobber that in-flight human edit. Inline and structural edits in **different** regions merge fine through the CRDT; only same-region simultaneous rewrites are at risk.
100
+ Full-content reconcile is **last-writer-wins** for the rare case where a human
101
+ has unsaved edits in the exact region the agent simultaneously rewrites. Edits
102
+ in **different** regions merge fine through the CRDT.
103
+
104
+ ## Security
105
+
106
+ ### Always set `resourceType`
107
+
108
+ ```ts
109
+ // server/plugins/collab.ts
110
+ import { createCollabPlugin } from "@agent-native/core/server";
111
+
112
+ export default createCollabPlugin({
113
+ table: "documents",
114
+ contentColumn: "content",
115
+ idColumn: "id",
116
+ resourceType: "document", // required
117
+ });
118
+ ```
119
+
120
+ Without `resourceType`, the server logs a one-time warning and collab push
121
+ events are delivered to **all authenticated users** without document-level
122
+ scoping. Set it to the resource type name registered via
123
+ `registerShareableResource`.
124
+
125
+ Non-owner sharees who have explicit access fall back to state-vector catch-up
126
+ (safe, slightly higher latency). Awareness routes require the same viewer
127
+ access as read routes.
128
+
129
+ ### Payload limits
130
+
131
+ Write routes reject payloads exceeding `maxPayloadBytes` (default 2 MB) with
132
+ HTTP 413. Override:
133
+
134
+ ```ts
135
+ createCollabPlugin({ resourceType: "document", maxPayloadBytes: 512 * 1024 });
136
+ ```
77
137
 
78
138
  ## Enabling Collaboration
79
139
 
80
140
  ### 1. Install packages
81
141
 
82
142
  ```bash
83
- pnpm add @tiptap/extension-collaboration @tiptap/extension-collaboration-caret @tiptap/y-tiptap
143
+ pnpm add @tiptap/extension-collaboration @tiptap/extension-collaboration-caret @tiptap/y-tiptap @tiptap/core
84
144
  ```
85
145
 
86
- ### 2. Add collab server plugin
146
+ ### 2. Add collab server plugin (with `resourceType`)
87
147
 
88
148
  ```ts
89
149
  // server/plugins/collab.ts
90
- import { createCollabPlugin } from "@agent-native/core/collab";
150
+ import { createCollabPlugin } from "@agent-native/core/server";
91
151
 
92
152
  export default createCollabPlugin({
93
153
  table: "documents",
94
154
  contentColumn: "content",
95
155
  idColumn: "id",
156
+ resourceType: "document",
96
157
  });
97
158
  ```
98
159
 
99
160
  ### 3. Use the client hook
100
161
 
101
162
  ```ts
102
- import { useCollaborativeDoc } from "@agent-native/core/client";
103
-
104
- const { ydoc, provider } = useCollaborativeDoc(documentId);
163
+ import { useCollaborativeDoc, emailToColor, emailToName } from "@agent-native/core/client";
164
+
165
+ const { ydoc, awareness, activeUsers, agentActive, agentPresent } =
166
+ useCollaborativeDoc({
167
+ docId: documentId,
168
+ requestSource: TAB_ID,
169
+ user: {
170
+ name: emailToName(session.email),
171
+ email: session.email,
172
+ color: emailToColor(session.email),
173
+ },
174
+ });
105
175
  ```
106
176
 
107
177
  ### 4. Add TipTap extensions
@@ -112,12 +182,14 @@ import { CollaborationCaret } from "@tiptap/extension-collaboration-caret";
112
182
 
113
183
  const editor = useEditor({
114
184
  extensions: [
185
+ StarterKit.configure({ history: false }), // Yjs handles undo
115
186
  Collaboration.configure({ document: ydoc }),
116
187
  CollaborationCaret.configure({
117
- provider,
188
+ provider: { awareness },
118
189
  user: { name: session.email, color: "#6366f1" },
119
190
  }),
120
191
  ],
192
+ // Do NOT pass content — Yjs owns it
121
193
  });
122
194
  ```
123
195
 
@@ -126,6 +198,9 @@ const editor = useEditor({
126
198
  ```ts
127
199
  optimizeDeps: {
128
200
  include: [
201
+ "yjs",
202
+ "y-protocols/awareness",
203
+ "@tiptap/core",
129
204
  "@tiptap/extension-collaboration",
130
205
  "@tiptap/extension-collaboration-caret",
131
206
  "@tiptap/y-tiptap",
@@ -137,22 +212,95 @@ optimizeDeps: {
137
212
 
138
213
  | Route | Purpose |
139
214
  | ----- | ------- |
140
- | `GET /_agent-native/collab/:docId/state` | Fetch full Y.Doc state |
215
+ | `GET /_agent-native/collab/:docId/state` | Fetch full Y.Doc state (accepts `?stateVector=` for diff) |
141
216
  | `POST /_agent-native/collab/:docId/update` | Apply client Yjs update |
142
217
  | `POST /_agent-native/collab/:docId/text` | Apply full text (diff-based) |
143
218
  | `POST /_agent-native/collab/:docId/search-replace` | Surgical find/replace in Y.XmlFragment |
219
+ | `POST /_agent-native/collab/:docId/json` | Apply full JSON diff to Y.Map/Y.Array |
220
+ | `GET /_agent-native/collab/:docId/json` | Read current JSON state |
221
+ | `POST /_agent-native/collab/:docId/patch` | Surgical JSON patch ops |
144
222
  | `POST /_agent-native/collab/:docId/awareness` | Sync cursor/presence state |
145
223
  | `GET /_agent-native/collab/:docId/users` | List active users |
146
224
 
225
+ ## Granular Server-Side Merge Pattern
226
+
227
+ For structured documents (slides, forms, design files) where body collab would
228
+ cause LWW conflicts at the container level, use **granular server-side merge**:
229
+ define an action with targeted per-item operations.
230
+
231
+ **When to use granular merge vs body collab:**
232
+
233
+ | Scenario | Recommended approach |
234
+ | -------- | -------------------- |
235
+ | Free-form rich text, cursor-level CRDT matters | Body collab (Y.XmlFragment + TipTap) |
236
+ | Structured items (slides, fields) where different users edit different items | Granular server-side merge (action with patch ops) |
237
+
238
+ Example operation shape for slides:
239
+
240
+ ```ts
241
+ type PatchDeckOp =
242
+ | { type: "patch"; slideId: string; fields: Partial<SlideFields> }
243
+ | { type: "add"; position: number; slide: SlideData }
244
+ | { type: "delete"; slideId: string }
245
+ | { type: "reorder"; slideId: string; newIndex: number };
246
+ ```
247
+
248
+ Concurrent edits to different slides both succeed at the action level; there
249
+ is no whole-deck LWW. Forms use the same shape with field-level ops.
250
+
251
+ ## Collaborative Undo Scoping (Y.UndoManager)
252
+
253
+ Scope undo/redo to the local user's own edits so peer and agent changes are
254
+ never accidentally reversed:
255
+
256
+ ```ts
257
+ import * as Y from "yjs";
258
+
259
+ const LOCAL_EDIT_ORIGIN = "local";
260
+
261
+ const undoManager = new Y.UndoManager(ydoc.getText("content"), {
262
+ trackedOrigins: new Set([LOCAL_EDIT_ORIGIN]),
263
+ captureTimeout: 800, // coalesces rapid slider drags into one undo step
264
+ });
265
+
266
+ // Mark local edits with the tracked origin
267
+ ydoc.transact(() => {
268
+ // apply local change
269
+ }, LOCAL_EDIT_ORIGIN);
270
+
271
+ undoManager.undo(); // only reverses LOCAL_EDIT_ORIGIN transactions
272
+ undoManager.redo();
273
+ ```
274
+
275
+ Rules:
276
+ - Pass a `Set` to `trackedOrigins` — not an array.
277
+ - Remote (`"remote"`) and agent (`"agent"`) origins are never captured.
278
+ - Recreate and destroy the manager when the active document changes.
279
+
147
280
  ## Common Pitfalls
148
281
 
149
- - **Don't pass `content` as a TipTap prop** when Collaboration is enabled Yjs owns the content. Set initial content via the Y.Doc instead.
150
- - **Don't call `editor.setContent()` ad hoc for agent edits.** The only sanctioned `setContent` is the editor's reconcile path described above — gated by `updatedAt` and guarded by `isReconcileLeadClient`. Calling it from elsewhere (e.g. on every poll, or from every client) re-applies stale content or duplicates the changed region across the CRDT.
151
- - **Add packages to `optimizeDeps`** — Vite won't pre-bundle Yjs packages correctly otherwise, causing runtime errors in dev.
152
- - **One `Y.Doc` per document** — Don't create multiple Y.Doc instances for the same document ID. Use the `useCollaborativeDoc` hook which caches by ID.
282
+ - **Missing `resourceType`**The server logs a warning on startup and
283
+ delivers collab events to all authenticated users without access scoping.
284
+ Always set `resourceType`.
285
+ - **Don't pass `content` as a TipTap prop** when Collaboration is enabled
286
+ Yjs owns the content. Seed via `editor.commands.setContent()` only when the
287
+ Y.XmlFragment is empty.
288
+ - **Don't call `editor.setContent()` ad hoc for agent edits** — the only
289
+ sanctioned `setContent` is gated by `updatedAt` and guarded by
290
+ `isReconcileLeadClient`. Calling it from elsewhere duplicates content across
291
+ the CRDT or re-applies stale snapshots.
292
+ - **Add packages to `optimizeDeps`** — Vite won't pre-bundle Yjs correctly
293
+ otherwise, causing runtime errors in dev.
294
+ - **One `Y.Doc` per document** — Don't create multiple Y.Doc instances for the
295
+ same document ID. `useCollaborativeDoc` caches by ID.
296
+ - **Destroy Y.UndoManager on doc change** — Stale managers hold Y.Doc
297
+ references and grow unboundedly. Recreate on `docId` change.
153
298
 
154
299
  ## Related Skills
155
300
 
156
- - `real-time-sync` — The change-sync system that delivers the `updatedAt` bump driving editor reconciliation; also `useReconciledState` for non-collaborative "copy a server value into local edit state" surfaces
157
- - `storing-data` The `_collab_docs` table where Yjs state is persisted; SQL holds the canonical document body that the editor reconciles from
158
- - `self-modifying-code` — Agent edits to collaborative documents edit canonical SQL content, not raw Yjs
301
+ - `real-time-sync` — The change-sync system that delivers the `updatedAt` bump
302
+ driving editor reconciliation; also `useReconciledState` for non-Yjs surfaces
303
+ - `storing-data` — The `_collab_docs` table and SQL canonical content
304
+ - `security` — `registerShareableResource`, `resolveAccess`, `assertAccess`
305
+ - `self-modifying-code` — Agent edits to collaborative documents edit canonical
306
+ SQL content, not raw Yjs
@@ -49,7 +49,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
49
49
 
50
50
  For list/sidebar queries, use the same pattern — pass the counter into the queryKey of every list query you want to keep fresh.
51
51
 
52
- 4. **Fallback** polling calls `/_agent-native/poll?since=N`. It runs every 2 seconds until SSE is connected, then relaxes to 15 seconds. If SSE is disabled or unavailable, polling continues at the normal cadence.
52
+ 4. **Fallback** polling calls `/_agent-native/poll?since=N`. It runs every 2 seconds until SSE is connected, then relaxes to 15 seconds (`SSE_FALLBACK_INTERVAL_MS`). If SSE is disabled or unavailable (e.g., edge/serverless deployments), polling continues at the 2 s cadence. Polling is the universal serverless fallback — it detects DB timestamp changes even when the write happened in a different process or invocation.
53
53
 
54
54
  5. When the agent writes to the database, the version increments, SSE/polling detects it, and React Query refetches the affected queries.
55
55
 
@@ -196,6 +196,16 @@ const [title, setTitle] = useReconciledState(props.title, { active: isEditing })
196
196
  | Local edit state copied from a server value (inputs, popovers, inline editors) | `useReconciledState(externalValue, { active })` |
197
197
  | Collaborative rich-text editor (Yjs) | `updatedAt`-gated reconcile + `isReconcileLeadClient` — see `real-time-collab` |
198
198
 
199
+ ## Granular server-side merge for non-body fields
200
+
201
+ For structured documents (slide decks, form builders, design files) where the
202
+ Yjs body collab would cause LWW conflicts at the container level, pair the
203
+ change-sync `updatedAt` bump with a **granular server-side merge action** that
204
+ accepts targeted per-item operations (add/patch/delete/reorder). Concurrent
205
+ edits to different items both survive at the action level; the `collab` source
206
+ version bump then propagates the merged state to all open clients. See
207
+ `real-time-collab` for the pattern and examples.
208
+
199
209
  ## Related Skills
200
210
 
201
211
  - **storing-data** — Application-state and settings are data stores that sync through change events
@@ -203,4 +213,4 @@ const [title, setTitle] = useReconciledState(props.title, { active: isEditing })
203
213
  - **actions** — Mutating actions trigger change events
204
214
  - **client-methods** — Route details belong in helpers/hooks, not components
205
215
  - **self-modifying-code** — Agent code edits trigger change events; rapid edits can cause event storms
206
- - **real-time-collab** — Collaborative editors reconcile agent edits into a shared Y.Doc, driven by the same change-sync `updatedAt` bump
216
+ - **real-time-collab** — Collaborative editors reconcile agent edits into a shared Y.Doc, driven by the same change-sync `updatedAt` bump; also the granular server-side merge pattern for structured data
@@ -12,7 +12,7 @@ The Agent-Native **Plan** app ships as one installable bundle. A single install
12
12
  One install gives you:
13
13
 
14
14
  - **Two skills** — `/visual-plan` (the canonical entry point) and `/visual-recap`.
15
- - **The Plan MCP connector** — registered against the hosted app at `https://plan.agent-native.com` (MCP endpoint `https://plan.agent-native.com/_agent-native/mcp`, server name `agent-native-plans`).
15
+ - **The Plan MCP connector** — registered against the hosted app at `https://plan.agent-native.com` (MCP endpoint `https://plan.agent-native.com/_agent-native/mcp`, server name `plan`, with legacy alias `agent-native-plans` during migration).
16
16
 
17
17
  By default, both skills publish to the hosted Plan app — they create a plan via
18
18
  the MCP connector and hand you a link or inline plan to review. They never dump
@@ -49,14 +49,27 @@ npx @agent-native/core@latest skills add visual-plan
49
49
  agent-native skills add visual-plan
50
50
  ```
51
51
 
52
- This installs `visual-plan` plus the companion `visual-recap` skill, then registers the `agent-native-plans` connector and runs auth (OAuth prompt for hosted/account-backed sharing). Useful flags:
52
+ This installs `visual-plan` plus the companion `visual-recap` skill, then registers the `plan` connector and its legacy `agent-native-plans` alias, then runs auth (OAuth prompt for hosted/account-backed sharing). Useful flags:
53
53
 
54
54
  - `--client codex|claude-code|claude-code-cli|cowork|all` — which local agents to write the MCP config for (default `codex`).
55
55
  - `--no-connect` — register the connector without authenticating; run `agent-native connect https://plan.agent-native.com` later.
56
56
  - `--mcp-url <url>` — point the connector at a custom origin (an ngrok tunnel, a local dev server, or a self-hosted deployment) instead of the hosted default.
57
57
  - `--with-github-action` — also write the PR Visual Recap GitHub Action (see [PR Visual Recap](/docs/pr-visual-recap)).
58
58
 
59
- After it finishes, restart or reload the agent client so the new skills and tools load, then run `/visual-plan`.
59
+ Interactive installs also offer the PR Visual Recap Action when no workflow is
60
+ present. Say yes to add it during skill setup, or run the command above later
61
+ with `--with-github-action`. After the workflow is written, run:
62
+
63
+ ```bash
64
+ agent-native recap setup
65
+ agent-native recap doctor
66
+ ```
67
+
68
+ `recap setup` configures the GitHub Action secrets and variables where possible,
69
+ and `recap doctor` verifies the workflow, local publish token, GitHub repo
70
+ access, and required Actions configuration. After install finishes, restart or
71
+ reload the agent client so the new skills and tools load, then run
72
+ `/visual-plan`.
60
73
 
61
74
  > Note: the bare `npx skills add BuilderIO/agent-native --skill visual-plan` (Vercel/open Skills CLI) installs **instructions only** — it does not register the MCP connector. Use the Agent-Native CLI above when you want the connector wired up too.
62
75
 
@@ -82,10 +95,12 @@ The same repo is a Codex plugin marketplace. Add it, install the plugin, then au
82
95
  ```bash
83
96
  codex plugin marketplace add BuilderIO/agent-native
84
97
  codex plugin add agent-native-visual-plans@agent-native-apps
85
- codex mcp login agent-native-plans # OAuth in the browser
98
+ codex mcp login plan # OAuth in the browser
99
+ # Existing installs may already be authenticated as:
100
+ codex mcp login agent-native-plans
86
101
  ```
87
102
 
88
- After install, **start a new Codex thread** so the skills and MCP tools load into the session. The plugin ships a URL-only connector (`[mcp_servers.agent-native-plans]` → `https://plan.agent-native.com/_agent-native/mcp`); `codex mcp login` runs the OAuth flow. The universal CLI route above also works for Codex (`agent-native skills add visual-plan --client codex`) if you prefer one command that installs and authenticates together.
103
+ After install, **start a new Codex thread** so the skills and MCP tools load into the session. The plugin ships URL-only connectors (`[mcp_servers.plan]` and legacy `[mcp_servers.agent-native-plans]` → `https://plan.agent-native.com/_agent-native/mcp`); `codex mcp login` runs the OAuth flow. The universal CLI route above also works for Codex (`agent-native skills add visual-plan --client codex`) if you prefer one command that installs and authenticates together.
89
104
 
90
105
  ## Updates {#updates}
91
106
 
@@ -93,7 +108,7 @@ The plugin routes auto-update — you do not re-pack or re-add the marketplace f
93
108
 
94
109
  - **Claude Code** — the marketplace entry sets `autoUpdate: true` and the plugin uses commit-SHA versioning, so Claude Code pulls new versions from the repo at startup; run `/reload-plugins` to activate. Every push to the repo's default branch reaches installed users automatically.
95
110
  - **Codex** — the plugin `version` embeds a content hash of the bundled skills and MCP endpoint (e.g. `1.0.0+codex.<hash>`), so any skill or endpoint change yields a new version. Codex's startup auto-upgrade re-installs configured git marketplaces on its own; just **start a new thread** to pick up the change. No manual `codex plugin marketplace upgrade` is needed for routine updates.
96
- - **Universal CLI route** — re-run `npx @agent-native/core@latest skills add visual-plan` to refresh the skills and re-register the connector. `@latest` always pulls the current skills from the published `@agent-native/core` package.
111
+ - **Universal CLI route** — run `npx @agent-native/core@latest skills status visual-plan` to check copied skill folders, or `npx @agent-native/core@latest skills update visual-plan` to refresh them in place. Re-running `skills add visual-plan` still works when you also want to re-register/authenticate the connector. `@latest` always pulls the current skills from the published `@agent-native/core` package.
97
112
 
98
113
  The connector points at a **hosted** app, so the Plan app's actions and live tool surface always reflect the deployed version regardless of when you installed; only the bundled skill instructions follow the update mechanisms above.
99
114
 
@@ -28,13 +28,34 @@ A re-push updates the same plan and the same sticky comment in place — no orph
28
28
 
29
29
  ## Installing it
30
30
 
31
- The Agent-Native CLI writes the workflow into your repository and prints the secrets to set:
31
+ When you install Plans interactively, the Agent-Native CLI asks whether to add
32
+ automatic PR Visual Recaps. Say yes to write the GitHub Action, or add it
33
+ explicitly at any time:
32
34
 
33
35
  ```bash
34
36
  agent-native skills add visual-plan --with-github-action
35
37
  ```
36
38
 
37
- This installs the `visual-plan` skill (which includes the `visual-recap` skill the action runs) and writes `.github/workflows/pr-visual-recap.yml` into your repo. The workflow calls **published CLI subcommands** — `agent-native recap scan|build-prompt|shot|comment` — so nothing is copied into your repo as helper scripts. Commit the generated workflow file, set the secrets below, and open a PR to see it run.
39
+ This installs the `visual-plan` skill (which includes the `visual-recap` skill the action runs) and writes `.github/workflows/pr-visual-recap.yml` into your repo. The workflow calls **published CLI subcommands** — `agent-native recap scan|build-prompt|shot|comment` — so nothing is copied into your repo as helper scripts.
40
+
41
+ Then run the guided setup helper:
42
+
43
+ ```bash
44
+ agent-native recap setup
45
+ agent-native recap doctor
46
+ ```
47
+
48
+ `recap setup` refreshes the workflow, uses `gh` to set GitHub Actions
49
+ secrets/variables when values are available from env or the local Plans
50
+ publish-token store, and prints exact missing commands for anything it cannot
51
+ set. Secret values are sent to `gh` through stdin, not command arguments. Commit
52
+ the generated workflow file and open a PR to see it run.
53
+
54
+ By default, the workflow builds its agent prompt from the latest bundled
55
+ `visual-recap` guidance in `@agent-native/core@latest`, including any sibling
56
+ reference files the skill ships with. If your repo intentionally customizes and
57
+ pins its committed `visual-recap` folder, set the repository variable
58
+ `VISUAL_RECAP_SKILL_SOURCE=repo`.
38
59
 
39
60
  ## Backend selection
40
61
 
@@ -53,6 +74,7 @@ Beyond the backend, two repository variables tune _how_ the agent runs:
53
74
 
54
75
  - **`VISUAL_RECAP_MODEL`** pins the model passed to the CLI (`--model`) — for example `gpt-5.5` for Codex, or a Claude model id. Leave it unset to use the CLI's own default model.
55
76
  - **`VISUAL_RECAP_REASONING`** sets the reasoning depth: `none`, `minimal`, `low`, `medium`, `high`, or `xhigh`. It applies to the Codex backend; Claude's reasoning is model-driven, so this variable is ignored there.
77
+ - **`VISUAL_RECAP_SKILL_SOURCE`** controls prompt freshness: `auto`/unset uses the latest bundled skill guidance, while `repo` pins to the committed repo-local `visual-recap` skill folder.
56
78
 
57
79
  For example, to run the recap on Codex with GPT-5.5 at high reasoning, set the repository variables `VISUAL_RECAP_AGENT=codex`, `VISUAL_RECAP_MODEL=gpt-5.5`, and `VISUAL_RECAP_REASONING=high`.
58
80
 
@@ -67,7 +89,18 @@ Set these in your repository's **Settings → Secrets and variables → Actions*
67
89
  | `PLAN_RECAP_TOKEN` | Per-user, revocable token minted by `agent-native connect`. Authorizes publishing the recap plan and the screenshot upload. |
68
90
  | `ANTHROPIC_API_KEY` | The LLM key for the default Claude Code backend. |
69
91
 
70
- Mint `PLAN_RECAP_TOKEN` with `agent-native connect` against your Plans app, then paste the printed token into the secret. Use a placeholder like `plan_recap_xxxxxxxxxxxxxxxx` only for examples — never commit a real token.
92
+ Mint `PLAN_RECAP_TOKEN` with `agent-native connect` against your Plans app. For
93
+ the hosted app, this also writes a local publish-token file that
94
+ `agent-native recap setup` can read:
95
+
96
+ ```bash
97
+ agent-native connect https://plan.agent-native.com --client codex
98
+ agent-native recap setup
99
+ ```
100
+
101
+ If you prefer manual setup, paste the token into the GitHub secret. Use a
102
+ placeholder like `plan_recap_xxxxxxxxxxxxxxxx` only for examples — never commit a
103
+ real token.
71
104
 
72
105
  ### Optional (only if you change defaults)
73
106
 
@@ -91,6 +124,22 @@ The workflow uses the plain `pull_request` trigger, **not** `pull_request_target
91
124
 
92
125
  This also means you can merge the workflow file **before** the secrets exist: with no token configured, every run is a quiet no-op until you set the secrets.
93
126
 
127
+ ## Self-modifying guard (sensitive paths)
128
+
129
+ The workflow's gate job skips the recap entirely if a PR touches any of the following paths, so a PR can never rewrite what the trusted recap job runs and exfiltrate secrets:
130
+
131
+ | Path pattern | Reason |
132
+ | ------------------------------------------ | --------------------------------------------------------- |
133
+ | `.github/workflows/pr-visual-recap.yml` | The workflow itself |
134
+ | `**/skills/visual-(recap\|plan\|plans)/**` | The visual-recap skill the agent follows |
135
+ | `**/.claude/**` | Agent settings the runner loads |
136
+ | `**/CLAUDE.md` | Agent instructions the runner loads |
137
+ | `**/AGENTS.md` | Agent instructions the runner loads |
138
+ | `**/.mcp.json` | MCP server config the runner loads |
139
+ | `packages/core/**` | Recap CLI source _(BuilderIO/agent-native monorepo only)_ |
140
+
141
+ The `packages/core/**` rule applies only in the `BuilderIO/agent-native` monorepo where `packages/core` is the recap CLI source. In consumer repos an unrelated `packages/core/` directory does not trigger the guard.
142
+
94
143
  ## Local-files privacy mode
95
144
 
96
145
  The GitHub Action is designed for hosted, shareable PR review. If you want a