pmx-canvas 0.2.0 → 0.2.2

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 (58) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/Readme.md +2 -2
  3. package/dist/canvas/global.css +260 -0
  4. package/dist/canvas/index.js +76 -76
  5. package/dist/json-render/index.js +2 -2
  6. package/dist/types/client/canvas/IntentLayer.d.ts +1 -0
  7. package/dist/types/client/state/intent-bridge.d.ts +10 -0
  8. package/dist/types/client/state/intent-store.d.ts +25 -0
  9. package/dist/types/json-render/server.d.ts +1 -1
  10. package/dist/types/server/ax-state-manager.d.ts +11 -0
  11. package/dist/types/server/ax-state.d.ts +2 -0
  12. package/dist/types/server/canvas-db.d.ts +13 -0
  13. package/dist/types/server/canvas-state.d.ts +5 -0
  14. package/dist/types/server/index.d.ts +34 -4
  15. package/dist/types/server/intent-registry.d.ts +45 -0
  16. package/dist/types/server/operations/ops/intent.d.ts +2 -0
  17. package/dist/types/shared/ax-intent.d.ts +58 -0
  18. package/docs/ax-host-adapter-contract.md +19 -1
  19. package/docs/http-api.md +4 -0
  20. package/docs/mcp.md +22 -3
  21. package/docs/screenshot.png +0 -0
  22. package/package.json +1 -1
  23. package/skills/pmx-canvas/SKILL.md +197 -1283
  24. package/skills/pmx-canvas/evals/evals.json +199 -0
  25. package/skills/pmx-canvas/references/ax-html-control-surface.md +93 -0
  26. package/skills/pmx-canvas/references/full-reference.md +1441 -0
  27. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +23 -7
  28. package/src/cli/index.ts +21 -4
  29. package/src/client/canvas/CanvasNode.tsx +13 -13
  30. package/src/client/canvas/CanvasViewport.tsx +2 -0
  31. package/src/client/canvas/ContextMenu.tsx +25 -19
  32. package/src/client/canvas/IntentLayer.tsx +278 -0
  33. package/src/client/nodes/ExtAppFrame.tsx +31 -22
  34. package/src/client/state/intent-bridge.ts +31 -0
  35. package/src/client/state/intent-store.ts +107 -0
  36. package/src/client/state/sse-bridge.ts +31 -0
  37. package/src/client/theme/global.css +260 -0
  38. package/src/json-render/charts/components.tsx +18 -4
  39. package/src/json-render/renderer/index.tsx +11 -2
  40. package/src/json-render/server.ts +1 -1
  41. package/src/server/ax-context.ts +8 -1
  42. package/src/server/ax-state-manager.ts +18 -0
  43. package/src/server/ax-state.ts +8 -0
  44. package/src/server/canvas-db.ts +35 -0
  45. package/src/server/canvas-state.ts +8 -0
  46. package/src/server/index.ts +240 -158
  47. package/src/server/intent-registry.ts +324 -0
  48. package/src/server/operations/composites.ts +11 -0
  49. package/src/server/operations/index.ts +2 -0
  50. package/src/server/operations/ops/edges.ts +1 -0
  51. package/src/server/operations/ops/groups.ts +3 -0
  52. package/src/server/operations/ops/intent.ts +132 -0
  53. package/src/server/operations/ops/json-render.ts +3 -0
  54. package/src/server/operations/ops/nodes.ts +3 -0
  55. package/src/server/operations/registry.ts +68 -3
  56. package/src/server/server.ts +40 -12
  57. package/src/shared/ax-intent.ts +64 -0
  58. package/src/shared/surface.ts +5 -1
@@ -181,6 +181,205 @@
181
181
  "type": "output_check"
182
182
  }
183
183
  ]
