pmx-canvas 0.3.4 → 0.4.1

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 (51) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/Readme.md +16 -3
  3. package/dist/canvas/index.js +80 -80
  4. package/dist/types/client/index.d.ts +1 -1
  5. package/dist/types/client/nodes/ExtAppFrame.d.ts +10 -0
  6. package/dist/types/client/nodes/iframe-document-url.d.ts +1 -0
  7. package/dist/types/client/nodes/use-surface-frame.d.ts +13 -0
  8. package/dist/types/client/state/canvas-store.d.ts +1 -0
  9. package/dist/types/client/state/iframe-mode.d.ts +25 -0
  10. package/dist/types/client/state/node-factory.d.ts +29 -0
  11. package/dist/types/client/uuid-polyfill.d.ts +0 -0
  12. package/dist/types/server/canvas-db.d.ts +0 -1
  13. package/dist/types/server/canvas-state.d.ts +14 -15
  14. package/dist/types/server/operations/composites.d.ts +6 -21
  15. package/dist/types/server/server.d.ts +1 -0
  16. package/docs/RELEASE.md +10 -4
  17. package/docs/cli.md +5 -0
  18. package/docs/environment.md +2 -2
  19. package/docs/http-api.md +47 -1
  20. package/docs/mcp.md +21 -30
  21. package/docs/plans/plan-009-tech-debt-backlog.md +22 -13
  22. package/docs/screenshot.png +0 -0
  23. package/docs/tech-debt-assessment-2026-06.md +5 -5
  24. package/package.json +7 -7
  25. package/skills/pmx-canvas/SKILL.md +36 -10
  26. package/skills/pmx-canvas/evals/evals.json +2 -2
  27. package/skills/pmx-canvas/references/full-reference.md +17 -15
  28. package/skills/pmx-canvas-testing/SKILL.md +3 -0
  29. package/src/cli/commands/history.ts +1 -1
  30. package/src/cli/commands/nodes.ts +31 -1
  31. package/src/cli/index.ts +8 -2
  32. package/src/cli/shared.ts +1 -0
  33. package/src/client/index.tsx +7 -0
  34. package/src/client/nodes/ExtAppFrame.tsx +50 -2
  35. package/src/client/nodes/HtmlNode.tsx +5 -1
  36. package/src/client/nodes/McpAppNode.tsx +5 -1
  37. package/src/client/nodes/iframe-document-url.ts +62 -9
  38. package/src/client/nodes/use-surface-frame.ts +53 -0
  39. package/src/client/state/canvas-store.ts +5 -0
  40. package/src/client/state/iframe-mode.ts +103 -0
  41. package/src/client/state/node-factory.ts +72 -0
  42. package/src/client/state/sse-bridge.ts +212 -86
  43. package/src/client/uuid-polyfill.ts +14 -0
  44. package/src/server/canvas-db.ts +0 -8
  45. package/src/server/canvas-schema.ts +2 -8
  46. package/src/server/canvas-state.ts +51 -151
  47. package/src/server/index.ts +3 -9
  48. package/src/server/operations/composites.ts +24 -21
  49. package/src/server/operations/ops/snapshots.ts +6 -7
  50. package/src/server/operations/registry.ts +4 -1
  51. package/src/server/server.ts +194 -75
