pmx-canvas 0.10.0 → 0.12.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 (343) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +3 -1
  2. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  3. package/.github/extensions/pmx-canvas/tools.json +688 -128
  4. package/CHANGELOG.md +69 -0
  5. package/dist/amp-plugin/pmx-canvas/LICENSE +21 -0
  6. package/dist/amp-plugin/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
  7. package/dist/amp-plugin/pmx-canvas/index.js +5445 -0
  8. package/dist/amp-plugin/pmx-canvas/runtime.json +3 -0
  9. package/dist/amp-plugin/pmx-canvas/shared-runtime.mjs +194 -0
  10. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/SKILL.md +251 -0
  11. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/evals.json +1086 -0
  12. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/jwt.ts +17 -0
  13. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/login.ts +12 -0
  14. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/middleware.ts +13 -0
  15. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/routes/auth.ts +13 -0
  16. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/investigation-board/src/handlers/users.ts +27 -0
  17. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/mcp.json +36 -0
  18. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/ax-html-control-surface.md +101 -0
  19. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/codex-app-adapter.md +151 -0
  20. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +150 -0
  21. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/full-reference.md +1785 -0
  22. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/github-copilot-app-adapter.md +223 -0
  23. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/html-primitives.md +139 -0
  24. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/importing-context.md +132 -0
  25. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/installing-pmx-canvas.md +251 -0
  26. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/tours-and-recording.md +111 -0
  27. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/workbench-workflows.md +900 -0
  28. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas-orchestration/SKILL.md +141 -0
  29. package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas-testing/SKILL.md +266 -0
  30. package/dist/amp-plugin/pmx-canvas/start-mcp.mjs +14857 -0
  31. package/dist/amp-plugin/pmx-canvas/tools.json +10963 -0
  32. package/dist/canvas/chunks/abnfDiagram-N423BO3Z-cwtsds5d.js +1 -0
  33. package/dist/canvas/chunks/architecture-TIHT7OUA-x63cwejs.js +1 -0
  34. package/dist/canvas/chunks/architectureDiagram-T3A2C74G-b4xc9tpz.js +36 -0
  35. package/dist/canvas/chunks/blockDiagram-VBNYF7ZC-ahp3yxxs.js +132 -0
  36. package/dist/canvas/chunks/c4Diagram-5PPSVZJV-nappnay7.js +10 -0
  37. package/dist/canvas/chunks/classDiagram-JCYQIIEL-k1j2xc1t.js +1 -0
  38. package/dist/canvas/chunks/classDiagram-v2-OCEON4UE-33x08xf2.js +1 -0
  39. package/dist/canvas/chunks/cose-bilkent-JH36ORCC-a428syda.js +1 -0
  40. package/dist/canvas/chunks/cynefin-VYW2F7L2-8v6jxfz8.js +1 -0
  41. package/dist/canvas/chunks/cynefinDiagram-MW4NZA55-nspsqebj.js +62 -0
  42. package/dist/canvas/chunks/dagre-VZM6K2ZE-8y6177fh.js +4 -0
  43. package/dist/canvas/chunks/diagram-7IWD3JNH-vym028wn.js +30 -0
  44. package/dist/canvas/chunks/diagram-B4RE2ZJO-jddqb8sy.js +3 -0
  45. package/dist/canvas/chunks/diagram-LBJQPF4R-bjwbpkrn.js +24 -0
  46. package/dist/canvas/chunks/diagram-Q27KOJAE-j6ph6hcx.js +24 -0
  47. package/dist/canvas/chunks/diagram-UB23O5K3-p09w8jej.js +41 -0
  48. package/dist/canvas/chunks/ebnfDiagram-BXEA7PRR-j975ycq5.js +1 -0
  49. package/dist/canvas/chunks/erDiagram-JOGREHBK-sxp6hjn8.js +85 -0
  50. package/dist/canvas/chunks/eventmodeling-45OFAUF4-xzfgn6xg.js +1 -0
  51. package/dist/canvas/chunks/flowDiagram-UKHOOZJN-2nsmkskb.js +1 -0
  52. package/dist/canvas/chunks/ganttDiagram-PKOTCBZU-4tv230y2.js +292 -0
  53. package/dist/canvas/chunks/gitGraph-TEB2WS4Q-macbhand.js +1 -0
  54. package/dist/canvas/chunks/gitGraphDiagram-DS77QQ5N-mqgmnw9g.js +106 -0
  55. package/dist/canvas/chunks/index-037z5dz2.js +232 -0
  56. package/dist/canvas/chunks/index-081n3har.js +207 -0
  57. package/dist/canvas/chunks/index-5n03dmb9.js +2 -0
  58. package/dist/canvas/chunks/index-5tv8v1hk.js +1 -0
  59. package/dist/canvas/chunks/index-60pgzdqe.js +157 -0
  60. package/dist/canvas/chunks/index-6ceya556.js +332 -0
  61. package/dist/canvas/chunks/index-8g1vb57n.js +2 -0
  62. package/dist/canvas/chunks/index-8rjes9r6.js +2 -0
  63. package/dist/canvas/chunks/index-9022d1n9.js +16 -0
  64. package/dist/canvas/chunks/index-9b02042t.js +2 -0
  65. package/dist/canvas/chunks/index-ap4q4vez.js +2 -0
  66. package/dist/canvas/chunks/index-dyp2nhd3.js +2 -0
  67. package/dist/canvas/chunks/index-frvta4tm.js +2 -0
  68. package/dist/canvas/chunks/index-gf4e0et4.js +2 -0
  69. package/dist/canvas/chunks/index-h9ycj8n2.js +2 -0
  70. package/dist/canvas/chunks/index-hj99nz1f.js +1 -0
  71. package/dist/canvas/chunks/index-mergmj97.js +2 -0
  72. package/dist/canvas/chunks/index-nwsya6dj.js +301 -0
  73. package/dist/canvas/chunks/index-nz8qbd1p.js +2 -0
  74. package/dist/canvas/chunks/index-qhfrazv3.js +2 -0
  75. package/dist/canvas/chunks/index-v119nznk.js +4 -0
  76. package/dist/canvas/chunks/index-vpt37m3d.js +89 -0
  77. package/dist/canvas/chunks/index-y9ma6azy.js +2 -0
  78. package/dist/canvas/chunks/index-zx0j3a2z.js +179 -0
  79. package/dist/canvas/chunks/info-DKCQHKI2-a0ykzs9z.js +1 -0
  80. package/dist/canvas/chunks/infoDiagram-6WML65LV-qv97qq6a.js +2 -0
  81. package/dist/canvas/chunks/ishikawaDiagram-WSZJBQD7-p9306gh0.js +70 -0
  82. package/dist/canvas/chunks/journeyDiagram-NVQOT4AX-sv67zats.js +139 -0
  83. package/dist/canvas/chunks/kanban-definition-27J2QSJJ-k2pfam6p.js +89 -0
  84. package/dist/canvas/chunks/katex-dmg8k4zm.js +257 -0
  85. package/dist/canvas/chunks/mermaid.core-83r0fdce.js +12 -0
  86. package/dist/canvas/chunks/mindmap-definition-FAOFIHXS-vg2newm1.js +96 -0
  87. package/dist/canvas/chunks/packet-7NZHBO7P-tqnabcdv.js +1 -0
  88. package/dist/canvas/chunks/pegDiagram-VL7TDLO6-k2h7zc5n.js +1 -0
  89. package/dist/canvas/chunks/pie-RZYD4A2V-wke6gyhe.js +1 -0
  90. package/dist/canvas/chunks/pieDiagram-7S7Q4E2Y-hg169p1v.js +39 -0
  91. package/dist/canvas/chunks/quadrantDiagram-CIZ2JOQS-bxtvsq0b.js +7 -0
  92. package/dist/canvas/chunks/radar-I7S5WNFK-w7ka0drw.js +1 -0
  93. package/dist/canvas/chunks/railroad-3IZDKUUU-y3vg318x.js +1 -0
  94. package/dist/canvas/chunks/railroad-abnf-AHOZXSZD-6hqvcz3c.js +1 -0
  95. package/dist/canvas/chunks/railroad-ebnf-EBAXGLYW-vcae260m.js +1 -0
  96. package/dist/canvas/chunks/railroad-peg-LSFZ7HO6-0dbyme23.js +1 -0
  97. package/dist/canvas/chunks/railroadDiagram-AXF67PYL-njyhcp4p.js +1 -0
  98. package/dist/canvas/chunks/requirementDiagram-LRYGKXZP-2z6v09gm.js +84 -0
  99. package/dist/canvas/chunks/sankeyDiagram-W5VNT64P-15mzam0e.js +40 -0
  100. package/dist/canvas/chunks/sequenceDiagram-SI44F4Z6-rbshyd2c.js +162 -0
  101. package/dist/canvas/chunks/sizeCapture-X5ZJPWSS-sg26xwts.js +1 -0
  102. package/dist/canvas/chunks/stateDiagram-OKZ733FA-6eahp98e.js +1 -0
  103. package/dist/canvas/chunks/stateDiagram-v2-UEYNNEHI-tty0ggn5.js +1 -0
  104. package/dist/canvas/chunks/swimlanes-SLNWSIFB-ze5db10t.js +1 -0
  105. package/dist/canvas/chunks/swimlanesDiagram-ULZ7WXOC-thze75gq.js +8 -0
  106. package/dist/canvas/chunks/timeline-definition-Z64GVDOM-vnp7jyy8.js +120 -0
  107. package/dist/canvas/chunks/treeView-QDETBFTQ-6ysffnm6.js +1 -0
  108. package/dist/canvas/chunks/treemap-6X3UGDF4-m3h9p066.js +1 -0
  109. package/dist/canvas/chunks/vennDiagram-T6HMQDX7-0qrnb7mj.js +34 -0
  110. package/dist/canvas/chunks/wardley-OPB4EBWU-dp37drwx.js +1 -0
  111. package/dist/canvas/chunks/wardleyDiagram-T6FBY63Y-8nxg8wqt.js +78 -0
  112. package/dist/canvas/chunks/xychartDiagram-ELKLHX3M-nj921e7s.js +7 -0
  113. package/dist/canvas/global.css +483 -230
  114. package/dist/canvas/index.js +158 -219
  115. package/dist/canvas/mcp-app.html +4301 -548
  116. package/dist/canvas/mermaid-entry.js +338 -338
  117. package/dist/canvas/surface-theme.css +6 -6
  118. package/dist/json-render/index.css +1 -1
  119. package/dist/json-render/index.js +49 -49
  120. package/dist/types/client/canvas/BarHint.d.ts +5 -3
  121. package/dist/types/client/canvas/BoardMoveDialog.d.ts +2 -0
  122. package/dist/types/client/canvas/CanvasNode.d.ts +0 -11
  123. package/dist/types/client/canvas/EdgeEditor.d.ts +19 -0
  124. package/dist/types/client/canvas/EdgeLayer.d.ts +1 -2
  125. package/dist/types/client/canvas/GroupNavigation.d.ts +2 -0
  126. package/dist/types/client/canvas/TextPrompt.d.ts +4 -0
  127. package/dist/types/client/canvas/auto-fit.d.ts +4 -5
  128. package/dist/types/client/mermaid-entry.d.ts +1 -2
  129. package/dist/types/client/mermaid-render.d.ts +1 -0
  130. package/dist/types/client/nodes/BoardNode.d.ts +1 -1
  131. package/dist/types/client/nodes/ExtAppFrame.d.ts +7 -90
  132. package/dist/types/client/nodes/McpAppNode.d.ts +1 -25
  133. package/dist/types/client/nodes/MermaidNode.d.ts +1 -8
  134. package/dist/types/client/nodes/PromptNode.d.ts +1 -0
  135. package/dist/types/client/nodes/ViewerFrame.d.ts +29 -0
  136. package/dist/types/client/nodes/ViewerHost.d.ts +6 -0
  137. package/dist/types/client/nodes/WorkCard.d.ts +7 -0
  138. package/dist/types/client/nodes/iframe-document-url.d.ts +3 -9
  139. package/dist/types/client/nodes/primitive-copy.d.ts +5 -0
  140. package/dist/types/client/nodes/use-ax-surface-bridge.d.ts +1 -0
  141. package/dist/types/client/nodes/use-iframe-content-height.d.ts +1 -1
  142. package/dist/types/client/nodes/use-surface-frame.d.ts +4 -6
  143. package/dist/types/client/nodes/viewer-runtime-state.d.ts +2 -1
  144. package/dist/types/client/state/canvas-store.d.ts +18 -1
  145. package/dist/types/client/state/human-store.d.ts +2 -0
  146. package/dist/types/client/state/intent-bridge.d.ts +5 -2
  147. package/dist/types/client/state/presentation.d.ts +17 -6
  148. package/dist/types/client/state/relation-store.d.ts +11 -0
  149. package/dist/types/client/types.d.ts +1 -1
  150. package/dist/types/json-render/catalog.d.ts +2 -2
  151. package/dist/types/json-render/server.d.ts +2 -1
  152. package/dist/types/server/agent-presence.d.ts +1 -0
  153. package/dist/types/server/ax-interaction.d.ts +1 -0
  154. package/dist/types/server/ax-state-manager.d.ts +10 -2
  155. package/dist/types/server/board-local.d.ts +4 -0
  156. package/dist/types/server/board-move.d.ts +38 -0
  157. package/dist/types/server/canvas-db.d.ts +12 -2
  158. package/dist/types/server/canvas-operations.d.ts +5 -6
  159. package/dist/types/server/canvas-provenance.d.ts +1 -1
  160. package/dist/types/server/canvas-serialization.d.ts +1 -0
  161. package/dist/types/server/canvas-state.d.ts +86 -20
  162. package/dist/types/server/card-relocation.d.ts +6 -0
  163. package/dist/types/server/context-reads.d.ts +3 -1
  164. package/dist/types/server/human-presence.d.ts +1 -1
  165. package/dist/types/server/index.d.ts +26 -10
  166. package/dist/types/server/intent-registry.d.ts +8 -4
  167. package/dist/types/server/knowledge-lifecycle.d.ts +4 -2
  168. package/dist/types/server/mutation-history.d.ts +7 -5
  169. package/dist/types/server/operations/ops/boards.d.ts +0 -1
  170. package/dist/types/server/operations/registry.d.ts +2 -0
  171. package/dist/types/server/tour-control.d.ts +13 -3
  172. package/dist/types/server/trace-manager.d.ts +1 -0
  173. package/dist/types/shared/ax-kinds.d.ts +6 -0
  174. package/dist/types/shared/ax-surface-protocol.d.ts +8 -9
  175. package/dist/types/shared/human-presence.d.ts +1 -0
  176. package/dist/types/shared/lifecycle-status.d.ts +2 -0
  177. package/dist/types/shared/tour.d.ts +8 -0
  178. package/docs/RELEASE.md +21 -14
  179. package/docs/agent-plugin.md +16 -2
  180. package/docs/amp-plugin.md +271 -0
  181. package/docs/cli.md +42 -3
  182. package/docs/design/Chrome.dc.html +32 -6
  183. package/docs/design/GuestName.dc.html +50 -0
  184. package/docs/design/PluginLogo.svg +7 -0
  185. package/docs/design/Types1.dc.html +3 -2
  186. package/docs/design/Types2.dc.html +1 -1
  187. package/docs/design/Types3.dc.html +1 -1
  188. package/docs/design/WorkCards.dc.html +39 -0
  189. package/docs/design/Zoom.dc.html +37 -60
  190. package/docs/design/canvas.json +17 -1
  191. package/docs/design/plugin-logo.png +0 -0
  192. package/docs/design.md +280 -12
  193. package/docs/evals/agents-md-sections.md +38 -0
  194. package/docs/http-api.md +179 -11
  195. package/docs/mcp.md +69 -9
  196. package/docs/node-types.md +109 -4
  197. package/docs/plans/plan-009-tech-debt-backlog.md +3 -3
  198. package/docs/plans/plan-019-post-0.9-roadmap.md +327 -31
  199. package/docs/plans/plan-020-wave-5-work-planning.md +553 -0
  200. package/docs/product-vision-2026-09.md +42 -17
  201. package/docs/sdk.md +18 -0
  202. package/package.json +7 -4
  203. package/plugins/pmx-canvas/assets/preview.png +0 -0
  204. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +3 -1
  205. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  206. package/plugins/pmx-canvas/plugin.json +2 -2
  207. package/plugins/pmx-canvas/runtime.json +1 -1
  208. package/plugins/pmx-canvas/tools.json +688 -128
  209. package/skills/data-analysis/SKILL.md +13 -26
  210. package/skills/frontend-design/LICENSE.txt +177 -0
  211. package/skills/frontend-design/SKILL.md +3 -0
  212. package/skills/pmx-canvas/SKILL.md +232 -849
  213. package/skills/pmx-canvas/evals/evals.json +26 -3
  214. package/skills/pmx-canvas/references/full-reference.md +89 -24
  215. package/skills/pmx-canvas/references/html-primitives.md +7 -0
  216. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  217. package/skills/pmx-canvas/references/tours-and-recording.md +29 -2
  218. package/skills/pmx-canvas/references/workbench-workflows.md +900 -0
  219. package/skills/pmx-canvas-testing/SKILL.md +7 -5
  220. package/skills/published-consumer-e2e/SKILL.md +25 -8
  221. package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +51 -77
  222. package/skills/web-artifacts-builder/LICENSE.txt +202 -0
  223. package/skills/web-artifacts-builder/SKILL.md +6 -0
  224. package/skills/web-artifacts-builder/scripts/init-artifact.sh +2 -2
  225. package/src/cli/commands/apps.ts +1 -0
  226. package/src/cli/commands/boards.ts +30 -0
  227. package/src/client/App.tsx +20 -4
  228. package/src/client/canvas/AttentionHistory.tsx +10 -2
  229. package/src/client/canvas/BarHint.tsx +6 -3
  230. package/src/client/canvas/BoardMoveDialog.tsx +225 -0
  231. package/src/client/canvas/CanvasNode.tsx +79 -49
  232. package/src/client/canvas/CanvasViewport.tsx +42 -14
  233. package/src/client/canvas/ContextMenu.tsx +97 -15
  234. package/src/client/canvas/EdgeEditor.tsx +149 -0
  235. package/src/client/canvas/EdgeLayer.tsx +441 -174
  236. package/src/client/canvas/ExpandedNodeOverlay.tsx +5 -2
  237. package/src/client/canvas/GroupNavigation.tsx +85 -0
  238. package/src/client/canvas/Presentation.tsx +53 -3
  239. package/src/client/canvas/RelationsPanel.tsx +9 -0
  240. package/src/client/canvas/SelectionBar.tsx +21 -0
  241. package/src/client/canvas/SessionPanel.tsx +35 -1
  242. package/src/client/canvas/TextPrompt.tsx +18 -1
  243. package/src/client/canvas/TopBar.tsx +54 -9
  244. package/src/client/canvas/auto-fit.ts +9 -11
  245. package/src/client/canvas/canvas-area.ts +2 -2
  246. package/src/client/canvas/use-focus-trap.ts +6 -1
  247. package/src/client/mermaid-entry.ts +6 -20
  248. package/src/client/mermaid-render.ts +39 -0
  249. package/src/client/nodes/AxStepControls.tsx +45 -69
  250. package/src/client/nodes/BoardNode.tsx +65 -9
  251. package/src/client/nodes/ContextNode.tsx +88 -396
  252. package/src/client/nodes/ExtAppFrame.tsx +302 -1465
  253. package/src/client/nodes/FileNode.tsx +92 -81
  254. package/src/client/nodes/HtmlNode.tsx +8 -36
  255. package/src/client/nodes/LedgerNode.tsx +31 -17
  256. package/src/client/nodes/MarkdownNode.tsx +14 -24
  257. package/src/client/nodes/McpAppNode.tsx +48 -154
  258. package/src/client/nodes/MermaidNode.tsx +80 -69
  259. package/src/client/nodes/PromptNode.tsx +44 -17
  260. package/src/client/nodes/ResponseNode.tsx +8 -6
  261. package/src/client/nodes/StatusNode.tsx +34 -7
  262. package/src/client/nodes/StatusSummary.tsx +16 -3
  263. package/src/client/nodes/TraceNode.tsx +8 -8
  264. package/src/client/nodes/ViewerFrame.tsx +273 -0
  265. package/src/client/nodes/ViewerHost.tsx +41 -0
  266. package/src/client/nodes/WebpageNode.tsx +22 -24
  267. package/src/client/nodes/WorkCard.tsx +175 -0
  268. package/src/client/nodes/iframe-document-url.ts +20 -27
  269. package/src/client/nodes/primitive-copy.ts +97 -0
  270. package/src/client/nodes/use-ax-surface-bridge.ts +18 -14
  271. package/src/client/nodes/use-iframe-content-height.ts +34 -10
  272. package/src/client/nodes/use-surface-frame.ts +22 -11
  273. package/src/client/nodes/viewer-runtime-state.ts +41 -35
  274. package/src/client/state/attention-bridge.ts +5 -0
  275. package/src/client/state/attention-store.ts +1 -0
  276. package/src/client/state/canvas-store.ts +202 -26
  277. package/src/client/state/human-store.ts +22 -2
  278. package/src/client/state/intent-bridge.ts +20 -5
  279. package/src/client/state/presentation.ts +135 -15
  280. package/src/client/state/relation-store.ts +127 -1
  281. package/src/client/state/sse-bridge.ts +18 -19
  282. package/src/client/theme/global.css +483 -230
  283. package/src/client/theme/surface-theme.css +6 -6
  284. package/src/client/types.ts +1 -0
  285. package/src/json-render/charts/tufte-components.tsx +44 -2
  286. package/src/json-render/renderer/index.css +62 -2
  287. package/src/json-render/renderer/index.tsx +31 -7
  288. package/src/json-render/server.ts +3 -2
  289. package/src/mcp/server.ts +13 -3
  290. package/src/server/agent-presence.ts +43 -23
  291. package/src/server/ax-flow-loop.ts +6 -0
  292. package/src/server/ax-gate-ttl.ts +8 -1
  293. package/src/server/ax-interaction.ts +16 -10
  294. package/src/server/ax-state-manager.ts +17 -4
  295. package/src/server/ax-wait.ts +20 -6
  296. package/src/server/board-export.ts +54 -30
  297. package/src/server/board-local.ts +20 -0
  298. package/src/server/board-move.ts +297 -0
  299. package/src/server/canvas-db.ts +249 -158
  300. package/src/server/canvas-geometry.ts +1 -0
  301. package/src/server/canvas-operations.ts +139 -109
  302. package/src/server/canvas-schema.ts +21 -0
  303. package/src/server/canvas-serialization.ts +2 -0
  304. package/src/server/canvas-state.ts +797 -159
  305. package/src/server/card-relocation.ts +53 -0
  306. package/src/server/code-graph.ts +4 -3
  307. package/src/server/context-reads.ts +83 -55
  308. package/src/server/demo-state.json +140 -114
  309. package/src/server/file-watcher.ts +30 -10
  310. package/src/server/html-primitives.ts +3 -9
  311. package/src/server/html-surface.ts +8 -0
  312. package/src/server/human-presence.ts +18 -5
  313. package/src/server/index.ts +54 -24
  314. package/src/server/intent-registry.ts +43 -5
  315. package/src/server/knowledge-lifecycle.ts +32 -9
  316. package/src/server/lifecycle-status.ts +29 -1
  317. package/src/server/mcp-app-runtime.ts +26 -7
  318. package/src/server/mutation-history.ts +37 -8
  319. package/src/server/operations/composites.ts +6 -3
  320. package/src/server/operations/mcp-contracts.ts +32 -2
  321. package/src/server/operations/mcp.ts +36 -9
  322. package/src/server/operations/ops/annotation.ts +22 -1
  323. package/src/server/operations/ops/ax-await.ts +1 -0
  324. package/src/server/operations/ops/ax-timeline.ts +28 -2
  325. package/src/server/operations/ops/batch.ts +25 -11
  326. package/src/server/operations/ops/boards.ts +45 -6
  327. package/src/server/operations/ops/nodes.ts +70 -0
  328. package/src/server/operations/ops/query.ts +32 -3
  329. package/src/server/operations/ops/snapshots.ts +1 -0
  330. package/src/server/operations/ops/viewport.ts +11 -9
  331. package/src/server/operations/registry.ts +49 -4
  332. package/src/server/server.ts +69 -75
  333. package/src/server/tour-control.ts +97 -12
  334. package/src/server/trace-manager.ts +4 -0
  335. package/src/server/web-artifacts/scripts/init-artifact.sh +2 -2
  336. package/src/shared/agent-context.ts +7 -1
  337. package/src/shared/auto-arrange.ts +5 -4
  338. package/src/shared/ax-kinds.ts +13 -0
  339. package/src/shared/ax-surface-protocol.ts +20 -9
  340. package/src/shared/content-height-reporter.ts +4 -3
  341. package/src/shared/human-presence.ts +1 -0
  342. package/src/shared/lifecycle-status.ts +2 -0
  343. package/src/shared/tour.ts +8 -0