184
+ },
185
+ {
186
+ "id": 8,
187
+ "name": "workspace-identity-preflight",
188
+ "prompt": "Attach to the running PMX canvas and add a status node for our deploy. There's already a daemon on port 4313.",
189
+ "expected_output": "Before mutating, reads GET /health (or serve status) and confirms the returned workspace matches the intended workspace root; refuses to mutate a mismatched/stale listener and starts an isolated server on an explicit --port instead.",
190
+ "assertions": [
191
+ {
192
+ "name": "reads-health",
193
+ "description": "Reads /health or serve status before any mutation",
194
+ "type": "output_check"
195
+ },
196
+ {
197
+ "name": "checks-workspace",
198
+ "description": "Compares the returned workspace to the intended workspace root",
199
+ "type": "output_check"
200
+ },
201
+ {
202
+ "name": "handles-mismatch",
203
+ "description": "On workspace mismatch or responsive+pidRunning:false, does NOT mutate; isolates via explicit --port and re-checks /health",
204
+ "type": "output_check"
205
+ }
206
+ ]
207
+ },
208
+ {
209
+ "id": 9,
210
+ "name": "current-composite-routing",
211
+ "prompt": "Create a markdown note, a bar chart, and an Excalidraw diagram on the canvas.",
212
+ "expected_output": "Uses current composites: canvas_node {action:add} for markdown, canvas_render {action:add-graph} for the chart, canvas_app {action:diagram} for Excalidraw. Avoids deprecated single-purpose tools (canvas_add_node/canvas_add_diagram/canvas_build_web_artifact/canvas_open_mcp_app).",
213
+ "assertions": [
214
+ {
215
+ "name": "uses-canvas-node",
216
+ "description": "Creates the markdown node via canvas_node {action:add}",
217
+ "type": "output_check"
218
+ },
219
+ {
220
+ "name": "uses-canvas-render",
221
+ "description": "Creates the chart via canvas_render {action:add-graph}",
222
+ "type": "output_check"
223
+ },
224
+ {
225
+ "name": "uses-canvas-app",
226
+ "description": "Creates the Excalidraw diagram via canvas_app {action:diagram}",
227
+ "type": "output_check"
228
+ },
229
+ {
230
+ "name": "avoids-deprecated",
231
+ "description": "Does not call deprecated standalones (canvas_add_node/canvas_add_diagram/etc.)",
232
+ "type": "output_check"
233
+ }
234
+ ]
235
+ },
236
+ {
237
+ "id": 10,
238
+ "name": "existing-board-extension",
239
+ "prompt": "Add two more findings to the investigation board that's already on the canvas.",
240
+ "expected_output": "Searches the existing board first, snapshots before extending, adds new nodes without duplicating existing titles, and validates the final layout for collisions.",
241
+ "assertions": [
242
+ {
243
+ "name": "searches-first",
244
+ "description": "Searches existing nodes (canvas_query {action:search}) before adding",
245
+ "type": "output_check"
246
+ },
247
+ {
248
+ "name": "snapshots-before",
249
+ "description": "Snapshots before extending the board",
250
+ "type": "output_check"
251
+ },
252
+ {
253
+ "name": "no-duplicates",
254
+ "description": "Adds new nodes without duplicating existing node titles",
255
+ "type": "output_check"
256
+ },
257
+ {
258
+ "name": "validates-layout",
259
+ "description": "Runs canvas_query {action:validate} (or arrange+validate) on the result",
260
+ "type": "output_check"
261
+ }
262
+ ]
263
+ },
264
+ {
265
+ "id": 11,
266
+ "name": "context-pin-workflow",
267
+ "prompt": "Pin the auth design node so I (the agent) keep it in context, then confirm it's pinned.",
268
+ "expected_output": "Pins via a verified path (canvas_pin_nodes, the CLI pin, or browser/right-click 'Pin as context'), then reads canvas://pinned-context to confirm; does not claim a broken UI action succeeded.",
269
+ "assertions": [
270
+ {
271
+ "name": "pins-via-verified-path",
272
+ "description": "Pins via canvas_pin_nodes / CLI pin / 'Pin as context' — not an unverified action",
273
+ "type": "output_check"
274
+ },
275
+ {
276
+ "name": "reads-pinned-context",
277
+ "description": "Reads canvas://pinned-context (or canvas_ax_state) to confirm the pin",
278
+ "type": "output_check"
279
+ },
280
+ {
281
+ "name": "confirms-count",
282
+ "description": "Confirms the pin is reflected (context count / pinned set) rather than assuming success",
283
+ "type": "output_check"
284
+ }
285
+ ]
286
+ },
287
+ {
288
+ "id": 12,
289
+ "name": "status-node-cleanup",
290
+ "prompt": "Remove the obsolete 'Deploy: pending' status node from the board.",
291
+ "expected_output": "Removes the status node via canvas_node {action:remove} (status nodes are removable like any other type) and verifies removal; undo remains available.",
292
+ "assertions": [
293
+ {
294
+ "name": "removes-status-node",
295
+ "description": "Removes the status node via canvas_node {action:remove}",
296
+ "type": "output_check"
297
+ },
298
+ {
299
+ "name": "verifies-removal",
300
+ "description": "Verifies the node is gone from the layout",
301
+ "type": "output_check"
302
+ },
303
+ {
304
+ "name": "preserves-undo",
305
+ "description": "Notes/relies on undo (canvas_history) remaining available",
306
+ "type": "output_check"
307
+ }
308
+ ]
309
+ },
310
+ {
311
+ "id": 13,
312
+ "name": "ax-html-control-surface",
313
+ "prompt": "Build an AX control surface as an HTML node with a button that creates a work item and a button that steers me.",
314
+ "expected_output": "Creates an opted-in html node (axCapabilities.enabled) using the blessed recipe: awaits window.PMX_AX.emit, reflects live state via pmx-ax-update, avoids sandbox-blocked localStorage, and labels the steer button as queued for the next turn (not immediate wake).",
315
+ "assertions": [
316
+ {
317
+ "name": "opts-in-ax",
318
+ "description": "Creates the html node with axCapabilities.enabled = true",
319
+ "type": "output_check"
320
+ },
321
+ {
322
+ "name": "awaits-emit",
323
+ "description": "Uses window.PMX_AX.emit with await (not fire-and-forget)",
324
+ "type": "output_check"
325
+ },
326
+ {
327
+ "name": "sandbox-safe",
328
+ "description": "Avoids localStorage/sessionStorage/cookies in the sandboxed iframe",
329
+ "type": "output_check"
330
+ },
331
+ {
332
+ "name": "labels-steer-queued",
333
+ "description": "Labels steering as queued for the next turn, not immediate agent wake",
334
+ "type": "output_check"
335
+ }
336
+ ]
337
+ },
338
+ {
339
+ "id": 14,
340
+ "name": "ax-delivery-lifecycle",
341
+ "prompt": "There are pending steering messages from the board. Pick them up and act on them.",
342
+ "expected_output": "Reads compact context (newest-first pendingSteering + totalPending/omittedPending) and/or claims via canvas_ax_delivery {action:claim}; acts, then marks delivered. Understands direct delivery stays FIFO oldest-first.",
343
+ "assertions": [
344
+ {
345
+ "name": "reads-newest-first",
346
+ "description": "Uses compact context (newest-first + counts) or canvas_ax_delivery claim to find fresh steering",
347
+ "type": "output_check"
348
+ },
349
+ {
350
+ "name": "acts-then-marks",
351
+ "description": "Acts on the steering then marks it delivered (canvas_ax_delivery {action:mark})",
352
+ "type": "output_check"
353
+ },
354
+ {
355
+ "name": "understands-fifo",
356
+ "description": "Recognizes the direct delivery queue is FIFO oldest-first, distinct from compact context",
357
+ "type": "output_check"
358
+ }
359
+ ]
360
+ },
361
+ {
362
+ "id": 15,
363
+ "name": "final-cleanup-and-validate",
364
+ "prompt": "We're done with the test board. Clean up the temporary nodes and leave the canvas as we found it.",
365
+ "expected_output": "Removes retry/test fixtures, runs canvas_query {action:validate} to confirm no accidental collisions/dangling edges, and restores the baseline snapshot.",
366
+ "assertions": [
367
+ {
368
+ "name": "removes-fixtures",
369
+ "description": "Removes the temporary/retry fixture nodes",
370
+ "type": "output_check"
371
+ },
372
+ {
373
+ "name": "validates-final",
374
+ "description": "Validates the final layout (canvas_query {action:validate})",
375
+ "type": "output_check"
376
+ },
377
+ {
378
+ "name": "restores-baseline",
379
+ "description": "Restores the baseline snapshot to leave the canvas as found",
380
+ "type": "output_check"
381
+ }
382
+ ]
184
383
  }