@@ -1 +1 @@
1
- export {};
1
+ import './uuid-polyfill';
@@ -51,6 +51,16 @@ export declare function buildExtAppAxBridgeScript(axToken: string, nodeId: strin
51
51
  * the host also checks event.source against the live iframe.
52
52
  */
53
53
  export declare function buildExtAppBootBeaconScript(frameToken: string, nodeId: string): string;
54
+ /**
55
+ * Base styles injected into every ext-app document (Finding Q, 0.3.4 report):
56
+ * apps aspect-fit their drawn surface to the frame width, and on narrow/tall
57
+ * tiles the leftover region shows the app document's own body background —
58
+ * pure black for the hosted Excalidraw bundle, in every engine. Making the
59
+ * app document transparent lets the host iframe's theme background
60
+ * (var(--c-panel)) show through the letterbox instead. Background only — app
61
+ * content, its drawn scene, and its own foreground styling are untouched.
62
+ */
63
+ export declare function buildExtAppSurfaceBaseStyles(): string;
54
64
  export declare function injectExtAppAxBridgeScript(html: string, axBridgeScript: string): string;
55
65
  export declare function resolveExtAppContainerDimensions(target: ExtAppHostDimensionsTarget | null | undefined, fallback: {
56
66
  width: number;
@@ -2,6 +2,7 @@ export declare function createIframeDocumentUrl(html: string, sandbox: string):
2
2
  export declare function useIframeDocument(html: string, sandbox: string): {
3
3
  attributes: {
4
4
  src?: string;
5
+ srcdoc?: string;
5
6
  };
6
7
  ready: boolean;
7
8
  key: string;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Attributes for a surface iframe, honoring the boot-wide iframe mode
3
+ * (state/iframe-mode.ts): `src` by default, fetch() + `srcdoc` when src-URL
4
+ * iframes are blocked by the embedding context (nested-iframe hosts like Amp
5
+ * orb portals). Cross-origin URLs always stay `src` — they cannot be fetched
6
+ * from here. While the probe is pending, same-origin frames stay empty rather
7
+ * than loading a src that may show the broken placeholder; a failed srcdoc
8
+ * fetch falls back to `src` (no worse than before).
9
+ */
10
+ export declare function useSurfaceFrame(url: string): {
11
+ src?: string;
12
+ srcdoc?: string;
13
+ };
@@ -5,6 +5,7 @@ export declare const edges: import("@preact/signals-core").Signal<Map<string, Ca
5
5
  export declare const annotations: import("@preact/signals-core").Signal<Map<string, CanvasAnnotation>>;
6
6
  export declare const activeNodeId: import("@preact/signals-core").Signal<string | null>;
7
7
  export declare const connectionStatus: import("@preact/signals-core").Signal<ConnectionStatus>;
8
+ export declare const workbenchConnectionEpoch: import("@preact/signals-core").Signal<number>;
8
9
  export declare const sessionId: import("@preact/signals-core").Signal<string>;
9
10
  export declare const traceEnabled: import("@preact/signals-core").Signal<boolean>;
10
11
  export declare const canvasTheme: import("@preact/signals-core").Signal<string>;
@@ -0,0 +1,25 @@
1
+ export type IframeMode = 'src' | 'srcdoc';
2
+ export declare const IFRAME_PROBE_PATH = "/api/canvas/iframe-probe";
3
+ /** Boot-wide iframe transport mode; null until the probe (or override) resolves. */
4
+ export declare const iframeMode: import("@preact/signals-core").Signal<IframeMode | null>;
5
+ export declare function forcedIframeMode(): IframeMode | null;
6
+ /**
7
+ * Probe whether `src`-URL iframes load in this embedding context. Resolves true
8
+ * when the hidden probe iframe fires `load`; false on error or timeout. The
9
+ * blocked-portal case shows its placeholder without reliably firing either
10
+ * event, so the timeout is the real signal there — and a false negative is
11
+ * safe, because srcdoc rendering works in normal hosts too.
12
+ */
13
+ export declare function probeSrcIframes(timeoutMs?: number): Promise<boolean>;
14
+ /**
15
+ * Resolve the boot-wide iframe mode once; all surface hooks share the result.
16
+ * The blocked-src condition only exists when the canvas page itself runs
17
+ * inside an iframe, so top-level documents resolve `src` synchronously — the
18
+ * normal path pays zero probe latency before surfaces mount. Only embedded
19
+ * documents run the probe (`embedded` is overridable for tests).
20
+ */
21
+ export declare function resolveIframeMode(opts?: {
22
+ embedded?: boolean;
23
+ }): Promise<IframeMode>;
24
+ /** Test hook: clear the memoized probe so each test starts unresolved. */
25
+ export declare function resetIframeModeForTests(): void;
@@ -0,0 +1,29 @@
1
+ import type { CanvasNodeState } from '../types';
2
+ export declare const DEFAULT_POSITIONS: Record<CanvasNodeState['type'], {
3
+ x: number;
4
+ y: number;
5
+ w: number;
6
+ h: number;
7
+ }> & Record<'prompt' | 'response', {
8
+ x: number;
9
+ y: number;
10
+ w: number;
11
+ h: number;
12
+ }>;
13
+ /**
14
+ * Build a canvas node with the shared client defaults. `position`/`size`
15
+ * default to the type's DEFAULT_POSITIONS entry; callers that compute
16
+ * placement (auto-placement, event-supplied geometry) pass explicit values.
17
+ * Status nodes sit at zIndex 0 (background chrome), everything else at 1.
18
+ */
19
+ export declare function makeNodeState(id: string, type: CanvasNodeState['type'], data: Record<string, unknown>, options?: {
20
+ position?: {
21
+ x: number;
22
+ y: number;
23
+ };
24
+ size?: {
25
+ width: number;
26
+ height: number;
27
+ };
28
+ dockPosition?: 'left' | 'right' | null;
29
+ }): CanvasNodeState;
File without changes
@@ -35,7 +35,6 @@ export declare function listSnapshotsFromDB(db: Database, options?: CanvasSnapsh
35
35
  export declare function deleteSnapshotFromDB(db: Database, id: string): boolean;
36
36
  export declare function writeBlobToDB(db: Database, sha256: string, jsonValue: string): number;
37
37
  export declare function readBlobFromDB(db: Database, sha256: string): string | null;
38
- export declare function hasBlobInDB(db: Database, sha256: string): boolean;
39
38
  export interface AxTimelineQuery {
40
39
  limit?: number;
41
40
  sessionId?: string;
@@ -7,7 +7,9 @@
7
7
  *
8
8
  * Persistence: canvas state auto-saves to `.pmx-canvas/canvas.db` (SQLite WAL mode)
9
9
  * in the workspace root on every mutation (debounced). Auto-loads on `loadFromDisk()`.
10
- * Legacy `.pmx-canvas/state.json` is auto-migrated on first boot.
10
+ * Legacy pre-0.2 formats (`.pmx-canvas/state.json`, `.pmx-canvas.json`,
11
+ * `.pmx-canvas-snapshots/`, loose blob files) are no longer imported as of 0.4.0 —
12
+ * the one-shot boot migration into SQLite was retired.
11
13
  */
12
14
  import { type PersistedCanvasState, type CanvasTheme, type AxTimelineQuery } from './canvas-db.js';
13
15
  import { type PmxAxActivityKind, type PmxAxElicitation, type PmxAxModeRequest, type PmxAxMode, type PmxAxCommandDescriptor, type PmxAxPolicy, type PmxAxFocusState, type PmxAxSource, type PmxAxState, type PmxAxWorkItem, type PmxAxWorkItemStatus, type PmxAxApprovalGate, type PmxAxReviewAnnotation, type PmxAxReviewKind, type PmxAxReviewSeverity, type PmxAxReviewStatus, type PmxAxReviewAnchorType, type PmxAxReviewRegion, type PmxAxEvent, type PmxAxEventKind, type PmxAxEvidence, type PmxAxEvidenceKind, type PmxAxSteeringMessage, type PmxAxHostCapability, type PmxAxTimelineSummary } from './ax-state.js';
@@ -155,7 +157,7 @@ declare class CanvasStateManager {
155
157
  private notifyChange;
156
158
  private _mutationRecorder;
157
159
  private _suppressRecordingDepth;
158
- /** Register a mutation recorder. Used by mutation-history to capture undo/redo closures. */
160
+ /** Register THE mutation recorder (single slot, last-write-wins — see note above). */
159
161
  onMutation(cb: (info: MutationRecordInfo) => void): void;
160
162
  /** Run a function with mutation recording suppressed (for undo/redo replay and computed edges). */
161
163
  withSuppressedRecording(fn: () => void): void;
@@ -171,7 +173,6 @@ declare class CanvasStateManager {
171
173
  private translateGroupChildren;
172
174
  private recomputeParentGroupBounds;
173
175
  private compactGroupChildren;
174
- private _stateFilePath;
175
176
  private _db;
176
177
  private _saveTimer;
177
178
  /** Workspace root backing persistence and workspace-relative path resolution. */
@@ -188,20 +189,9 @@ declare class CanvasStateManager {
188
189
  isBlobReference(value: unknown): value is PersistedBlobRef;
189
190
  resolveBlobReference(value: unknown): unknown;
190
191
  private externalizePersistedStateBlobs;
191
- /**
192
- * One-time migration: rename files from the pre-consolidation layout
193
- * (`.pmx-canvas.json` + `.pmx-canvas-snapshots/`) into `.pmx-canvas/`.
194
- * No-op when the new layout already exists.
195
- */
196
- private migrateLegacyLayout;
197
- /**
198
- * One-time migration: import state.json + snapshot JSON files + blob files
199
- * into the SQLite database. Renames originals to `.bak`.
200
- */
201
- private migrateJsonToSqlite;
202
192
  getWorkspaceRoot(): string;
203
193
  private emptyPersistedState;
204
- /** Load canvas state from SQLite (or legacy JSON fallback). Call once on server startup. */
194
+ /** Load canvas state from SQLite. Call once on server startup. */
205
195
  loadFromDisk(options?: LoadFromDiskOptions): boolean;
206
196
  /**
207
197
  * Whether this workspace's canvas DB already holds saved state. Used to gate
@@ -215,6 +205,15 @@ declare class CanvasStateManager {
215
205
  flushToDisk(): void;
216
206
  /** Write current state to SQLite immediately. */
217
207
  private saveToDisk;
208
+ private _lastPersistenceError;
209
+ /** Health view of state persistence: ok until a save fails, ok again after the next success. */
210
+ get persistenceHealth(): {
211
+ ok: boolean;
212
+ lastError: {
213
+ message: string;
214
+ at: string;
215
+ } | null;
216
+ };
218
217
  /** Close the SQLite database cleanly. Call on server shutdown. */
219
218
  close(): void;
220
219
  private get snapshotsDir();
@@ -16,27 +16,12 @@
16
16
  * operation (plan-005 slices 1–7 + plan-008 Wave 1) that is now reachable ONLY
17
17
  * through its composite (or `canvas_batch`).
18
18
  *
19
- * Deferred to v0.4: the `canvas_snapshot` composite (still a name collision —
20
- * see below). Its 6 legacy snapshot standalones (`canvas_snapshot`,
21
- * `canvas_list_snapshots`, `canvas_restore`, `canvas_delete_snapshot`,
22
- * `canvas_gc_snapshots`, `canvas_diff`) stay registered in v0.3.0 but are marked
23
- * deprecated (description-prefixed) per docs/api-stability.md's
24
- * deprecate-one-minor-before-removal rule. (`canvas_webview` shipped in plan-008
25
- * Wave 3 via runner injection; `canvas_app` shipped in Wave 4 — open-mcp-app /
26
- * diagram / build-artifact. Wave 5 folded the last three legacy tools
27
- * deprecate-only — NO per-action input-injection mechanism was needed:
28
- * `canvas_add_html_node` / `canvas_add_html_primitive` → `canvas_node` action
29
- * "add" (type:"html" [+ primitive]); `canvas_refresh_webpage_node` → `canvas_node`
30
- * action "update" (refresh:true). `canvas_screenshot` stays standalone — it
31
- * returns a binary image payload the composite/registry JSON wire shape does not
32
- * model.)
33
- *
34
- * Not shipped here: the `canvas_snapshot` composite (plan-006 #7). Its target
35
- * name is ALREADY a legacy standalone tool (the save-snapshot tool, op
36
- * `snapshot.save`), so it cannot be added additively without a name clash, and
37
- * repurposing `canvas_snapshot` to be action-discriminated now would break
38
- * existing callers. It lands in v0.4, in the same change that removes the 6
39
- * kept-but-deprecated legacy snapshot tools and frees the name.
19
+ * v0.4.0 completed the fold: the `canvas_snapshot` composite shipped (its name
20
+ * was held by the legacy save-snapshot standalone through v0.3.x, which — with
21
+ * the other 5 snapshot standalones — was deprecated in 0.3.0 and removed here
22
+ * per docs/api-stability.md's deprecate-one-minor-before-removal rule).
23
+ * `canvas_screenshot` stays standalone — it returns a binary image payload the
24
+ * composite/registry JSON wire shape does not model.
40
25
  *
41
26
  * This module must never import server.ts or index.ts.
42
27
  */
@@ -24,6 +24,7 @@
24
24
  * - GET /api/canvas/json-render/view?nodeId=... -> local json-render viewer
25
25
  * - POST /api/canvas/web-artifact -> build bundled HTML artifact + optional canvas node
26
26
  * - GET /api/workbench/events -> SSE event stream
27
+ * - GET /api/workbench/poll -> proxy-safe polling transport (same events, JSON)
27
28
  * - GET /api/workbench/state -> workbench state snapshot
28
29
  * - POST /api/workbench/intent -> workbench intents
29
30
  * - GET /api/workbench/webview -> Bun.WebView automation status
package/docs/RELEASE.md CHANGED
@@ -128,12 +128,18 @@ it uses `NPM_TOKEN` directly.
128
128
 
129
129
  Every release since 0.3.0 has been retested against **stale** skill mirrors:
130
130
  consumer workspaces (e.g. `personalclaude`) carry copies of the pmx-canvas
131
- skill under `.github/`, `.codex/`, `.claude/`, and `.agents/`, and testers
131
+ skills under `.github/`, `.codex/`, `.claude/`, and `.agents/`, and testers
132
132
  repeatedly had to sync them mid-cycle before results were trustworthy. After
133
133
  publishing, update the installed package in the consumer workspace and re-sync
134
- its four mirrors from
135
- `<global npm root>/pmx-canvas/skills/pmx-canvas/` so the first test pass runs
136
- against the released skill, not the previous version's guidance.
134
+ its four mirrors from `<global npm root>/pmx-canvas/skills/` so the first test
135
+ pass runs against the released skills, not the previous version's guidance.
136
+
137
+ Sync the **complete tree of every bundled skill** — `pmx-canvas` AND
138
+ `pmx-canvas-testing`, including `references/`, `evals/`, and fixtures — and
139
+ verify by diffing whole directories, not by comparing the `SKILL.md` hash
140
+ alone. The 0.3.4 and 0.4.0 cycles both hit drift where `SKILL.md` matched but
141
+ references/evals were stale, or a newly bundled skill was missing from the
142
+ mirrors entirely.
137
143
 
138
144
  ## GitHub release
139
145
 
package/docs/cli.md CHANGED
@@ -51,6 +51,11 @@ pmx-canvas node add --help --type webpage --json
51
51
  pmx-canvas external-app add --kind excalidraw --title "Diagram"
52
52
 
53
53
  pmx-canvas edge add --from-search "DVT O3 — GitOps" --to-search "deep work trend" --type relation
54
+
55
+ # Updates return a compact { ok, id, position, size, updatedFields } envelope;
56
+ # pass --full for the complete node payload (ext-app payloads can be huge).
57
+ pmx-canvas node update <node-id> --width 840 --height 620
58
+ pmx-canvas node update <node-id> --title "Renamed" --full
54
59
  ```
55
60
 
56
61
  `--from-search` / `--to-search` must each resolve to exactly one node — broad
@@ -8,7 +8,7 @@ optional; defaults are listed.
8
8
  | Variable | Read by | Effect |
9
9
  |---|---|---|
10
10
  | `PMX_WEB_CANVAS_PORT` | server, CLI `serve` | Preferred server port (default `4313`). CLI `--port` wins over it. |
11
- | `PMX_CANVAS_PORT` | agent CLI, MCP transport | Client-side default target port for CLI commands and the `--mcp` same-workspace lookup. Does not change what the server binds. |
11
+ | `PMX_CANVAS_PORT` | all entry points | Client-side target port for CLI commands and the `--mcp` lookup; as of 0.4.0 the server also falls back to it when `--port`/`PMX_WEB_CANVAS_PORT` are unset, so one var can drive the whole stack. |
12
12
  | `PMX_CANVAS_URL` | agent CLI, MCP transport | Full base URL target (wins over `PMX_CANVAS_PORT`). |
13
13
  | `PMX_CANVAS_WORKSPACE_ROOT` | server, MCP transport | Pins the workspace root for daemon binds and the MCP same-workspace lookup, overriding the launch cwd. Set it when a host spawns `--mcp` from an incidental dir. |
14
14
  | `PMX_CANVAS_ALLOW_WORKSPACE_SPLIT` | MCP transport | `1` forces a separate canvas instead of attaching to a different-workspace daemon on the preferred port. |
@@ -18,7 +18,7 @@ optional; defaults are listed.
18
18
  | Variable | Read by | Effect |
19
19
  |---|---|---|
20
20
  | `PMX_CANVAS_DB_PATH` | server | Overrides the SQLite path (default `.pmx-canvas/canvas.db`). |
21
- | `PMX_CANVAS_STATE_FILE` | server | Legacy JSON state path (back-compat; a `.db` value is treated as the DB path). |
21
+ | `PMX_CANVAS_STATE_FILE` | server | `.db`-path alias only as of 0.4.0 (non-`.db` values are ignored with a boot warning; the pre-0.2 JSON import was removed). |
22
22
  | `PMX_CANVAS_BLOB_THRESHOLD_BYTES` | server | Node payloads above this many JSON bytes are stored as blobs (default `2048`). |
23
23
 
24
24
  ## Server behavior
package/docs/http-api.md CHANGED
@@ -5,7 +5,9 @@ any language. Default base URL: `http://localhost:4313`.
5
5
 
6
6
  A non-empty request body that is not valid JSON returns
7
7
  `400 { "ok": false, "error": "Malformed JSON body." }` on every route; empty
8
- bodies are treated as an empty request.
8
+ bodies are treated as an empty request. As of 0.4.0 every error response is a
9
+ JSON envelope of that same shape (`{ ok: false, error }`, status unchanged) —
10
+ no plain-text errors remain.
9
11
 
10
12
  ## Canvas state
11
13
 
@@ -321,6 +323,50 @@ curl -N http://localhost:4313/api/workbench/events
321
323
  The browser, the CLI `watch` command, and the MCP resource notifications
322
324
  all consume this stream. Auto-reconnect with exponential backoff.
323
325
 
326
+ ## Polling transport (proxy-safe)
327
+
328
+ Some proxies (e.g. the Amp orb portal) buffer streaming responses and only
329
+ flush them on close, so the SSE stream above delivers nothing. The polling
330
+ transport returns the same events as short-lived JSON responses that pass
331
+ through any proxy:
332
+
333
+ ```bash
334
+ # No `since`: a full connect snapshot (same events an SSE connect sends) + the
335
+ # current cursor. With `since=<seq>`: only events after that cursor.
336
+ curl "http://localhost:4313/api/workbench/poll"
337
+ curl "http://localhost:4313/api/workbench/poll?since=42"
338
+ ```
339
+
340
+ Responses are `{ ok, transport: "poll", snapshot, seq, events }` — poll again
341
+ with `since=<seq>` every couple of seconds. A `since` from a previous server
342
+ run or one that fell off the bounded event ring recovers with a fresh
343
+ snapshot. The browser client uses this automatically: if the SSE stream
344
+ produces no event within 3 seconds of connecting, it switches to polling.
345
+ Force a transport with `/workbench?transport=poll` (or `transport=sse` to
346
+ disable the fallback).
347
+
348
+ ## Iframe embed probe (nested-iframe hosts)
349
+
350
+ Some hosts embed the canvas page itself inside an iframe (the Amp orb portal
351
+ does), and Chrome then refuses to load child iframes from `src` URLs — even
352
+ same-origin ones — so iframe-backed nodes would show a broken placeholder.
353
+ When (and only when) the canvas page detects it is embedded, it probes the
354
+ real behavior at boot by loading a hidden iframe against:
355
+
356
+ ```bash
357
+ curl http://localhost:4313/api/canvas/iframe-probe
358
+ ```
359
+
360
+ If that iframe never loads, the client switches every same-origin surface
361
+ (HTML nodes, graph/json-render viewers, frame documents) to `fetch()` +
362
+ `srcdoc` inline rendering. External app URLs stay on `src` — they cannot be
363
+ fetched cross-origin. Force a mode with `/workbench?iframe-mode=srcdoc` (or
364
+ `iframe-mode=src`).
365
+
366
+ `/api/canvas/frame-documents/:id` also answers `HEAD`, which the browser uses
367
+ after every reconnect to detect frame documents lost to a server restart and
368
+ re-mint them automatically (Finding S).
369
+
324
370
  ## Time travel
325
371
 
326
372
  ```bash
package/docs/mcp.md CHANGED
@@ -1,19 +1,17 @@
1
1
  # MCP reference
2
2
 
3
- PMX Canvas ships an MCP stdio server with **27 tools** + **14 core resources**,
3
+ PMX Canvas ships an MCP stdio server with **22 tools** + **14 core resources**,
4
4
  plus per-skill resources at `canvas://skills/<name>`. The server emits
5
5
  `notifications/resources/updated` when canvas state changes — humans pin
6
6
  nodes in the browser, agents are notified immediately.
7
7
 
8
- > **Consolidation completed in v0.3.0 (plan-006/008).** The MCP surface shrank
9
- > from 84 tools to 27: 15 action-discriminated **composites** (recommended —
10
- > see below) plus 12 standalone tools. The 57 legacy single-purpose tools that
11
- > the composites replaced were removed in v0.3.0 per
12
- > [`api-stability.md`](api-stability.md); their descriptions carried
13
- > `Deprecated:` prefixes for one minor before removal. The 6 snapshot tools
14
- > remain registered as deprecated standalones — they fold into a
15
- > `canvas_snapshot` composite in v0.4 (see [Standalone tools](#standalone-tools)
16
- > below). **Prefer the composites.**
8
+ > **Consolidation complete (plan-006/008).** The MCP surface shrank from 84
9
+ > tools to 22: 16 action-discriminated **composites** (recommended — see
10
+ > below) plus 6 standalone tools. The 57 legacy single-purpose tools the
11
+ > composites replaced were removed in v0.3.0, and v0.4.0 finished the fold by
12
+ > shipping the `canvas_snapshot` composite and removing the 6 deprecated
13
+ > snapshot standalones — each step per [`api-stability.md`](api-stability.md)'s
14
+ > deprecate-one-minor-before-removal rule. **Prefer the composites.**
17
15
 
18
16
  ## Connect
19
17
 
@@ -54,6 +52,7 @@ its `action` to the same operation the legacy tool used, so results are identica
54
52
  | `canvas_ax_timeline` | `read` · `record-event` · `add-evidence` · `send-steering` | `canvas_get_ax_timeline`, `canvas_record_ax_event`, `canvas_add_evidence`, `canvas_send_steering` |
55
53
  | `canvas_ax_delivery` | `claim` · `mark` | `canvas_claim_ax_delivery`, `canvas_mark_ax_delivery` |
56
54
  | `canvas_intent` | `signal` · `update` · `clear` | _(new — Ghost Cursor of Intent; no legacy standalone tool)_ |
55
+ | `canvas_snapshot` | `save` · `list` · `restore` · `delete` · `gc` · `diff` | `canvas_snapshot` (legacy save tool), `canvas_list_snapshots`, `canvas_restore`, `canvas_delete_snapshot`, `canvas_gc_snapshots`, `canvas_diff` — removed in v0.4.0 after one deprecated minor |
57
56
 
58
57
  ### `canvas_intent` — Ghost Cursor of Intent
59
58
 
@@ -89,20 +88,16 @@ tools). `canvas_screenshot` also stays standalone — it returns a binary image
89
88
  the composite/registry JSON wire shape does not model. (Wave 5 folded
90
89
  `canvas_refresh_webpage_node` → `canvas_node { action: "update", refresh: true }` after
91
90
  fixing `node.update`'s `formatResult` to surface a FAILED refresh as `isError` +
92
- `{ ok:false, error }` instead of masking it as a false `{ ok:true }`.) The 6
93
- snapshot tools are the one deferred fold: a `canvas_snapshot` composite is
94
- planned for v0.4 (the name collides with the legacy `canvas_snapshot` save
95
- tool, and [`api-stability.md`](api-stability.md) requires deprecating a tool
96
- for one minor before removing it) — they stay registered as deprecated
97
- standalones until then.
91
+ `{ ok:false, error }` instead of masking it as a false `{ ok:true }`.) The snapshot
92
+ fold completed in v0.4.0: `canvas_snapshot { action: "save" | "list" | "restore" |
93
+ "delete" | "gc" | "diff" }` replaced the 6 deprecated snapshot standalones.
98
94
 
99
95
  ## Standalone tools
100
96
 
101
- 12 tools that intentionally stay outside the composites — either because
102
- folding them would hurt (distinct callers, binary payloads, trust-boundary or
103
- execution-intent semantics), or because they're mid-deprecation pending a
104
- v0.4 composite. See [Migration reference](#migration-reference) below for the
105
- 57 tools these composites replaced.
97
+ 6 tools that intentionally stay outside the composites — folding them would
98
+ hurt (distinct callers, binary payloads, trust-boundary or execution-intent
99
+ semantics). See [Migration reference](#migration-reference) below for the
100
+ legacy tools the composites replaced.
106
101
 
107
102
  | Tool | Description |
108
103
  |------|-------------|
@@ -112,12 +107,6 @@ v0.4 composite. See [Migration reference](#migration-reference) below for the
112
107
  | `canvas_ax_interaction` | Submit one capability-gated AX interaction envelope (`{ type, sourceNodeId, payload }`) that maps onto an AX operation; the server re-validates and clamps sandboxed surfaces to their own node |
113
108
  | `canvas_ingest_activity` | Ingest a harness-forwarded agent activity (tool/session event); the board auto-reacts with kind-driven, overridable defaults (failure → work item + review + evidence; `tool-result`+success → evidence). Makes AX bidirectional |
114
109
  | `canvas_screenshot` | Capture a screenshot from the active workbench automation session |
115
- | `canvas_snapshot` | **Deprecated** → folds into the `canvas_snapshot` composite in v0.4 (actions `save\|list\|restore\|delete\|gc\|diff`). Save current canvas as a named snapshot |
116
- | `canvas_list_snapshots` | **Deprecated** → folds into the `canvas_snapshot` composite in v0.4 (actions `save\|list\|restore\|delete\|gc\|diff`). List saved snapshots, bounded to the newest 20 by default |
117
- | `canvas_restore` | **Deprecated** → folds into the `canvas_snapshot` composite in v0.4 (actions `save\|list\|restore\|delete\|gc\|diff`). Restore canvas from a saved snapshot |
118
- | `canvas_delete_snapshot` | **Deprecated** → folds into the `canvas_snapshot` composite in v0.4 (actions `save\|list\|restore\|delete\|gc\|diff`). Delete a saved snapshot |
119
- | `canvas_gc_snapshots` | **Deprecated** → folds into the `canvas_snapshot` composite in v0.4 (actions `save\|list\|restore\|delete\|gc\|diff`). Delete old snapshots while keeping the newest N |
120
- | `canvas_diff` | **Deprecated** → folds into the `canvas_snapshot` composite in v0.4 (actions `save\|list\|restore\|delete\|gc\|diff`). Compare current canvas vs a saved snapshot |
121
110
 
122
111
  `canvas_node { action: "add", type: "html" }` accepts optional `summary`, `agentSummary`,
123
112
  `embeddedNodeIds`, and `embeddedUrls`. PMX also derives a bounded text summary from visible
@@ -125,7 +114,9 @@ HTML, so rich HTML nodes stay searchable and readable in pinned/spatial context.
125
114
 
126
115
  ## Migration reference
127
116
 
128
- The 57 legacy single-purpose tools below were removed in v0.3.0. Each row is
117
+ The 57 legacy single-purpose tools below were removed in v0.3.0, and v0.4.0
118
+ additionally removed the 6 deprecated snapshot standalones (see the
119
+ `canvas_snapshot` composite row above for their replacements). Each row is
129
120
  the composite call that replaces it — kept as a lookup table for anyone
130
121
  migrating an older integration.
131
122
 
@@ -304,5 +295,5 @@ CLI and MCP are kept aligned for the main canvas operations: node and edge
304
295
  creation, graph/json-render/html/html-primitive nodes, web artifacts, external apps, groups,
305
296
  batch builds, layout validation, snapshots, search, focus, pins, undo/redo,
306
297
  semantic watch streams, WebView automation, and daemon/server control where
307
- it applies. A few agent-native capabilities — resource subscriptions and
308
- `canvas_diff` — remain MCP-only.
298
+ it applies. A few agent-native capabilities — such as resource
299
+ subscriptions — remain MCP-only.
@@ -11,7 +11,8 @@ Each item is tagged by shape: **project** (multi-day, own plan/PR), **decision**
11
11
  The remaining items were re-analyzed with pros/cons and tiered; the maintainer approved this ordering:
12
12
 
13
13
  - **Tier 1 (do):** CLI raw fetches → operation invoker **together with** the H3 parser/module split (same file, one project); H7 test coverage **as a prerequisite** for any client refactor; M2 AX-bridge dedup (first client task after H7); the M8 flake subset (mock the Excalidraw network tests, share `getAvailablePort`, Playwright `retries`/`forbidOnly`, `e2e-cli` into CI). *C1b done (2026-07-08):* all 60 raw `api()` calls migrated onto the invoker (9-agent verified mapping; `webartifact.build` gained `errorBodyAsResult`; batch failure prints the envelope + exits 1; the `api()` helper is deleted; two deliberate raw `fetch` sites remain — binary screenshot, SSE watch). *H3 module split done (2026-07-09):* `agent.ts` is a 227-line entry over `src/cli/shared.ts` (cycle-free core: registry, parser, transport) + 11 `src/cli/commands/*` domain modules, moved byte-identically; ax-parity reads the whole cli tree. **Parser sub-item reshaped:** `parseFlags`/`BOOL_FLAGS` is now single-sited in shared.ts; `index.ts` keeps its intentionally tiny fixed-flag launcher reader (different domain — server launch, not agent commands). The spec-driven per-command flag parser (`defineCommand`) remains the ideal but is a redesign of all 83 command surfaces — deferred to a future CLI UX change, not worth manufactured churn now.
14
- - **Tier 2 (reshaped):** the PmxCanvas→executeOperation collapse is **not** mechanical — `executeOperation` is async while 57/76 SDK methods are sync with typed returns, so a full collapse is a breaking SDK change. Verdict: migrate only the already-async/void methods now; decide the full async-SDK question as a **v0.4 breaking item** with the `canvas_snapshot` composite. Post-registry the sync methods are thin presentation over the shared cores (single logic site) — not logic duplication. H5 (sse-bridge factory) proceeds **after H7**. M6 gets the middle path: runtime per-action required-field errors, no `oneOf` schema break (many MCP hosts handle `oneOf` poorly). M1 gets the minimum four (minify, devtools→devDeps, lazy-load, replace the python mtime gate); the React-stack decision stays open.
14
+ - **Tier 2 — async-SDK question DECIDED NO (2026-08-03, v0.4.0):** the sync `PmxCanvas` facade is the contract. Post-registry the 57 sync methods are thin presentation over the shared operation cores (single logic site — the tier's own analysis), so forcing them async would break every SDK consumer to remove zero duplication. The already-async/void methods dispatch through `executeOperation`; the rest stay sync by design. No further collapse is planned.
15
+ - **Tier 2 (original analysis):** the PmxCanvas→executeOperation collapse is **not** mechanical — `executeOperation` is async while 57/76 SDK methods are sync with typed returns, so a full collapse is a breaking SDK change. Verdict: migrate only the already-async/void methods now; decide the full async-SDK question as a **v0.4 breaking item** with the `canvas_snapshot` composite. Post-registry the sync methods are thin presentation over the shared cores (single logic site) — not logic duplication. H5 (sse-bridge factory) proceeds **after H7**. M6 gets the middle path: runtime per-action required-field errors, no `oneOf` schema break (many MCP hosts handle `oneOf` poorly). M1 gets the minimum four (minify, devtools→devDeps, lazy-load, replace the python mtime gate); the React-stack decision stays open.
15
16
  - **Tier 3 (cheap batch):** CI check that CLAUDE.md/AGENTS.md stay identical (instead of doc generation); document `PMX_DATA` + the `PMX_CANVAS_JSON_RENDER_*`/`PMX_MCP_APP_HOST_*` env families; publish.yml verifies the tag SHA passed test.yml; `@types/turndown` → devDependencies; L1 dead `ContextPinHud`; L4 one-shot migration marker; L6 timer-leak fixes.
16
17
  - **Tier 4 (closed as by-design / parked):** the six server-coupled routes (nothing to dedup — injection machinery for single-copy handlers isn't worth it); M3 ExtAppFrame refactor (the timing hacks are load-bearing WebKit fixes — document invariants, enter only when a feature forces it); M7 notification fan-out, L2, L3, `noUncheckedIndexedAccess` (parked); M10 packaging closed after the `@types` one-liner (`bin`→`src` under Bun and shipped `dist/` are both load-bearing by design).
17
18
 
@@ -19,10 +20,10 @@ The remaining items were re-analyzed with pros/cons and tiered; the maintainer a
19
20
 
20
21
  | Item | Source | What's open | Shape |
21
22
  |---|---|---|---|
22
- | Complete the registry migration | C1 | Fold the remaining hand-written routes in `server.ts` into operations; collapse the 76-method `PmxCanvas` onto `executeOperation`; migrate the CLI's 60 raw `fetch` calls onto the operation invoker. **Slice 1 done post-0.3.0:** the 7 `/api/ext-app/*` routes moved to `ops/ext-app.ts` (~290 lines out of `server.ts`, the 5 clone handlers — old M5 — collapsed to one parametrized definition; wire envelopes byte-identical, no new MCP tools). **Slice 2 done:** the 15 ax wire routes (8 GET lists + `ax/context` + `ax/surface-snapshot` + `pinned-context` + `code-graph` + `ax/activity` + `ax/interaction` + legacy `PATCH /api/canvas/ax`) moved to `ops/ax-read.ts` (~370 more lines out of `server.ts`); `open-external` stays hand-written (coupled to the live server port + system browser opener). **Slice 3 done:** theme GET/POST, canvas update, viewport, annotation POST, and node `:id/refresh` moved to the registry (`ops/canvas-wire.ts`, plus additions to `ops/viewport.ts`, `ops/annotation.ts`, `ops/nodes.ts`); the two dispatch gates unified into one `/api/` gate; `server.ts` is at ~2,980 lines (from 3,873 at the audit). **Remaining routes (~20)** are the deliberate poor fits (SSE, binary, HTML/static, file bodies) plus four server-coupled handlers: `prompt` + workbench `state`/`open`/`intent` (module-local `primaryWorkbench*` session state), `render` (Excalidraw URL-normalizer chain), `open-external` (live port + system browser). Those need state injection like the webview runner — a deliberate follow-up, not mechanical. After routes, the bigger halves remain: `PmxCanvas` (76 methods) onto `executeOperation`, and the CLI's raw fetches onto the invoker. | project (in progress) |
23
- | One HTTP envelope | M4 residual | Registry ops return bare bodies, legacy ext-app handlers `{ok:true,result}`, 404s plaintext. **Reclassified v0.4-breaking (2026-08-03):** unifying changes response shapes that the Copilot adapter and the external test harnesses parse — it cannot ship in a patch. Bundle with the other v0.4 breaking items (`canvas_snapshot` composite, async-SDK decision). The `/health` degraded flag for the 63 catch→warn→continue persistence sites is additive and can ship earlier. | project (v0.4) |
23
+ | Complete the registry migration | C1 | Fold the remaining hand-written routes in `server.ts` into operations; collapse the 76-method `PmxCanvas` onto `executeOperation`; migrate the CLI's 60 raw `fetch` calls onto the operation invoker. **Slice 1 done post-0.3.0:** the 7 `/api/ext-app/*` routes moved to `ops/ext-app.ts` (~290 lines out of `server.ts`, the 5 clone handlers — old M5 — collapsed to one parametrized definition; wire envelopes byte-identical, no new MCP tools). **Slice 2 done:** the 15 ax wire routes (8 GET lists + `ax/context` + `ax/surface-snapshot` + `pinned-context` + `code-graph` + `ax/activity` + `ax/interaction` + legacy `PATCH /api/canvas/ax`) moved to `ops/ax-read.ts` (~370 more lines out of `server.ts`); `open-external` stays hand-written (coupled to the live server port + system browser opener). **Slice 3 done:** theme GET/POST, canvas update, viewport, annotation POST, and node `:id/refresh` moved to the registry (`ops/canvas-wire.ts`, plus additions to `ops/viewport.ts`, `ops/annotation.ts`, `ops/nodes.ts`); the two dispatch gates unified into one `/api/` gate; `server.ts` is at ~2,980 lines (from 3,873 at the audit). **Remaining routes (~20)** are the deliberate poor fits (SSE, binary, HTML/static, file bodies) plus four server-coupled handlers: `prompt` + workbench `state`/`open`/`intent` (module-local `primaryWorkbench*` session state), `render` (Excalidraw URL-normalizer chain), `open-external` (live port + system browser). Those need state injection like the webview runner — a deliberate follow-up, not mechanical. **Status (2026-08-03):** the CLI fetches are done (C1b) and the `PmxCanvas` collapse is closed by the async-SDK decision (sync facade stays). The 4 server-coupled routes (`prompt`, workbench `state`/`open`/`intent`, `render`, `open-external`) remain the only open C1 work — internal state-injection refactor, zero wire impact, deliberately post-0.4. | tail open (post-0.4) |
24
+ | One HTTP envelope | M4 residual | **Done (2026-08-03, v0.4.0):** every hand-written error response is now a JSON `{ ok:false, error }` envelope with unchanged status codes (matching the registry dispatcher); `/health` gained a `persistence` block that reports degraded durability after a failed save (ok again on the next success). The ext-app `{ok:true,result}` wrapper is KEPT deliberately — it is the app-bridge proxy contract our own panel parses; changing it bought no consumer value. | done |
24
25
  | `dist/` commit policy | C3 residual | **Decided + done post-0.3.0:** `dist/` stays committed; `test.yml` now has a `dist/types` staleness gate (rebuild + fail on drift). The `dist/canvas`/`dist/json-render` bundles are deliberately not byte-checked — bundler output varies across bun versions; their staleness story folds into M1. | done |
25
- | Skill-mirror validator | C2 residual | `validate:agent-skills` exits 0 when the (local-only, gitignored) trees are absent. Docs are honest about this now; decide whether to repoint, gate, or delete the check. | decision |
26
+ | Skill-mirror validator | C2 residual | **Decided + done (2026-08-03):** the script prints a labeled SKIPPED notice when trees are absent (local behavior unchanged), and the permanently-no-op CI step was removed with a comment. | done |
26
27
  | Generate CLAUDE.md/AGENTS.md from one source | M9 residual | **Done (tier-3 batch):** CI fails on any CLAUDE.md/AGENTS.md byte drift (`test.yml` identity gate) — the cheap alternative to generation the decision analysis chose. Generation/symlinking stays unnecessary while the gate holds. | done |
27
28
 
28
29
  ## Phase 2 — make the safety net real
@@ -30,19 +31,19 @@ The remaining items were re-analyzed with pros/cons and tiered; the maintainer a
30
31
  | Item | Source | What's open | Shape |
31
32
  |---|---|---|---|
32
33
  | Lint/format tooling | H6 | **Done post-0.3.0:** Biome 2.5 adopted — `bun run lint` / `lint:fix`, wired into `release:check` (making RELEASE.md's "lints" claim true) and CI. First-run format sweep landed as its own mechanical commit; 7 lint errors hand-fixed (incl. a real conditional-hooks bug in `SelectionBar.tsx`). Deliberately disabled rules to revisit: a11y group, `useExhaustiveDependencies` (misfires on Preact signals), `noNonNullAssertion` (143 deliberate uses), `noArrayIndexKey` (13 sites). ~77 warnings (unused imports/vars, optional-chain) left as a burn-down list — warnings don't fail the gate. | done |
33
- | Client + server unit coverage | H7 | **Done post-0.3.3 (first tranche):** happy-dom + @testing-library/preact harness under `tests/client/` (own preload so DOM globals never leak into server suites); 20 client render tests incl. the SelectionBar conditional-hooks regression; `semantic-attention` (25 tests) and `canvas-schema` (21 tests incl. an M12 stale-tool-drift invariant) covered; tests are TYPECHECKED (`tsconfig.tests.json`, 270 latent errors fixed, wired into `typecheck` + CI); coverage now GATES via `scripts/check-coverage.ts` total floors (bun's bunfig threshold is per-file and its table form is silently ignored — documented). **Remaining (opportunistic):** dedicated tests for `ax-state-manager.ts` and `mcp-app-host.ts`, and growing the client suite alongside H5/M2 work. | first tranche done |
34
+ | Client + server unit coverage | H7 | **Done post-0.3.3 (first tranche):** happy-dom + @testing-library/preact harness under `tests/client/` (own preload so DOM globals never leak into server suites); 20 client render tests incl. the SelectionBar conditional-hooks regression; `semantic-attention` (25 tests) and `canvas-schema` (21 tests incl. an M12 stale-tool-drift invariant) covered; tests are TYPECHECKED (`tsconfig.tests.json`, 270 latent errors fixed, wired into `typecheck` + CI); coverage now GATES via `scripts/check-coverage.ts` total floors (bun's bunfig threshold is per-file and its table form is silently ignored — documented). **Done (2026-08-03):** dedicated suites for `ax-state-manager.ts` (39 tests, injected-deps harness + in-memory SQLite) and `mcp-app-host.ts` (15 tests; stateful paths in hermetic subprocesses). Unit-test scripts now pin `PMX_MCP_APP_HOST_STATE_FILE` to a scratch path so suites never touch the real home-dir state. Client suite grows alongside H5. | done |
34
35
  | Daemon port/lock lifecycle | H4 | **Fixed post-0.3.0** (`src/cli/daemon.ts` + `tests/unit/cli-daemon.test.ts`). Note the original finding was partly wrong: the daemon child was already strict-port (`PmxCanvas.start` pins `allowPortFallback: false`), so fallback-blind polling could not occur. The real defects — fixed — were: the pre-check accepted any responsive `/health` as "already running" (even a foreign workspace's daemon), a TOCTOU double-spawn window, the pid recorded only after health passed (slow start → unkillable orphan), and no PID-recycling guard. The flaky `pmx-canvas-sdk.test.ts` port collision is NOT this — it's M8's copy-pasted `getAvailablePort()` racing. | done |
35
- | Test-suite structure | M8 | Integration monoliths under `tests/unit/` (`server-api` 5,213 lines, `cli-node` 2,903); `getAvailablePort()` copy-pasted in 5 files; `waitForPersistence(650ms)` fixed sleeps in 9 files + ~19 magic `Bun.sleep`s; Playwright lacks `retries`/`forbidOnly`; `test:e2e-cli` (517-line script) runs in no CI workflow. *Post-0.3.0:* `operation-parity` now binds port 0 and derives the port from the base URL (removes one reserve-then-rebind race); the shared-helper dedup and the rest remain. Also noted: `ax-parity.test.ts` is a string-matching source-analysis test whose needles broke when Biome re-wrapped `cmd(` calls — its CLI needles are now layout-independent, but any future needle should avoid depending on line layout. **Also noted (2026-07-07):** the `server-api` Excalidraw diagram tests do real network round-trips to the hosted `mcp.excalidraw.com` MCP server and intermittently time out (30s) under full parallel runs. **M8 subset done (2026-07-08):** those tests now run against the local stdio fixture via `PMX_CANVAS_EXCALIDRAW_MCP_COMMAND`; `getAvailablePort` is one shared helper; Playwright has `retries`/`forbidOnly` on CI; `e2e-cli` is a CI job. **Remaining (parked):** the `tests/unit/` monolith split and the `waitForPersistence` fixed-sleep sweep. | subset done |
36
+ | Test-suite structure | M8 | Integration monoliths under `tests/unit/` (`server-api` 5,213 lines, `cli-node` 2,903); `getAvailablePort()` copy-pasted in 5 files; `waitForPersistence(650ms)` fixed sleeps in 9 files + ~19 magic `Bun.sleep`s; Playwright lacks `retries`/`forbidOnly`; `test:e2e-cli` (517-line script) runs in no CI workflow. *Post-0.3.0:* `operation-parity` now binds port 0 and derives the port from the base URL (removes one reserve-then-rebind race); the shared-helper dedup and the rest remain. Also noted: `ax-parity.test.ts` is a string-matching source-analysis test whose needles broke when Biome re-wrapped `cmd(` calls — its CLI needles are now layout-independent, but any future needle should avoid depending on line layout. **Also noted (2026-07-07):** the `server-api` Excalidraw diagram tests do real network round-trips to the hosted `mcp.excalidraw.com` MCP server and intermittently time out (30s) under full parallel runs. **M8 subset done (2026-07-08):** those tests now run against the local stdio fixture via `PMX_CANVAS_EXCALIDRAW_MCP_COMMAND`; `getAvailablePort` is one shared helper; Playwright has `retries`/`forbidOnly` on CI; `e2e-cli` is a CI job. **Sleep sweep done (2026-08-03):** fixed persistence sleeps replaced by condition polling via a shared helper. **Remaining (post-0.4, test organization only):** the `tests/unit/` monolith split. | sleep sweep done |
36
37
  | `noUncheckedIndexedAccess` | L7 residual | Enable in tsconfig; will surface many index-access errors — treat as its own sweep. | small (noisy) |
37
38
 
38
39
  ## Phase 3 — client consolidation
39
40
 
40
41
  | Item | Source | What's open | Shape |
41
42
  |---|---|---|---|
42
- | One node factory | H5 | `sse-bridge.ts` (1,066 lines) keeps its own `DEFAULT_POSITIONS`/`makeNode` divergent from `canvas-store`; `responseToThreadMap` is not cleaned on SSE reconnect; server-side `syncEventToCanvasState` (~330 lines) mirrors it as an if/else chain with geometry defaults duplicated against `ensureDefaultDockedNodes`. Share one factory; treat `canvas-layout-update` as the single source of truth; table-drive the event sync. | project |
43
+ | One node factory | H5 | **Client half done (2026-08-03):** the `responseToThreadMap` reconnect leak is fixed (cleared in the shared per-connection reset path; mutation-verified test) and all four sse-bridge construction sites route through a shared `node-factory.ts`. **Premise correction:** `canvas-store` holds no node-creation defaults — the real divergences are client-vs-SERVER (server `defaultNodeSize` vs the client table; `ensureDefaultDockedNodes` seeding collapsed/docked vs the bridge's expanded/undocked; a same-class `serverResponseToThreadMap` leak at server.ts:2469) — all catalogued in the factory for the deferred server half (table-drive `syncEventToCanvasState`, single source of truth). | client half done |
43
44
  | Shared AX iframe bridge | M2 | **Done (2026-08-03):** `useAxSurfaceBridge` (`src/client/nodes/use-ax-surface-bridge.ts`) single-sites the sandboxed-surface trust boundary previously copy-pasted in `HtmlNode.tsx`, `McpAppNode.tsx`, `ExtAppFrame.tsx`; `src/shared/ax-surface-protocol.ts` owns the wire tags (`pmx-canvas-ax`, `-ack`, `pmx-canvas-html-node`, the ext-app boot beacon) — client parsers import them, generated surface scripts (html-surface.ts, ext-app builders, json-render viewer) interpolate them byte-identically. Client render tests pin the validation gates + submit→ack round-trip (`tests/client/use-ax-surface-bridge.test.tsx`). | done |
44
45
  | `ExtAppFrame.tsx` timing hacks | M3 | 915 lines, 12 refs, 7 `setTimeout`s, 5 rAFs, WebKit remount slot counter, 1200 ms fallback bootstrap, two near-duplicate `buildHostContext` blocks. Extract `useExtAppBridge`; single `buildHostContext`. | project |
45
- | json-render stack decision | M1 | Preact + React 19 + recharts all runtime deps; 7 exact-pinned `@json-render/*` incl. 3 devtools packages shipped; `dist/json-render/index.js` 1.37 MB and `dist/canvas/index.js` 599 KB both unminified; build script shells to python3 with a stale-bundle fallback. Minimum: minify, move devtools to devDependencies, lazy-load the viewer, replace the python mtime gate. Bigger call: separate optional package if the React stack stays. | decision + project |
46
+ | json-render stack decision | M1 | **Minimum-four done (2026-08-03):** both bundles verified minified (the audit's "unminified" was stale — React+recharts dominate the 1.31 MiB minified size); the 2 `@json-render/devtools*` packages moved to devDependencies; the viewer is already a separate page bundle (nothing to lazy-load — verified, no churn manufactured); the python mtime gate is now Bun with parity-tested exit codes. **Still open (decision):** the React-stack question — a separate optional package remains the only real size lever. | decision |
46
47
  | Client dead code / duplication | L1, L2, L3 | `ContextPinHud.tsx` unreferenced; three pure re-export shims; two parallel markdown-format stacks; `global.css` 3,746 lines with 11 `!important` vs 227 inline styles and unscaled z-indexes. | small (batchable) |
47
48
 
48
49
  ## Smaller items (no phase, pick up opportunistically)
@@ -51,13 +52,13 @@ The remaining items were re-analyzed with pros/cons and tiered; the maintainer a
51
52
  |---|---|---|
52
53
  | Per-action composite schemas | M6 | **Done (2026-08-03, middle path per the decision analysis):** composite dispatch re-enforces each member op's required MCP fields at runtime and names the missing field (`requiredMcpFields` in `operations/mcp.ts`) — no `oneOf` schema break (many MCP hosts handle `oneOf` poorly). Advertised schemas stay all-optional by design. |
53
54
  | Packaging | M10 | `files` ships both `src/` and `dist/` (~double tarball, two import paths); `bin` points at raw `.ts` (undocumented Bun-only constraint); `@types/turndown` in runtime deps. |
54
- | Env var hygiene | M11 residual | Docs side **done (tier-3 batch)**: `docs/environment.md` covers `PMX_DATA` + the `PMX_CANVAS_JSON_RENDER_*`/`PMX_MCP_APP_HOST_*` families. Still open: `PMX_CANVAS_PORT` vs `PMX_WEB_CANVAS_PORT` applied inconsistently across entry points (partly mitigated by the global `--port`/`--server-url` CLI flags). |
55
+ | Env var hygiene | M11 residual | **Done (2026-08-03):** server startup now falls back `--port` → `PMX_WEB_CANVAS_PORT` → `PMX_CANVAS_PORT` → 4313, so setting only the client var no longer splits client and server ports; documented in `docs/environment.md`. |
55
56
  | `publish.yml` e2e gap | M11 residual | **Done (tier-3 batch):** publish.yml verifies the tagged SHA has a successful `test.yml` run before publishing. |
56
57
  | MCP notification fan-out | M7 residual | Five `canvas://` resource URIs notify on every mutation regardless of type (`mcp/server.ts:95-99`). |
57
- | Legacy migration code on every boot | L4 | **Parked deliberately (2026-07-08):** the "unconditional" run is a couple of `existsSync` stats per boot — a one-shot marker adds state machinery to save nothing measurable and risks breaking upgrades from old layouts. Retire the migration code wholesale in a future major instead. |
58
- | Suppression/mutation plumbing | L5 | Two parallel suppression depth-counters; `onMutation` is a single-slot setter wired twice (last-write-wins). |
58
+ | Legacy migration code on every boot | L4 | **Done (2026-08-03, v0.4.0 breaking window):** the pre-0.2 import path is deleted (~150 lines); fresh-boot bootstrapping untouched; `PMX_CANVAS_STATE_FILE` survives only as a `.db`-path alias with a boot warning otherwise. Verified with a legacy-artifacts smoke (files untouched, no `.bak`). |
59
+ | Suppression/mutation plumbing | L5 | **Done (2026-08-03):** the "two counters" are semantically distinct by design (emit-suppression for batch vs recording-suppression for undo/redo — merging breaks one or the other; documented at both declarations). The redundant second `onMutation` wire in `PmxCanvas.start` was deleted; the slot contract is documented. |
59
60
  | Module-global lifecycle leaks | L6 | ~20 module-level `let`s in `server.ts`; code-graph debounce timer survives `stopCanvasServer`; remote SSE watcher loop has no cancellation. |
60
- | June doc citations | L7 residual | `tech-debt-assessment-2026-06.md` cites gitignored `.learnings/` entries future maintainers can't read. |
61
+ | June doc citations | L7 residual | **Done (2026-08-03):** each cited entry's substance is inlined so the doc stands alone; one miscitation corrected. |
61
62
 
62
63
  ## Added post-0.3.3 (2026-08-03 sweep)
63
64
 
@@ -65,5 +66,13 @@ The remaining items were re-analyzed with pros/cons and tiered; the maintainer a
65
66
  |---|---|
66
67
  | Windows web-artifacts | The artifact build pipeline is bash-script-backed (POSIX-only); `tests/unit/web-artifacts.test.ts` self-skips on win32 and docs now state the boundary. Decide: port the build scripts to a cross-platform runner, or keep the documented "build on POSIX, render anywhere" stance. |
67
68
  | Composite parity no longer machine-checked | Since v0.3.0 removed the legacy standalones, the byte-identical composite-vs-legacy guarantee has no head-to-head test — regressions in `buildInput`/`formatResult` reuse surface only via output-shape assertions. Accepted at review; revisit if a composite drifts. |
68
- | Portal-layer ext-app mount (contingency) | If the maintainer sets the WebKit UX bar at "never any visible gap" (0.3.3 report leaves it open), inline ext-app iframes must render outside the world transform — an architectural change that reopens the parked M3. Only scope on a failing report cycle. |
69
+ | Portal-layer ext-app mount (contingency) | If the maintainer sets the WebKit UX bar at "never any visible gap", inline ext-app iframes must render outside the world transform — an architectural change that reopens the parked M3. The 0.4.0 Copilot report keeps Finding N open (residual black after settled recovery; enlarge/close still the reliable unstick) — 0.4.1 ships a cheaper mimic first (post-settle WebKit repaint nudge, `repaint-nudge` trail event). If the next Copilot cycle still fails with the nudge in place, THIS contingency is the next step. |
70
+
71
+ ## Added post-0.4.0 (2026-08-04 report cycle)
72
+
73
+ | Item | What's open |
74
+ |---|---|
75
+ | Finding S — frame documents vs server restart | **Done (2026-08-04):** the browser revalidates minted frame-document URLs on every reconnect epoch (`workbenchConnectionEpoch`, HEAD probe) and re-mints against the new process when the store is gone; `/api/canvas/frame-documents/:id` answers HEAD. Confirmed-fix pending the next Copilot/Codex kill-respawn cycle. Server-side persistence of frame docs deliberately NOT done — they are client-derived and re-mintable. |
76
+ | Amp portal nested-iframe embedding | **Done (2026-08-04):** boot-time embed probe (`/api/canvas/iframe-probe`) + fetch()+srcdoc fallback for all same-origin surfaces (`useSurfaceFrame`, `useIframeDocument` srcdoc short-circuit), `?iframe-mode` override, e2e-forced-mode coverage. External (cross-origin) app URLs cannot be inlined — documented limitation. |
77
+ | Copilot adapter `canvas_snapshot list` descriptor lag | Host-side cached tool schema still requires `name`; fresh `--mcp` is correct. Adapter/packaging issue, not a daemon defect — nothing to change in core; re-verify next cycle. |
69
78
  | Registry-migration dead-code sweep | **Done (2026-08-03):** removed the orphaned `describeOp`/`describeGateOp` (composites.ts, dead since the v0.3.0 registration filter) and the `compactNodePayload`/`compactLayoutPayload`/`wantsFullPayload` duplicates in `mcp/server.ts` (live copies are in `ops/nodes.ts`). |
Binary file