@@ -10,6 +10,7 @@ import { watch, existsSync, statSync } from 'node:fs';
10
10
  import type { FSWatcher } from 'node:fs';
11
11
  import { canvasState } from './canvas-state.js';
12
12
  import { applyFileContentToNodeData, readFileNodeContent } from './file-content.js';
13
+ import { createBoardLocal } from './board-local.js';
13
14
 
14
15
  function logFileWatcherWarning(action: string, error: unknown, details?: Record<string, unknown>): void {
15
16
  console.warn(`[file-watcher] ${action}`, { error, ...(details ?? {}) });
@@ -22,7 +23,22 @@ interface WatchedFile {
22
23
  lastMtime: number;
23
24
  }
24
25
 
25
- const watched = new Map<string, WatchedFile>();
26
+ const watcherSets = new Set<Map<string, WatchedFile>>();
27
+ function stopWatching(watched: Map<string, WatchedFile>): void {
28
+ for (const entry of watched.values()) entry.watcher.close();
29
+ watched.clear();
30
+ }
31
+ const boardWatchers = createBoardLocal(
32
+ () => {
33
+ const watched = new Map<string, WatchedFile>();
34
+ watcherSets.add(watched);
35
+ return watched;
36
+ },
37
+ (watched) => {
38
+ stopWatching(watched);
39
+ watcherSets.delete(watched);
40
+ },
41
+ );
26
42
  let onFileChanged: ((nodeId: string) => void) | null = null;
27
43
 
28
44
  /** Register a callback for when a watched file changes. */
@@ -34,6 +50,7 @@ export function onFileNodeChanged(cb: (nodeId: string) => void): void {
34
50
  export function watchFileForNode(nodeId: string, filePath: string): void {
35
51
  if (!filePath || !existsSync(filePath)) return;
36
52
 
53
+ const watched = boardWatchers.current;
37
54
  const existing = watched.get(filePath);
38
55
  if (existing) {
39
56
  existing.nodeIds.add(nodeId);
@@ -42,10 +59,14 @@ export function watchFileForNode(nodeId: string, filePath: string): void {
42
59
 
43
60
  try {
44
61
  const stat = statSync(filePath);
45
- const watcher = watch(filePath, { persistent: false }, (eventType) => {
46
- if (eventType !== 'change') return;
47
- handleFileChange(filePath);
48
- });
62
+ const watcher = watch(
63
+ filePath,
64
+ { persistent: false },
65
+ canvasState.bindBoard((eventType) => {
66
+ if (eventType !== 'change') return;
67
+ handleFileChange(filePath);
68
+ }),
69
+ );
49
70
 
50
71
  watched.set(filePath, {
51
72
  path: filePath,
@@ -60,6 +81,7 @@ export function watchFileForNode(nodeId: string, filePath: string): void {
60
81
 
61
82
  /** Stop watching a file for a given node. */
62
83
  export function unwatchFileForNode(nodeId: string, filePath?: string): void {
84
+ const watched = boardWatchers.current;
63
85
  for (const [path, entry] of watched) {
64
86
  if (filePath && path !== filePath) continue;
65
87
  entry.nodeIds.delete(nodeId);
@@ -72,14 +94,11 @@ export function unwatchFileForNode(nodeId: string, filePath?: string): void {
72
94
 
73
95
  /** Stop all watchers. */
74
96
  export function unwatchAll(): void {
75
- for (const entry of watched.values()) {
76
- entry.watcher.close();
77
- }
78
- watched.clear();
97
+ for (const watched of watcherSets) stopWatching(watched);
79
98
  }
80
99
 
81
100
  export function rewatchAllFileNodes(): void {
82
- unwatchAll();
101
+ stopWatching(boardWatchers.current);
83
102
  for (const node of canvasState.getLayout().nodes) {
84
103
  if (node.type === 'file' && typeof node.data.path === 'string') {
85
104
  watchFileForNode(node.id, node.data.path);
@@ -88,6 +107,7 @@ export function rewatchAllFileNodes(): void {
88
107
  }
89
108
 
90
109
  function handleFileChange(filePath: string): void {
110
+ const watched = boardWatchers.current;
91
111
  const entry = watched.get(filePath);
92
112
  if (!entry) return;
93
113
 
@@ -812,13 +812,12 @@ function page(input: {
812
812
  * { box-sizing: border-box; }
813
813
  body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }
814
814
  .shell { max-width: 1160px; margin: 0 auto; }
815
- header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }
815
+ header.hero { margin-bottom: 20px; }
816
816
  .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }
817
817
  h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }
818
818
  h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }
819
819
  h3 { margin: 0 0 8px; font-size: 15px; }
820
820
  p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }
821
- .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
822
821
  button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }
823
822
  button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }
824
823
  table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }
@@ -861,20 +860,20 @@ function page(input: {
861
860
  .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }
862
861
  .preview { white-space: pre-wrap; min-height: 220px; }
863
862
  .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }
864
- @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }
863
+ @media (max-width: 760px) { body { padding: 14px; } .two, .three { grid-template-columns: 1fr; } .sticky { position: static; } }
865
864
  </style>
866
865
  </head>
867
866
  <body>
868
867
  <div class="shell">
869
868
  <header class="hero">
870
869
  <div><div class="kicker">PMX HTML primitive / ${escapeHtml(input.kind)}</div><h1>${escapeHtml(input.title)}</h1><p>${escapeHtml(input.summary)}</p></div>
871
- <div class="actions"><button type="button" data-copy-json>Copy JSON</button><button type="button" data-copy-prompt>Copy prompt</button></div>
872
870
  </header>
873
871
  ${input.body}
874
872
  </div>
875
873
  <script type="application/json" id="pmx-data">${safeJson(input.data)}</script>
876
874
  <script>
877
875
  const PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);
876
+ window.__pmxGetCopyJson = () => PMX_DATA;
878
877
  function fallbackCopy(text) {
879
878
  const el = document.createElement('textarea');
880
879
  el.value = text;
@@ -890,11 +889,6 @@ async function copyText(text) {
890
889
  if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);
891
890
  else fallbackCopy(text);
892
891
  }
893
- document.querySelector('[data-copy-json]')?.addEventListener('click', () => {
894
- const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;
895
- copyText(JSON.stringify(payload, null, 2));
896
- });
897
- document.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));
898
892
  ${input.script ?? ''}
899
893
  </script>
900
894
  </body>
@@ -61,6 +61,14 @@ const PMX_CANVAS_THEME_TOKEN = ${token};
61
61
  window.addEventListener('message', (event) => {
62
62
  const message = event.data;
63
63
  if (event.source !== window.parent || !message || message.source !== '${HTML_SURFACE_PUSH_SOURCE}' || message.token !== PMX_CANVAS_THEME_TOKEN) return;
64
+ if (message.type === 'copy-json' && PMX_CANVAS_THEME_TOKEN && typeof message.requestId === 'string') {
65
+ let json;
66
+ try {
67
+ if (typeof window.__pmxGetCopyJson === 'function') json = JSON.stringify(window.__pmxGetCopyJson(), null, 2);
68
+ } catch {}
69
+ window.parent.postMessage({ source: '${HTML_SURFACE_PUSH_SOURCE}', type: 'copy-json-result', token: PMX_CANVAS_THEME_TOKEN, requestId: message.requestId, json }, '*');
70
+ return;
71
+ }
64
72
  if (message.type === 'check-ready') {
65
73
  const reply = (type) => window.parent.postMessage({ source: '${HTML_SURFACE_PUSH_SOURCE}', type, token: PMX_CANVAS_THEME_TOKEN }, '*');
66
74
  reply('mounted');
@@ -1,12 +1,14 @@
1
1
  import { z } from 'zod';
2
2
  import {
3
3
  HUMAN_GRAB_TTL_MS,
4
+ HUMAN_NAME_MAX_LENGTH,
4
5
  HUMAN_PRESENCE_TTL_MS,
5
6
  type HumanPresence,
6
7
  type HumanPresenceSnapshot,
7
8
  MAX_HUMANS,
8
9
  } from '../shared/human-presence.js';
9
10
  import { OperationError } from './operations/types.js';
11
+ import { canvasState } from './canvas-state.js';
10
12
 
11
13
  /**
12
14
  * Registry of open workbench tabs (rail-chrome-v2 phase 8). Fed by the
@@ -17,7 +19,7 @@ import { OperationError } from './operations/types.js';
17
19
  */
18
20
  export const HUMAN_PRESENCE_SHAPE = {
19
21
  clientId: z.string().min(1).max(64),
20
- name: z.string().min(1).max(40).optional(),
22
+ name: z.string().min(1).max(HUMAN_NAME_MAX_LENGTH).optional(),
21
23
  cursor: z.object({ x: z.number().finite(), y: z.number().finite() }).nullable().optional(),
22
24
  grabbingNodeId: z.string().max(200).nullable().optional(),
23
25
  /** Explicit leave (tab closing): drop the presence now. */
@@ -26,6 +28,7 @@ export const HUMAN_PRESENCE_SHAPE = {
26
28
  const presenceSchema = z.object(HUMAN_PRESENCE_SHAPE);
27
29
 
28
30
  interface Stored extends HumanPresence {
31
+ boardId: string | null;
29
32
  lastSeenMs: number;
30
33
  grabSinceMs: number | null;
31
34
  }
@@ -57,6 +60,7 @@ export class HumanPresenceRegistry {
57
60
  }
58
61
  const existing = this.humans.get(input.clientId);
59
62
  const stored: Stored = existing ?? {
63
+ boardId: canvasState.activeBoardId,
60
64
  clientId: input.clientId,
61
65
  name: input.name ?? 'Human',
62
66
  cursor: null,
@@ -65,6 +69,11 @@ export class HumanPresenceRegistry {
65
69
  lastSeenMs: now,
66
70
  grabSinceMs: null,
67
71
  };
72
+ if (stored.boardId !== canvasState.activeBoardId) {
73
+ stored.boardId = canvasState.activeBoardId;
74
+ stored.grabbingNodeId = null;
75
+ stored.grabSinceMs = null;
76
+ }
68
77
  stored.lastSeenMs = now;
69
78
  stored.lastSeenAt = new Date(now).toISOString();
70
79
  if (input.name) stored.name = input.name;
@@ -91,9 +100,10 @@ export class HumanPresenceRegistry {
91
100
  }
92
101
 
93
102
  /** Nodes a human is holding right now → that human's name. The edit lock. */
94
- lockedNodes(now = Date.now()): Map<string, string> {
103
+ lockedNodes(now = Date.now(), boardId = canvasState.activeBoardId): Map<string, string> {
95
104
  const locked = new Map<string, string>();
96
105
  for (const human of this.humans.values()) {
106
+ if (human.boardId !== boardId) continue;
97
107
  if (human.grabbingNodeId && human.grabSinceMs !== null && now - human.grabSinceMs <= HUMAN_GRAB_TTL_MS) {
98
108
  locked.set(human.grabbingNodeId, human.name);
99
109
  }
@@ -111,13 +121,16 @@ export class HumanPresenceRegistry {
111
121
  }
112
122
 
113
123
  private publicView(stored: Stored): HumanPresence {
114
- const { lastSeenMs: _lastSeenMs, grabSinceMs, ...human } = stored;
124
+ const { lastSeenMs: _lastSeenMs, grabSinceMs, boardId, ...human } = stored;
115
125
  // An expired grab reads as released even before the sweep drops it.
116
126
  const grabbing =
117
- human.grabbingNodeId && grabSinceMs !== null && Date.now() - grabSinceMs <= HUMAN_GRAB_TTL_MS
127
+ boardId === canvasState.activeBoardId &&
128
+ human.grabbingNodeId &&
129
+ grabSinceMs !== null &&
130
+ Date.now() - grabSinceMs <= HUMAN_GRAB_TTL_MS
118
131
  ? human.grabbingNodeId
119
132
  : null;
120
- return { ...human, grabbingNodeId: grabbing };
133
+ return { ...human, cursor: boardId === canvasState.activeBoardId ? human.cursor : null, grabbingNodeId: grabbing };
121
134
  }
122
135
 
123
136
  private sweep(now = Date.now(), options: { emit?: boolean } = {}): void {
@@ -1,5 +1,7 @@
1
1
  import { EventEmitter } from 'node:events';
2
- import type { Camera, Tour, TourStop } from '../shared/tour.js';
2
+ import type { BoardMoveInput, BoardMovePreview, BoardMoveResult } from './board-move.js';
3
+ export type { BoardMoveInput, BoardMovePreview, BoardMoveResult } from './board-move.js';
4
+ import type { Camera, Tour, TourPresenter, TourStop } from '../shared/tour.js';
3
5
  import {
4
6
  currentTour,
5
7
  exitTour,
@@ -8,9 +10,10 @@ import {
8
10
  type TourState,
9
11
  type TourStep,
10
12
  type TourStepResult,
13
+ type TourSuperseded,
11
14
  } from './tour-control.js';
12
- export type { Tour, TourStop } from '../shared/tour.js';
13
- export type { TourState, TourStep, TourStepResult } from './tour-control.js';
15
+ export type { Tour, TourPresenter, TourStop } from '../shared/tour.js';
16
+ export type { TourState, TourStep, TourStepResult, TourSuperseded } from './tour-control.js';
14
17
  import { agentPresence } from './agent-presence.js';
15
18
  import { checkFenceTarget, type FenceTarget } from './scope-fence.js';
16
19
  import type { AgentPhase, AgentPresence, AgentPresenceSnapshot } from '../shared/agent-presence.js';
@@ -507,21 +510,19 @@ export class PmxCanvas extends EventEmitter {
507
510
  return (await executeOperation('edge.update', { id, ...patch }, { source: 'sdk' })) as CanvasEdge;
508
511
  }
509
512
 
510
- addAnnotation(input: Omit<CanvasAnnotation, 'id' | 'createdAt'> & { id?: string; createdAt?: string }): string {
511
- const id = input.id ?? `ann-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
512
- canvasState.addAnnotation({
513
- ...input,
514
- id,
515
- createdAt: input.createdAt ?? new Date().toISOString(),
516
- });
517
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
518
- return id;
513
+ async addAnnotation(
514
+ input: Pick<CanvasAnnotation, 'points'> &
515
+ Partial<Pick<CanvasAnnotation, 'id' | 'type' | 'color' | 'width' | 'text' | 'label'>>,
516
+ ): Promise<string> {
517
+ const result = (await executeOperation('annotation.add', input, { source: 'sdk' })) as {
518
+ annotation: { id: string };
519
+ };
520
+ return result.annotation.id;
519
521
  }
520
522
 
521
- removeAnnotation(id: string): boolean {
522
- const removed = canvasState.removeAnnotation(id);
523
- if (removed) emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
524
- return removed;
523
+ async removeAnnotation(id: string): Promise<boolean> {
524
+ const result = (await executeOperation('annotation.remove', { id }, { source: 'sdk' })) as { ok: boolean };
525
+ return result.ok;
525
526
  }
526
527
 
527
528
  removeEdge(id: string): void {
@@ -631,16 +632,25 @@ export class PmxCanvas extends EventEmitter {
631
632
  }
632
633
 
633
634
  /** Present a tour stop (index, `next` or `previous`) in the workbench. Not recorded in undo history. */
634
- goToTourStop(step: TourStep, options?: { present?: boolean }): TourStepResult {
635
- const result = goToTourStop(step);
636
- emitPrimaryWorkbenchEvent('canvas-tour-step', { ...result, present: options?.present !== false });
635
+ goToTourStop(step: TourStep, options?: { present?: boolean; presenter?: undefined }): TourStepResult;
636
+ goToTourStop(
637
+ step: TourStep,
638
+ options: { present?: boolean; presenter?: TourPresenter },
639
+ ): TourStepResult | TourSuperseded;
640
+ goToTourStop(
641
+ step: TourStep,
642
+ options?: { present?: boolean; presenter?: TourPresenter },
643
+ ): TourStepResult | TourSuperseded {
644
+ const result = goToTourStop(step, options?.presenter);
645
+ if (!('superseded' in result))
646
+ emitPrimaryWorkbenchEvent('canvas-tour-step', { ...result, present: options?.present !== false });
637
647
  return result;
638
648
  }
639
649
 
640
650
  /** Leave presentation in every viewer. */
641
- exitTour(): void {
642
- exitTour();
643
- emitPrimaryWorkbenchEvent('canvas-tour-exit', {});
651
+ exitTour(presenter?: TourPresenter): void {
652
+ const result = exitTour(presenter);
653
+ if (!('superseded' in result)) emitPrimaryWorkbenchEvent('canvas-tour-exit', { ...result });
644
654
  }
645
655
 
646
656
  /** Ease the camera to a node, world rect or viewport. Not recorded in undo history. */
@@ -754,7 +764,12 @@ export class PmxCanvas extends EventEmitter {
754
764
  }
755
765
 
756
766
  /** Undelivered steering for a consumer (loop-safe; excludes consumer-originated). */
757
- getPendingSteering(options?: { consumer?: string; limit?: number }): PmxAxSteeringMessage[] {
767
+ getPendingSteering(options?: {
768
+ consumer?: string;
769
+ limit?: number;
770
+ addressedOnly?: boolean;
771
+ offset?: number;
772
+ }): PmxAxSteeringMessage[] {
758
773
  return canvasState.getPendingSteering(options ?? {});
759
774
  }
760
775
 
@@ -944,6 +959,15 @@ export class PmxCanvas extends EventEmitter {
944
959
  return (await executeOperation('board.create-from', input, { source: 'sdk' })) as Record<string, unknown>;
945
960
  }
946
961
 
962
+ /** Relocate identities atomically; preview is read-only and retries reuse the original receipt. */
963
+ async moveSelectionToNewBoard(
964
+ input: BoardMoveInput,
965
+ ): Promise<{ ok: true; preview: BoardMovePreview } | (BoardMoveResult & { ok: true; replayed: boolean })> {
966
+ return (await executeOperation('board.move-selection', input, { source: 'sdk' })) as
967
+ | { ok: true; preview: BoardMovePreview }
968
+ | (BoardMoveResult & { ok: true; replayed: boolean });
969
+ }
970
+
947
971
  async searchLibrary(query: string, limit?: number): Promise<Record<string, unknown>> {
948
972
  return (await executeOperation('search', { q: query, scope: 'library', limit }, { source: 'sdk' })) as Record<
949
973
  string,
@@ -960,7 +984,13 @@ export class PmxCanvas extends EventEmitter {
960
984
 
961
985
  /** Budgeted cross-board brief. Budget is measured in UTF-16 characters. */
962
986
  async getContextBrief(
963
- options: { consumer?: string; since?: number; budget?: number } = {},
987
+ options: {
988
+ consumer?: string;
989
+ since?: number;
990
+ budget?: number;
991
+ contextDelivery?: boolean;
992
+ includeSteering?: boolean;
993
+ } = {},
964
994
  ): Promise<Record<string, unknown>> {
965
995
  return (await executeOperation('context.get', options, { source: 'sdk' })) as Record<string, unknown>;
966
996
  }
@@ -25,6 +25,8 @@ import {
25
25
  type PmxAxIntentKind,
26
26
  } from '../shared/ax-intent.js';
27
27
  import { OperationError } from './operations/types.js';
28
+ import { createBoardLocal } from './board-local.js';
29
+ import { canvasState } from './canvas-state.js';
28
30
 
29
31
  type IntentEmitter = (event: string, payload: Record<string, unknown>) => void;
30
32
  const MAX_VETO_TOMBSTONES = 128;
@@ -103,11 +105,33 @@ function nextIntentId(): string {
103
105
  }
104
106
 
105
107
  export class IntentRegistry {
106
- private readonly intents = new Map<string, PmxAxIntent>();
107
- private readonly vetoedIntentIds = new Map<string, number>();
108
- private readonly committingIntentIds = new Set<string>();
108
+ private readonly board = createBoardLocal(
109
+ () => ({
110
+ intents: new Map<string, PmxAxIntent>(),
111
+ vetoedIntentIds: new Map<string, number>(),
112
+ committingIntentIds: new Set<string>(),
113
+ sweepTimer: null as ReturnType<typeof setInterval> | null,
114
+ }),
115
+ (state) => {
116
+ if (state.sweepTimer) clearInterval(state.sweepTimer);
117
+ },
118
+ );
119
+ private get intents() {
120
+ return this.board.current.intents;
121
+ }
122
+ private get vetoedIntentIds() {
123
+ return this.board.current.vetoedIntentIds;
124
+ }
125
+ private get committingIntentIds() {
126
+ return this.board.current.committingIntentIds;
127
+ }
109
128
  private emit: IntentEmitter = () => {};
110
- private sweepTimer: ReturnType<typeof setInterval> | null = null;
129
+ private get sweepTimer() {
130
+ return this.board.current.sweepTimer;
131
+ }
132
+ private set sweepTimer(value: ReturnType<typeof setInterval> | null) {
133
+ this.board.current.sweepTimer = value;
134
+ }
111
135
 
112
136
  /** Inject the workbench SSE emitter (server.ts wires this at module load). */
113
137
  setEmitter(emitter: IntentEmitter | null): void {
@@ -118,6 +142,17 @@ export class IntentRegistry {
118
142
  return [...this.intents.values()];
119
143
  }
120
144
 
145
+ /** The first write from Home adopts its ghosts and vetoes without opening on signal. */
146
+ moveHomeIntentsToBoard(boardId: string): void {
147
+ const home = this.board.current;
148
+ canvasState.withBoard(boardId, () => {
149
+ for (const [id, intent] of home.intents) this.intents.set(id, intent);
150
+ for (const [id, expires] of home.vetoedIntentIds) this.vetoedIntentIds.set(id, expires);
151
+ this.ensureSweeper();
152
+ });
153
+ this.reset();
154
+ }
155
+
121
156
  /** Signal a new (or replace an existing) intent. Returns the stored envelope. */
122
157
  signal(raw: unknown): PmxAxIntent {
123
158
  const input = parseOrThrow(intentSignalSchema, raw, 'intent');
@@ -335,7 +370,10 @@ export class IntentRegistry {
335
370
 
336
371
  private ensureSweeper(): void {
337
372
  if (this.sweepTimer || this.intents.size === 0) return;
338
- this.sweepTimer = setInterval(() => this.sweep(), 1000);
373
+ this.sweepTimer = setInterval(
374
+ canvasState.bindBoard(() => this.sweep()),
375
+ 1000,
376
+ );
339
377
  // Don't keep the process (or a test runner) alive just for ghost expiry.
340
378
  (this.sweepTimer as { unref?: () => void }).unref?.();
341
379
  }
@@ -4,6 +4,7 @@ import { normalizeBoardFolder } from '../shared/boards.js';
4
4
  import { memoryKey, type MemoryTarget } from '../shared/memory-graph.js';
5
5
  import { currentActor, type ActorAttribution } from './attribution.js';
6
6
  import { OperationError } from './operations/types.js';
7
+ import { CARD_HISTORY_CTE } from './card-relocation.js';
7
8
  import type { LifecycleQualification } from '../shared/lifecycle-status.js';
8
9
  import type { CanvasNodeState } from './canvas-state.js';
9
10
  import { RELATION_PRESETS, relationPresetSchema, type RelationPreset } from '../shared/relations.js';
@@ -281,15 +282,19 @@ export class KnowledgeLifecycle {
281
282
  if (!targets.length) return new Map();
282
283
  const rows = this.indexedDb()
283
284
  .query<{ boardId: string; nodeId: string; validation: string | null; retirement: string | null }, [string]>(
284
- `WITH targets AS (
285
- SELECT json_extract(value, '$.boardId') AS boardId, json_extract(value, '$.cardId') AS nodeId
286
- FROM json_each(?)
287
- ) SELECT boardId, nodeId,
288
- (SELECT value FROM review_records WHERE board_id = boardId AND node_id = nodeId
285
+ `${CARD_HISTORY_CTE}
286
+ SELECT t.current_board AS boardId, t.node_id AS nodeId,
287
+ (SELECT value FROM review_records r JOIN history h
288
+ ON r.board_id = h.board_id AND r.node_id = h.node_id
289
+ AND r.sequence > h.review_after AND r.sequence <= h.review_through
290
+ WHERE h.current_board = t.current_board AND h.node_id = t.node_id
289
291
  AND ${validationPredicate} ORDER BY sequence DESC LIMIT 1) AS validation,
290
- (SELECT json_extract(value, '$.outcome') FROM review_records WHERE board_id = boardId AND node_id = nodeId
292
+ (SELECT json_extract(value, '$.outcome') FROM review_records r JOIN history h
293
+ ON r.board_id = h.board_id AND r.node_id = h.node_id
294
+ AND r.sequence > h.review_after AND r.sequence <= h.review_through
295
+ WHERE h.current_board = t.current_board AND h.node_id = t.node_id
291
296
  AND ${retirementPredicate} ORDER BY sequence DESC LIMIT 1) AS retirement
292
- FROM targets`,
297
+ FROM targets t`,
293
298
  )
294
299
  .all(JSON.stringify(targets));
295
300
  return new Map(
@@ -306,11 +311,29 @@ export class KnowledgeLifecycle {
306
311
  );
307
312
  }
308
313
 
314
+ /** Resolve a current card's location when evidence was recorded, without rewriting that evidence. */
315
+ locationAtReview(review: ReviewRecord, target: MemoryTarget): MemoryTarget {
316
+ const sequence = this.db()
317
+ .query<{ sequence: number }, [string, string, string]>(
318
+ "SELECT sequence FROM review_records WHERE board_id = ? AND node_id = ? AND json_extract(value, '$.id') = ?",
319
+ )
320
+ .get(review.boardId, review.nodeId, review.id)?.sequence;
321
+ if (sequence === undefined) return target;
322
+ const location = this.db()
323
+ .query<{ board_id: string }, [string, number, number]>(
324
+ `${CARD_HISTORY_CTE} SELECT board_id FROM history WHERE review_after < ? AND review_through >= ? LIMIT 1`,
325
+ )
326
+ .get(JSON.stringify([target]), sequence, sequence);
327
+ return location ? { ...target, boardId: location.board_id } : target;
328
+ }
329
+
309
330
  reviews(boardId: string, nodeId: string) {
310
331
  const records = (
311
332
  this.db()
312
- .query('SELECT value FROM review_records WHERE board_id = ? AND node_id = ? ORDER BY sequence')
313
- .all(boardId, nodeId) as { value: string }[]
333
+ .query(`${CARD_HISTORY_CTE} SELECT value FROM review_records r JOIN history h
334
+ ON r.board_id = h.board_id AND r.node_id = h.node_id
335
+ AND r.sequence > h.review_after AND r.sequence <= h.review_through ORDER BY sequence`)
336
+ .all(JSON.stringify([{ boardId, cardId: nodeId }])) as { value: string }[]
314
337
  ).map((r): ReviewRecord => {
315
338
  const record = JSON.parse(r.value) as ReviewRecord;
316
339
  return { ...record, contentRevision: record.contentRevision ?? null };
@@ -11,6 +11,7 @@ const processStartedAt = Date.now();
11
11
  interface Source {
12
12
  key: string;
13
13
  target: MemoryTarget;
14
+ edge?: { owner: MemoryTarget; id: string };
14
15
  baseline?: number;
15
16
  kind: string;
16
17
  observedAt?: string;
@@ -179,6 +180,7 @@ export function lifecycleSnapshot(now = Date.now(), atUse: MemoryTarget[] = [],
179
180
  existing.push({
180
181
  key: JSON.stringify([board.id, edge.id, kind, memoryKey(target)]),
181
182
  target,
183
+ edge: { owner: from, id: edge.id },
182
184
  kind,
183
185
  baseline: edge.sourceRevision,
184
186
  });
@@ -219,13 +221,30 @@ export function lifecycleSnapshot(now = Date.now(), atUse: MemoryTarget[] = [],
219
221
  const current = cards.get(memoryKey(source.target));
220
222
  const revision = source.availability === 'unavailable' ? null : (current?.contentRevision ?? null);
221
223
  status.observations[source.key] = revision;
222
- const observed = validation ? validation.sourceRevisions?.[source.key] : source.baseline;
224
+ let observed = validation ? validation.sourceRevisions?.[source.key] : source.baseline;
225
+ if (validation?.sourceRevisions && observed === undefined) {
226
+ const target = canvasState.knowledge.locationAtReview(validation, source.target);
227
+ const historicalKey = source.edge
228
+ ? JSON.stringify([
229
+ canvasState.knowledge.locationAtReview(validation, source.edge.owner).boardId,
230
+ source.edge.id,
231
+ source.kind,
232
+ memoryKey(target),
233
+ ])
234
+ : `provenance:${memoryKey(target)}`;
235
+ observed = validation.sourceRevisions[historicalKey];
236
+ }
223
237
  const title = current ? String(current.data.title ?? current.id) : 'Unavailable source';
238
+ const before = status.reasons.length;
224
239
  if (!current) status.reasons.push('A dependency source is unavailable; no fresh check.');
225
240
  else if (source.availability === 'unavailable')
226
241
  status.reasons.push(`${title}: source unavailable; cached content is not a fresh check.`);
227
242
  else if (observed !== revision || (!validation && ['contradicts', 'supersedes'].includes(source.kind)))
228
243
  status.reasons.push(`${source.kind}: ${title}${observed === undefined ? ' needs review' : ' changed'}.`);
244
+ if (status.reasons.length > before) {
245
+ status.sourceWarnings ??= {};
246
+ status.sourceWarnings[source.key] = status.reasons.slice(before);
247
+ }
229
248
  }
230
249
  }
231
250
  // Fixed point, not recursive traversal: cycles cannot overflow and unrelated links never propagate.
@@ -246,6 +265,15 @@ export function lifecycleSnapshot(now = Date.now(), atUse: MemoryTarget[] = [],
246
265
  }
247
266
  }
248
267
  for (const [key, status] of Object.entries(result)) {
268
+ for (const source of sources.get(key) ?? []) {
269
+ const upstream = result[memoryKey(source.target)];
270
+ if (upstream?.due || upstream?.retired) {
271
+ status.sourceWarnings ??= {};
272
+ status.sourceWarnings[source.key] ??= [
273
+ upstream.retired ? 'Source is retired.' : 'An explicit source needs review.',
274
+ ];
275
+ }
276
+ }
249
277
  const references: Source[] = [...(sources.get(key) ?? [])];
250
278
  const boardId = portals.get(key);
251
279
  if (boardId) {
@@ -18,6 +18,7 @@ import { EXTENSION_ID, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps
18
18
  import type { McpUiClientCapabilities, McpUiResourceCsp, McpUiResourceMeta } from '@modelcontextprotocol/ext-apps';
19
19
  import { getToolUiResourceUri } from '@modelcontextprotocol/ext-apps/app-bridge';
20
20
  import { normalizeExtAppToolResult } from './ext-app-tool-result.js';
21
+ import { canvasState } from './canvas-state.js';
21
22
 
22
23
  // A restriction follows async rehydration spawned by an embedded operation,
23
24
  // without disabling independent requests from the local workbench.
@@ -71,10 +72,12 @@ type RuntimeTransport = StdioClientTransport | StreamableHTTPClientTransport;
71
72
 
72
73
  interface McpAppSession {
73
74
  id: string;
75
+ board: object;
74
76
  serverName: string;
75
77
  client: Client;
76
78
  transport: RuntimeTransport;
77
79
  tools: Tool[];
80
+ unregisterBoardDispose: () => void;
78
81
  }
79
82
 
80
83
  const uiCapabilities: McpUiClientCapabilities = {
@@ -223,21 +226,35 @@ async function createSession(
223
226
  }
224
227
  const transport = buildTransport(transportConfig);
225
228
  const client = new Client({ name: 'pmx-canvas-app-host', version: '0.1.0' }, { capabilities: clientCapabilities });
226
- await client.connect(transport, requestOptions(timeoutMs));
227
-
228
- const toolList = await client.listTools(undefined, requestOptions(timeoutMs));
229
+ const id = randomId('mcp-app-session');
230
+ let disposed = false;
229
231
  const session: McpAppSession = {
230
- id: randomId('mcp-app-session'),
232
+ id,
233
+ board: canvasState.boardIdentity,
231
234
  serverName: normalizeServerName(serverName, transportConfig),
232
235
  client,
233
236
  transport,
234
- tools: toolList.tools,
237
+ tools: [],
238
+ unregisterBoardDispose: canvasState.onBoardDispose(() => {
239
+ disposed = true;
240
+ closeMcpAppSession(id);
241
+ }),
235
242
  };
236
243
  sessions.set(session.id, session);
237
- return session;
244
+ try {
245
+ await client.connect(transport, requestOptions(timeoutMs));
246
+ const toolList = await client.listTools(undefined, requestOptions(timeoutMs));
247
+ if (disposed) throw new Error('The app board was deleted while connecting.');
248
+ session.tools = toolList.tools;
249
+ return session;
250
+ } catch (error) {
251
+ await closeSession(session);
252
+ throw error;
253
+ }
238
254
  }
239
255
 
240
256
  async function closeSession(session: McpAppSession): Promise<void> {
257
+ session.unregisterBoardDispose();
241
258
  sessions.delete(session.id);
242
259
  await session.transport.close();
243
260
  }
@@ -468,5 +485,7 @@ export function hasMcpAppSession(sessionId: string): boolean {
468
485
  }
469
486
 
470
487
  export function listMcpAppSessionIds(): string[] {
471
- return [...sessions.keys()];
488
+ return [...sessions.values()]
489
+ .filter((session) => session.board === canvasState.boardIdentity)
490
+ .map((session) => session.id);
472
491
  }