185
384
  ]
186
385
  }
@@ -0,0 +1,93 @@
1
+ # AX HTML Control Surface — a blessed, copy-paste-safe recipe
2
+
3
+ A canvas `html` node can be a live **AX control surface**: it emits AX interactions
4
+ (create work, request approval, steer the agent, …) and reflects the current AX board
5
+ back, all from inside the sandboxed iframe. The bridge is already injected for you —
6
+ you do **not** need to read `axToken` from the URL or hand-post `pmx-canvas-ax`
7
+ messages. Just opt the node in and use `window.PMX_AX`.
8
+
9
+ ## Opt in
10
+
11
+ ```js
12
+ // MCP / SDK
13
+ canvas_add_html_node({
14
+ title: "AX Control Room",
15
+ html: "<!-- see below -->",
16
+ axCapabilities: { enabled: true, allowed: ["ax.work.create", "ax.steer"] },
17
+ });
18
+ // HTTP: POST /api/canvas/node { type:"html", title, html, axCapabilities } (top-level html + axCapabilities both accepted)
19
+ ```
20
+
21
+ Without `axCapabilities.enabled = true`, `window.PMX_AX` is **not** injected — the node
22
+ renders but can't emit. `allowed` narrows what it may emit (never escalates the type's
23
+ ceiling). Flip an existing node on with `canvas_node({ action: "update", id, axCapabilities: { enabled: true, allowed: [...] } })`.
24
+
25
+ ## Three footguns (this is why a hand-rolled node looks "inert")
26
+
27
+ 1. **The iframe is sandboxed opaque-origin** (no `allow-same-origin`). `localStorage`,
28
+ `sessionStorage`, and `document.cookie` **throw** — and an uncaught throw at script
29
+ start aborts the whole script, so the node renders blank/inert. Keep state in plain
30
+ JS variables (or `window.PMX_AX.state`); if you must touch storage, wrap it in
31
+ `try/catch`.
32
+ 2. **`window.PMX_AX.emit(type, payload)` is async.** It returns a Promise that resolves
33
+ with the result once the canvas acks it. `await` it (or `.then` / `window.PMX_AX.on('ack', cb)`).
34
+ Reading a result synchronously gets you `undefined`.
35
+ 3. **`ax.steer` is recorded, not delivered.** A successful emit (`{ ok: true }`) means
36
+ the steer was **queued** on the timeline — it does **not** wake or notify the active
37
+ agent. A cooperating host adapter must drain the delivery queue and call its native
38
+ send to create a visible turn (host-owned). Otherwise the steer is picked up on the
39
+ human's next turn. Label steering buttons honestly ("Queued for the agent's next turn").
40
+
41
+ ## Drop-in template (work + steer, with ack + live reflection)
42
+
43
+ ```html
44
+ <style>
45
+ body { font: 13px system-ui; margin: 0; padding: 12px; }
46
+ button { cursor: pointer; }
47
+ #s { margin-left: 8px; color: #6b7280; }
48
+ ul { padding-left: 18px; }
49
+ </style>
50
+ <button id="add">+ Work item</button>
51
+ <button id="steer">Steer agent</button>
52
+ <span id="s"></span>
53
+ <ul id="q"></ul>
54
+ <script>
55
+ // In-memory only — NO localStorage/sessionStorage/cookies (sandboxed: they throw).
56
+ const $ = (id) => document.getElementById(id);
57
+ const flash = (msg) => { $('s').textContent = msg; setTimeout(() => { $('s').textContent = ''; }, 1500); };
58
+
59
+ function render(state) {
60
+ const items = (state && state.workItems) || [];
61
+ $('q').innerHTML = items.map((w) => '<li>[' + w.status + '] ' + w.title + '</li>').join('');
62
+ }
63
+
64
+ $('add').onclick = async () => {
65
+ const r = await window.PMX_AX.emit('ax.work.create', { title: 'New task' });
66
+ flash(r && r.ok ? 'queued ✓' : ('failed: ' + (r && (r.error || r.code))));
67
+ };
68
+ $('steer').onclick = async () => {
69
+ const r = await window.PMX_AX.emit('ax.steer', { message: 'Prioritize the auth refactor' });
70
+ // Honest: recorded/queued, NOT delivered to the live agent.
71
+ flash(r && r.ok ? 'queued for next turn ✓' : 'failed');
72
+ };
73
+
74
+ // Reflect: seeded once at load, then live via the pmx-ax-update event.
75
+ render(window.PMX_AX && window.PMX_AX.state);
76
+ window.addEventListener('pmx-ax-update', (e) => render(e.detail));
77
+ </script>
78
+ ```
79
+
80
+ ## API surface (injected by PMX when opted in)
81
+
82
+ - `window.PMX_AX.emit(type, payload) → Promise<{ ok, primitive?, status?, code?, error? }>`
83
+ — `ok:true` on accept; `ok:false` with `code`/`error` on reject; falls back to an
84
+ `ax-ack-timeout` result after 10s so `await` never hangs.
85
+ - `window.PMX_AX.on('ack', (result, interaction) => …)` — also fires a `pmx-ax-ack`
86
+ CustomEvent; use instead of `await` if you prefer a listener.
87
+ - `window.PMX_AX.state` — compact board snapshot `{ focus, workItems, approvalGates,
88
+ reviewAnnotations, elicitations, modeRequests, policy }` (human review text redacted).
89
+ - `pmx-ax-update` window event — fires with the fresh snapshot on every AX change.
90
+
91
+ Allowed `type`s are gated per node capability (see the node-capability matrix in
92
+ `SKILL.md`). Emits are clamped to the surface's own node; the server re-validates every
93
+ interaction — the bridge is convenience, not a trust boundary.