pmx-canvas 0.2.7 → 0.3.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 (189) hide show
  1. package/CHANGELOG.md +148 -1
  2. package/Readme.md +13 -11
  3. package/dist/canvas/index.js +2 -2
  4. package/dist/json-render/index.css +1 -1
  5. package/dist/types/cli/daemon.d.ts +74 -0
  6. package/dist/types/cli/watch.d.ts +2 -2
  7. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  8. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  9. package/dist/types/client/canvas/Minimap.d.ts +1 -1
  10. package/dist/types/client/nodes/FileNode.d.ts +1 -1
  11. package/dist/types/client/nodes/ImageNode.d.ts +1 -1
  12. package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
  13. package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
  14. package/dist/types/client/nodes/PromptNode.d.ts +1 -1
  15. package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
  16. package/dist/types/mcp/canvas-access.d.ts +0 -7
  17. package/dist/types/server/ax-state-manager.d.ts +3 -2
  18. package/dist/types/server/bundled-skills.d.ts +3 -3
  19. package/dist/types/server/canvas-schema.d.ts +1 -1
  20. package/dist/types/server/canvas-state.d.ts +2 -0
  21. package/dist/types/server/ext-app-lookup.d.ts +1 -3
  22. package/dist/types/server/html-primitives.d.ts +1 -1
  23. package/dist/types/server/index.d.ts +4 -4
  24. package/dist/types/server/operations/composites.d.ts +29 -23
  25. package/dist/types/server/operations/http.d.ts +2 -1
  26. package/dist/types/server/operations/index.d.ts +1 -1
  27. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  28. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  29. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  30. package/dist/types/shared/ax-intent.d.ts +1 -1
  31. package/docs/RELEASE.md +23 -8
  32. package/docs/ax-host-adapter-contract.md +7 -7
  33. package/docs/ax-state-contract.md +7 -6
  34. package/docs/bun-webview-integration.md +5 -5
  35. package/docs/cli.md +1 -1
  36. package/docs/http-api.md +35 -3
  37. package/docs/mcp.md +121 -100
  38. package/docs/node-types.md +45 -35
  39. package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
  40. package/docs/plans/plan-008-registry-finish.md +2 -0
  41. package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
  42. package/docs/screenshot.png +0 -0
  43. package/docs/tech-debt-assessment-2026-06.md +1 -1
  44. package/docs/tech-debt-assessment-2026-07.md +135 -0
  45. package/package.json +5 -2
  46. package/skills/data-analysis/SKILL.md +3 -3
  47. package/skills/frontend-design/SKILL.md +3 -2
  48. package/skills/json-render-mcp/SKILL.md +4 -3
  49. package/skills/playwright-cli/SKILL.md +1 -1
  50. package/skills/pmx-canvas/SKILL.md +12 -7
  51. package/skills/pmx-canvas/evals/evals.json +2 -2
  52. package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
  53. package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
  54. package/skills/pmx-canvas/references/full-reference.md +58 -56
  55. package/skills/pmx-canvas/references/html-primitives.md +3 -3
  56. package/skills/tufte-viz/SKILL.md +5 -4
  57. package/skills/web-artifacts-builder/SKILL.md +4 -4
  58. package/src/cli/agent.ts +1861 -1548
  59. package/src/cli/daemon.ts +460 -0
  60. package/src/cli/index.ts +63 -326
  61. package/src/cli/watch.ts +2 -10
  62. package/src/client/App.tsx +48 -46
  63. package/src/client/canvas/AttentionHistory.tsx +11 -1
  64. package/src/client/canvas/CanvasNode.tsx +41 -29
  65. package/src/client/canvas/CanvasViewport.tsx +101 -66
  66. package/src/client/canvas/CommandPalette.tsx +61 -27
  67. package/src/client/canvas/ContextMenu.tsx +13 -20
  68. package/src/client/canvas/ContextPinBar.tsx +1 -5
  69. package/src/client/canvas/ContextPinHud.tsx +1 -6
  70. package/src/client/canvas/DockedNode.tsx +4 -4
  71. package/src/client/canvas/EdgeLayer.tsx +37 -36
  72. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  73. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  74. package/src/client/canvas/IntentLayer.tsx +31 -14
  75. package/src/client/canvas/Minimap.tsx +11 -16
  76. package/src/client/canvas/SelectionBar.tsx +4 -11
  77. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  78. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  79. package/src/client/canvas/auto-fit.ts +15 -14
  80. package/src/client/canvas/snap-guides.ts +12 -12
  81. package/src/client/canvas/use-node-resize.ts +1 -5
  82. package/src/client/canvas/use-pan-zoom.ts +25 -26
  83. package/src/client/ext-app/bridge.ts +3 -12
  84. package/src/client/icons.tsx +63 -20
  85. package/src/client/nodes/ContextNode.tsx +14 -25
  86. package/src/client/nodes/ExtAppFrame.tsx +60 -39
  87. package/src/client/nodes/FileNode.tsx +74 -62
  88. package/src/client/nodes/GroupNode.tsx +4 -6
  89. package/src/client/nodes/HtmlNode.tsx +76 -46
  90. package/src/client/nodes/ImageNode.tsx +18 -27
  91. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  92. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  93. package/src/client/nodes/LedgerNode.tsx +10 -4
  94. package/src/client/nodes/MarkdownNode.tsx +3 -10
  95. package/src/client/nodes/McpAppNode.tsx +26 -22
  96. package/src/client/nodes/MdFormatBar.tsx +10 -7
  97. package/src/client/nodes/PromptNode.tsx +23 -51
  98. package/src/client/nodes/ResponseNode.tsx +3 -13
  99. package/src/client/nodes/StatusNode.tsx +5 -9
  100. package/src/client/nodes/StatusSummary.tsx +2 -8
  101. package/src/client/nodes/WebpageNode.tsx +20 -14
  102. package/src/client/nodes/iframe-document-url.ts +25 -16
  103. package/src/client/nodes/image-warnings.ts +1 -7
  104. package/src/client/nodes/md-format.ts +20 -5
  105. package/src/client/state/attention-bridge.ts +4 -9
  106. package/src/client/state/attention-store.ts +1 -7
  107. package/src/client/state/canvas-store.ts +52 -36
  108. package/src/client/state/intent-bridge.ts +176 -112
  109. package/src/client/state/intent-store.ts +4 -1
  110. package/src/client/state/sse-bridge.ts +53 -70
  111. package/src/json-render/catalog.ts +12 -16
  112. package/src/json-render/charts/components.tsx +16 -20
  113. package/src/json-render/charts/extra-components.tsx +8 -16
  114. package/src/json-render/charts/extra-definitions.ts +1 -2
  115. package/src/json-render/charts/tufte-components.tsx +37 -20
  116. package/src/json-render/charts/tufte-definitions.ts +8 -2
  117. package/src/json-render/renderer/index.tsx +42 -22
  118. package/src/json-render/schema.ts +6 -3
  119. package/src/json-render/server.ts +33 -39
  120. package/src/mcp/canvas-access.ts +40 -121
  121. package/src/mcp/server.ts +206 -271
  122. package/src/server/agent-context.ts +63 -36
  123. package/src/server/ax-context.ts +7 -5
  124. package/src/server/ax-interaction.ts +176 -43
  125. package/src/server/ax-state-manager.ts +185 -41
  126. package/src/server/ax-state.ts +142 -47
  127. package/src/server/bundled-skills.ts +3 -3
  128. package/src/server/canvas-db.ts +213 -95
  129. package/src/server/canvas-operations.ts +177 -120
  130. package/src/server/canvas-provenance.ts +1 -4
  131. package/src/server/canvas-schema.ts +500 -102
  132. package/src/server/canvas-serialization.ts +27 -35
  133. package/src/server/canvas-state.ts +155 -58
  134. package/src/server/chart-template.ts +4 -5
  135. package/src/server/code-graph.ts +20 -7
  136. package/src/server/diagram-presets.ts +28 -29
  137. package/src/server/ext-app-lookup.ts +4 -20
  138. package/src/server/html-node-summary.ts +19 -10
  139. package/src/server/html-primitives.ts +326 -97
  140. package/src/server/html-surface.ts +6 -9
  141. package/src/server/image-source.ts +6 -4
  142. package/src/server/index.ts +321 -218
  143. package/src/server/intent-registry.ts +3 -6
  144. package/src/server/mcp-app-candidate.ts +5 -10
  145. package/src/server/mcp-app-host.ts +14 -38
  146. package/src/server/mcp-app-runtime.ts +12 -20
  147. package/src/server/mutation-history.ts +15 -5
  148. package/src/server/operations/composites.ts +38 -40
  149. package/src/server/operations/http.ts +5 -5
  150. package/src/server/operations/index.ts +7 -1
  151. package/src/server/operations/invoker.ts +4 -3
  152. package/src/server/operations/mcp.ts +29 -36
  153. package/src/server/operations/ops/annotation.ts +122 -10
  154. package/src/server/operations/ops/app.ts +102 -77
  155. package/src/server/operations/ops/ax-await.ts +17 -10
  156. package/src/server/operations/ops/ax-read.ts +347 -0
  157. package/src/server/operations/ops/ax-shared.ts +2 -7
  158. package/src/server/operations/ops/ax-state.ts +32 -14
  159. package/src/server/operations/ops/ax-timeline.ts +32 -19
  160. package/src/server/operations/ops/ax-work.ts +54 -37
  161. package/src/server/operations/ops/batch.ts +39 -14
  162. package/src/server/operations/ops/canvas-wire.ts +91 -0
  163. package/src/server/operations/ops/edges.ts +37 -25
  164. package/src/server/operations/ops/ext-app.ts +346 -0
  165. package/src/server/operations/ops/groups.ts +49 -20
  166. package/src/server/operations/ops/intent.ts +18 -12
  167. package/src/server/operations/ops/json-render.ts +239 -98
  168. package/src/server/operations/ops/nodes.ts +300 -111
  169. package/src/server/operations/ops/query.ts +62 -33
  170. package/src/server/operations/ops/snapshots.ts +35 -26
  171. package/src/server/operations/ops/validate.ts +2 -1
  172. package/src/server/operations/ops/viewport.ts +60 -16
  173. package/src/server/operations/ops/webview.ts +44 -18
  174. package/src/server/operations/registry.ts +2 -3
  175. package/src/server/operations/types.ts +7 -5
  176. package/src/server/operations/webview-runner.ts +1 -3
  177. package/src/server/placement.ts +8 -18
  178. package/src/server/server.ts +121 -1012
  179. package/src/server/spatial-analysis.ts +39 -25
  180. package/src/server/trace-manager.ts +3 -8
  181. package/src/server/web-artifacts.ts +23 -27
  182. package/src/server/webpage-node.ts +5 -13
  183. package/src/shared/auto-arrange.ts +12 -5
  184. package/src/shared/ax-intent.ts +1 -1
  185. package/src/shared/content-height-reporter.ts +8 -6
  186. package/src/shared/ext-app-tool-result.ts +2 -6
  187. package/src/shared/placement.ts +1 -4
  188. package/src/shared/semantic-attention.ts +39 -37
  189. package/src/shared/surface.ts +8 -4
@@ -0,0 +1,347 @@
1
+ /**
2
+ * AX read + wire-compat operations (plan-009 C1 slice 2): the GET read
3
+ * surface (`ax/work`, `ax/approval`, `ax/review`, `ax/elicitation`,
4
+ * `ax/mode`, `ax/command`, `ax/policy`, `ax/host-capability`, `ax/context`,
5
+ * `ax/surface-snapshot`, `pinned-context`, `code-graph`) plus the three
6
+ * remaining AX writes (`ax/activity` ingest, `ax/interaction` submit, and
7
+ * the legacy `PATCH /api/canvas/ax` focus shape). Wire envelopes are
8
+ * byte-identical to the legacy server.ts handlers they replace. HTTP-only:
9
+ * the MCP read surface stays the composites/resources — no new tools, the
10
+ * frozen 27-tool surface is untouched.
11
+ *
12
+ * This module must never import server.ts or index.ts.
13
+ */
14
+ import { z } from 'zod';
15
+ import { buildAgentContextPreamble, serializeNodeForAgentContext } from '../../agent-context.js';
16
+ import { buildCanvasAxContext, buildCanvasAxSurfaceSnapshot } from '../../ax-context.js';
17
+ import { applyAxInteraction } from '../../ax-interaction.js';
18
+ import {
19
+ isAxActivityKind,
20
+ isAxEvidenceKind,
21
+ type PmxAxEvidenceKind,
22
+ type PmxAxReviewAnchorType,
23
+ type PmxAxReviewKind,
24
+ type PmxAxReviewSeverity,
25
+ type PmxAxWorkItemStatus,
26
+ } from '../../ax-state.js';
27
+ import { canvasState, type CanvasNodeState } from '../../canvas-state.js';
28
+ import { buildCodeGraphSummary } from '../../code-graph.js';
29
+ import { defineOperation, OperationError, type Operation, type OperationContext } from '../types.js';
30
+ import { normalizeAxNodeIds, normalizeAxSource } from './ax-shared.js';
31
+ import { isRecord } from './nodes.js';
32
+
33
+ // ── Activity-reaction validation (moved from server.ts) ───────
34
+
35
+ const AX_WORK_STATUSES = new Set(['todo', 'in-progress', 'blocked', 'done', 'cancelled']);
36
+
37
+ function normalizeAxWorkItemStatus(value: unknown): PmxAxWorkItemStatus | undefined {
38
+ return typeof value === 'string' && AX_WORK_STATUSES.has(value) ? (value as PmxAxWorkItemStatus) : undefined;
39
+ }
40
+
41
+ function isReviewSeverity(v: unknown): v is PmxAxReviewSeverity {
42
+ return v === 'info' || v === 'warning' || v === 'error';
43
+ }
44
+ function isReviewKind(v: unknown): v is PmxAxReviewKind {
45
+ return v === 'comment' || v === 'finding';
46
+ }
47
+ function isReviewAnchor(v: unknown): v is PmxAxReviewAnchorType {
48
+ return v === 'node' || v === 'file' || v === 'region';
49
+ }
50
+
51
+ // Validate untrusted activity `reactions` from an HTTP body into the typed override
52
+ // shape ingestActivity expects. `false` suppresses a default reaction; an object
53
+ // overrides its fields (invalid fields are dropped, not stored raw).
54
+ function normalizeActivityReactions(input: Record<string, unknown>): {
55
+ workItem?: false | { status?: PmxAxWorkItemStatus; detail?: string | null };
56
+ evidence?: false | { kind?: PmxAxEvidenceKind; body?: string | null };
57
+ review?:
58
+ | false
59
+ | {
60
+ severity?: PmxAxReviewSeverity;
61
+ kind?: PmxAxReviewKind;
62
+ anchorType?: PmxAxReviewAnchorType;
63
+ nodeId?: string | null;
64
+ };
65
+ } {
66
+ const out: ReturnType<typeof normalizeActivityReactions> = {};
67
+ if (input.workItem === false) out.workItem = false;
68
+ else if (isRecord(input.workItem)) {
69
+ const status = normalizeAxWorkItemStatus(input.workItem.status);
70
+ out.workItem = {
71
+ ...(status ? { status } : {}),
72
+ ...(typeof input.workItem.detail === 'string' ? { detail: input.workItem.detail } : {}),
73
+ };
74
+ }
75
+ if (input.evidence === false) out.evidence = false;
76
+ else if (isRecord(input.evidence)) {
77
+ out.evidence = {
78
+ ...(isAxEvidenceKind(input.evidence.kind) ? { kind: input.evidence.kind } : {}),
79
+ ...(typeof input.evidence.body === 'string' ? { body: input.evidence.body } : {}),
80
+ };
81
+ }
82
+ if (input.review === false) out.review = false;
83
+ else if (isRecord(input.review)) {
84
+ out.review = {
85
+ ...(isReviewSeverity(input.review.severity) ? { severity: input.review.severity } : {}),
86
+ ...(isReviewKind(input.review.kind) ? { kind: input.review.kind } : {}),
87
+ ...(isReviewAnchor(input.review.anchorType) ? { anchorType: input.review.anchorType } : {}),
88
+ ...(typeof input.review.nodeId === 'string' ? { nodeId: input.review.nodeId } : {}),
89
+ };
90
+ }
91
+ return out;
92
+ }
93
+
94
+ // ── GET list/read factory ─────────────────────────────────────
95
+
96
+ const emptyShape = {};
97
+ const emptySchema = z.looseObject(emptyShape);
98
+
99
+ /** A no-input GET whose body is `{ ok: true, ...read() }` (legacy list shape). */
100
+ function defineAxListOperation(name: string, path: string, read: () => Record<string, unknown>): Operation {
101
+ return defineOperation<z.infer<typeof emptySchema>, Record<string, unknown>>({
102
+ name,
103
+ mutates: false,
104
+ input: emptySchema,
105
+ inputShape: emptyShape,
106
+ http: {
107
+ method: 'GET',
108
+ path,
109
+ },
110
+ handler: () => ({ ok: true, ...read() }),
111
+ });
112
+ }
113
+
114
+ // ── ax.context.get / ax.surface-snapshot.get ──────────────────
115
+
116
+ const axContextShape = {
117
+ consumer: z.unknown().optional().describe('Optional consumer label filtering the pending-delivery lead block'),
118
+ };
119
+ const axContextSchema = z.looseObject(axContextShape);
120
+
121
+ const axContextGetOperation = defineOperation<z.infer<typeof axContextSchema>, Record<string, unknown>>({
122
+ name: 'ax.context.get',
123
+ mutates: false,
124
+ input: axContextSchema,
125
+ inputShape: axContextShape,
126
+ http: {
127
+ method: 'GET',
128
+ path: '/api/canvas/ax/context',
129
+ },
130
+ // Optional ?consumer= filters the compact `delivery` lead block (loop-safe — a
131
+ // consumer never sees steering/activity it originated), so a host adapter can
132
+ // inject its own un-truncated pending block per turn (report #54 hardening).
133
+ handler: (input) => {
134
+ const consumer = typeof input.consumer === 'string' ? input.consumer : undefined;
135
+ return buildCanvasAxContext(consumer) as unknown as Record<string, unknown>;
136
+ },
137
+ });
138
+
139
+ const axSurfaceSnapshotOperation = defineOperation<z.infer<typeof emptySchema>, Record<string, unknown>>({
140
+ name: 'ax.surface-snapshot.get',
141
+ mutates: false,
142
+ input: emptySchema,
143
+ inputShape: emptyShape,
144
+ http: {
145
+ method: 'GET',
146
+ path: '/api/canvas/ax/surface-snapshot',
147
+ },
148
+ // Compact AX state for surfaces (the same shape seeded into AX-enabled iframes).
149
+ // The client fetches this and pushes it to surfaces over the ax-update channel.
150
+ handler: () => buildCanvasAxSurfaceSnapshot() as unknown as Record<string, unknown>,
151
+ });
152
+
153
+ // ── pinned-context.get ────────────────────────────────────────
154
+
155
+ const pinnedContextOperation = defineOperation<z.infer<typeof emptySchema>, Record<string, unknown>>({
156
+ name: 'pinned-context.get',
157
+ mutates: false,
158
+ input: emptySchema,
159
+ inputShape: emptyShape,
160
+ http: {
161
+ method: 'GET',
162
+ path: '/api/canvas/pinned-context',
163
+ },
164
+ handler: () => {
165
+ const pinnedIds = Array.from(canvasState.contextPinnedNodeIds);
166
+ const nodes = pinnedIds
167
+ .map((id) => canvasState.getNode(id))
168
+ .filter((node): node is CanvasNodeState => node !== undefined);
169
+ const preamble =
170
+ pinnedIds.length > 0 ? buildAgentContextPreamble(nodes, { defaultTextLength: 700, webpageTextLength: 1600 }) : '';
171
+ const serialized = nodes.map((node) =>
172
+ serializeNodeForAgentContext(node, {
173
+ defaultTextLength: 700,
174
+ webpageTextLength: 1600,
175
+ includePosition: true,
176
+ }),
177
+ );
178
+ return { preamble, nodeIds: pinnedIds, count: pinnedIds.length, nodes: serialized };
179
+ },
180
+ });
181
+
182
+ // ── code-graph.get ────────────────────────────────────────────
183
+
184
+ const codeGraphOperation = defineOperation<z.infer<typeof emptySchema>, Record<string, unknown>>({
185
+ name: 'code-graph.get',
186
+ mutates: false,
187
+ input: emptySchema,
188
+ inputShape: emptyShape,
189
+ http: {
190
+ method: 'GET',
191
+ path: '/api/canvas/code-graph',
192
+ },
193
+ handler: () => buildCodeGraphSummary() as unknown as Record<string, unknown>,
194
+ });
195
+
196
+ // ── ax.activity.ingest ────────────────────────────────────────
197
+
198
+ const activityShape = {
199
+ kind: z
200
+ .unknown()
201
+ .optional()
202
+ .describe('Activity kind: tool-start, tool-result, failure, error, session-start, session-end, command, note'),
203
+ title: z.unknown().optional().describe('Activity title'),
204
+ summary: z.unknown().optional(),
205
+ outcome: z.unknown().optional().describe('success or failure'),
206
+ ref: z.unknown().optional().describe('File path, URL, or commit the activity refers to'),
207
+ nodeIds: z.unknown().optional(),
208
+ data: z.unknown().optional(),
209
+ reactions: z.unknown().optional().describe('Override or suppress the kind-driven default reactions'),
210
+ source: z.unknown().optional(),
211
+ };
212
+ const activitySchema = z.looseObject(activityShape);
213
+
214
+ // Report primitive A: ingest a harness-forwarded agent activity; the board auto-reacts.
215
+ const activityIngestOperation = defineOperation<z.infer<typeof activitySchema>, Record<string, unknown>>({
216
+ name: 'ax.activity.ingest',
217
+ mutates: false,
218
+ input: activitySchema,
219
+ inputShape: activityShape,
220
+ http: {
221
+ method: 'POST',
222
+ path: '/api/canvas/ax/activity',
223
+ },
224
+ handler: (input, ctx: OperationContext) => {
225
+ const body: Record<string, unknown> = input;
226
+ if (!isAxActivityKind(body.kind)) {
227
+ throw new OperationError(
228
+ "activity requires a valid 'kind': one of tool-start, tool-result, failure, error, session-start, session-end, command, note.",
229
+ );
230
+ }
231
+ if (typeof body.title !== 'string' || !body.title.trim()) {
232
+ throw new OperationError('activity requires a title.');
233
+ }
234
+ const result = canvasState.ingestActivity(
235
+ {
236
+ kind: body.kind,
237
+ title: body.title,
238
+ ...(typeof body.summary === 'string' ? { summary: body.summary } : {}),
239
+ ...(body.outcome === 'success' || body.outcome === 'failure' ? { outcome: body.outcome } : {}),
240
+ ...(typeof body.ref === 'string' ? { ref: body.ref } : {}),
241
+ ...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
242
+ ...(isRecord(body.data) ? { data: body.data } : {}),
243
+ ...(isRecord(body.reactions) ? { reactions: normalizeActivityReactions(body.reactions) } : {}),
244
+ },
245
+ { source: normalizeAxSource(body.source, 'api') },
246
+ );
247
+ ctx.emit('ax-event-created', { event: result.event });
248
+ if (result.workItem) ctx.emit('ax-state-changed', { workItem: result.workItem });
249
+ if (result.evidence) ctx.emit('ax-event-created', { evidence: result.evidence });
250
+ if (result.review) ctx.emit('ax-state-changed', { reviewAnnotation: result.review });
251
+ return { ok: true, ...result };
252
+ },
253
+ });
254
+
255
+ // ── ax.interaction.submit ─────────────────────────────────────
256
+
257
+ const interactionShape = {
258
+ type: z.unknown().optional().describe('Interaction type, e.g. ax.work.create'),
259
+ sourceNodeId: z.unknown().optional(),
260
+ payload: z.unknown().optional(),
261
+ sourceSurface: z.unknown().optional(),
262
+ correlationId: z.unknown().optional(),
263
+ source: z.unknown().optional(),
264
+ };
265
+ const interactionSchema = z.looseObject(interactionShape);
266
+
267
+ const interactionOperation = defineOperation<z.infer<typeof interactionSchema>, Record<string, unknown>>({
268
+ name: 'ax.interaction.submit',
269
+ mutates: false,
270
+ input: interactionSchema,
271
+ inputShape: interactionShape,
272
+ http: {
273
+ method: 'POST',
274
+ path: '/api/canvas/ax/interaction',
275
+ // Legacy wire: 200 when accepted, the envelope's own status when refused.
276
+ status: (result) =>
277
+ isRecord(result) && result.ok !== true && typeof result.status === 'number' ? result.status : 200,
278
+ },
279
+ handler: (input, ctx: OperationContext) => {
280
+ const body: Record<string, unknown> = input;
281
+ const { result, events } = applyAxInteraction(canvasState, body, normalizeAxSource(body.source, 'api'));
282
+ for (const e of events) {
283
+ ctx.emit(e.event, e.payload);
284
+ }
285
+ return result as unknown as Record<string, unknown>;
286
+ },
287
+ });
288
+
289
+ // ── ax.state.patch (legacy PATCH /api/canvas/ax focus shape) ──
290
+
291
+ const statePatchShape = {
292
+ focus: z.unknown().optional().describe('Focus object: { nodeIds, source? }'),
293
+ };
294
+ const statePatchSchema = z.looseObject(statePatchShape);
295
+
296
+ const statePatchOperation = defineOperation<z.infer<typeof statePatchSchema>, Record<string, unknown>>({
297
+ name: 'ax.state.patch',
298
+ mutates: false,
299
+ input: statePatchSchema,
300
+ inputShape: statePatchShape,
301
+ http: {
302
+ method: 'PATCH',
303
+ path: '/api/canvas/ax',
304
+ },
305
+ handler: (input, ctx: OperationContext) => {
306
+ const body: Record<string, unknown> = input;
307
+ if (!body.focus || typeof body.focus !== 'object' || Array.isArray(body.focus)) {
308
+ throw new OperationError('PATCH /api/canvas/ax currently requires a focus object.');
309
+ }
310
+ const focusInput = body.focus as Record<string, unknown>;
311
+ const focus = canvasState.setAxFocus(normalizeAxNodeIds(focusInput.nodeIds), {
312
+ source: normalizeAxSource(focusInput.source, 'api'),
313
+ });
314
+ ctx.emit('ax-state-changed', { focus });
315
+ return { ok: true, state: canvasState.getAxState() };
316
+ },
317
+ });
318
+
319
+ export const axReadOperations: Operation[] = [
320
+ defineAxListOperation('ax.work.list', '/api/canvas/ax/work', () => ({ workItems: canvasState.getWorkItems() })),
321
+ defineAxListOperation('ax.approval.list', '/api/canvas/ax/approval', () => ({
322
+ approvalGates: canvasState.getApprovalGates(),
323
+ })),
324
+ defineAxListOperation('ax.review.list', '/api/canvas/ax/review', () => ({
325
+ reviewAnnotations: canvasState.getReviewAnnotations(),
326
+ })),
327
+ defineAxListOperation('ax.elicitation.list', '/api/canvas/ax/elicitation', () => ({
328
+ elicitations: canvasState.getElicitations(),
329
+ })),
330
+ defineAxListOperation('ax.mode.list', '/api/canvas/ax/mode', () => ({
331
+ modeRequests: canvasState.getModeRequests(),
332
+ })),
333
+ defineAxListOperation('ax.command.list', '/api/canvas/ax/command', () => ({
334
+ commands: canvasState.getCommandRegistry(),
335
+ })),
336
+ defineAxListOperation('ax.policy.get', '/api/canvas/ax/policy', () => ({ policy: canvasState.getPolicy() })),
337
+ defineAxListOperation('ax.host-capability.get', '/api/canvas/ax/host-capability', () => ({
338
+ host: canvasState.getHostCapability(),
339
+ })),
340
+ axContextGetOperation,
341
+ axSurfaceSnapshotOperation,
342
+ pinnedContextOperation,
343
+ codeGraphOperation,
344
+ activityIngestOperation,
345
+ interactionOperation,
346
+ statePatchOperation,
347
+ ];
@@ -15,16 +15,11 @@ import type { PmxAxSource } from '../../ax-state.js';
15
15
  export const AX_SOURCES = ['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'] as const;
16
16
 
17
17
  /** Zod schema for the optional `source` field shared by AX MCP tool shapes. */
18
- export const AX_SOURCE_SHAPE = z
19
- .enum(AX_SOURCES)
20
- .optional()
21
- .describe('Optional host/source label. Defaults to mcp.');
18
+ export const AX_SOURCE_SHAPE = z.enum(AX_SOURCES).optional().describe('Optional host/source label. Defaults to mcp.');
22
19
 
23
20
  /** An absent or unrecognized source falls back to the per-surface default. */
24
21
  export function normalizeAxSource(value: unknown, fallback: PmxAxSource): PmxAxSource {
25
- return (AX_SOURCES as readonly string[]).includes(value as string)
26
- ? (value as PmxAxSource)
27
- : fallback;
22
+ return (AX_SOURCES as readonly string[]).includes(value as string) ? (value as PmxAxSource) : fallback;
28
23
  }
29
24
 
30
25
  export function normalizeAxNodeIds(value: unknown): string[] {
@@ -52,7 +52,8 @@ const axGetOperation = defineOperation<z.infer<typeof axGetSchema>, Record<strin
52
52
  },
53
53
  mcp: {
54
54
  toolName: 'canvas_get_ax',
55
- description: 'Read the host-agnostic PMX AX state and agent-ready AX context. Use this when you need pinned context plus the current focus field.',
55
+ description:
56
+ 'Read the host-agnostic PMX AX state and agent-ready AX context. Use this when you need pinned context plus the current focus field.',
56
57
  extraShape: {
57
58
  includeContext: z.boolean().optional().describe('Include serialized agent-ready AX context. Default true.'),
58
59
  },
@@ -85,7 +86,12 @@ const axGetOperation = defineOperation<z.infer<typeof axGetSchema>, Record<strin
85
86
 
86
87
  const axFocusSetShape = {
87
88
  nodeIds: z.unknown().optional().describe('Node IDs to place in the AX focus field. Missing nodes are ignored.'),
88
- source: z.unknown().optional().describe('Optional host/source label for adapter-originated focus. Defaults to mcp. Use codex from the Codex app adapter.'),
89
+ source: z
90
+ .unknown()
91
+ .optional()
92
+ .describe(
93
+ 'Optional host/source label for adapter-originated focus. Defaults to mcp. Use codex from the Codex app adapter.',
94
+ ),
89
95
  };
90
96
 
91
97
  const axFocusSetSchema = z.looseObject(axFocusSetShape);
@@ -101,12 +107,16 @@ const axFocusSetOperation = defineOperation<z.infer<typeof axFocusSetSchema>, Re
101
107
  },
102
108
  mcp: {
103
109
  toolName: 'canvas_set_ax_focus',
104
- description: 'Set the PMX AX focus field without requiring viewport movement. Focus is persisted and available through canvas://ax-context.',
110
+ description:
111
+ 'Set the PMX AX focus field without requiring viewport movement. Focus is persisted and available through canvas://ax-context.',
105
112
  extraShape: {
106
113
  nodeIds: z.array(z.string()).describe('Node IDs to place in the AX focus field. Missing nodes are ignored.'),
107
- source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
114
+ source: z
115
+ .enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
108
116
  .optional()
109
- .describe('Optional host/source label for adapter-originated focus. Defaults to mcp. Use codex from the Codex app adapter.'),
117
+ .describe(
118
+ 'Optional host/source label for adapter-originated focus. Defaults to mcp. Use codex from the Codex app adapter.',
119
+ ),
110
120
  },
111
121
  buildInput: (input) => ({ ...input, source: normalizeAxSource(input.source, 'mcp') }),
112
122
  formatResult: (result) => {
@@ -146,13 +156,16 @@ const axPolicySetOperation = defineOperation<z.infer<typeof axPolicySetSchema>,
146
156
  },
147
157
  mcp: {
148
158
  toolName: 'canvas_set_ax_policy',
149
- description: 'Set the declarative AX policy (allowed/excluded/approval-required tools; prompt mode/append). PMX stores it and exposes it via canvas://ax-context; host adapters READ and enforce it. Merges with the existing policy.',
159
+ description:
160
+ 'Set the declarative AX policy (allowed/excluded/approval-required tools; prompt mode/append). PMX stores it and exposes it via canvas://ax-context; host adapters READ and enforce it. Merges with the existing policy.',
150
161
  extraShape: {
151
- tools: z.object({
152
- allowed: z.array(z.string()).optional(),
153
- excluded: z.array(z.string()).optional(),
154
- approvalRequired: z.array(z.string()).optional(),
155
- }).optional(),
162
+ tools: z
163
+ .object({
164
+ allowed: z.array(z.string()).optional(),
165
+ excluded: z.array(z.string()).optional(),
166
+ approvalRequired: z.array(z.string()).optional(),
167
+ })
168
+ .optional(),
156
169
  prompt: z.object({ systemAppend: z.string().optional(), mode: z.string().optional() }).optional(),
157
170
  source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system']).optional(),
158
171
  },
@@ -196,7 +209,10 @@ const axHostCapabilityReportShape = {
196
209
 
197
210
  const axHostCapabilityReportSchema = z.looseObject(axHostCapabilityReportShape);
198
211
 
199
- const axHostCapabilityReportOperation = defineOperation<z.infer<typeof axHostCapabilityReportSchema>, Record<string, unknown>>({
212
+ const axHostCapabilityReportOperation = defineOperation<
213
+ z.infer<typeof axHostCapabilityReportSchema>,
214
+ Record<string, unknown>
215
+ >({
200
216
  name: 'ax.host-capability.report',
201
217
  mutates: false,
202
218
  input: axHostCapabilityReportSchema,
@@ -208,7 +224,8 @@ const axHostCapabilityReportOperation = defineOperation<z.infer<typeof axHostCap
208
224
  },
209
225
  mcp: {
210
226
  toolName: 'canvas_report_host_capability',
211
- description: 'Report host/session capability from an adapter: what the host can do (canvas/hooks/tools/sessionMessaging/permissions/files/uiPrompts). Stored for diagnostics; core does not depend on a host.',
227
+ description:
228
+ 'Report host/session capability from an adapter: what the host can do (canvas/hooks/tools/sessionMessaging/permissions/files/uiPrompts). Stored for diagnostics; core does not depend on a host.',
212
229
  extraShape: {
213
230
  host: z.string().optional().describe('Host identifier (e.g. copilot, codex).'),
214
231
  canvas: z.boolean().optional(),
@@ -219,7 +236,8 @@ const axHostCapabilityReportOperation = defineOperation<z.infer<typeof axHostCap
219
236
  files: z.boolean().optional(),
220
237
  uiPrompts: z.boolean().optional(),
221
238
  raw: z.record(z.string(), z.unknown()).optional().describe('Optional raw capability payload for diagnostics.'),
222
- source: z.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
239
+ source: z
240
+ .enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
223
241
  .optional()
224
242
  .describe('Optional host/source label. Defaults to mcp.'),
225
243
  },
@@ -53,7 +53,15 @@ import { defineOperation, OperationError, type Operation } from '../types.js';
53
53
  import { isRecord } from './nodes.js';
54
54
  import { AX_SOURCE_SHAPE, axJsonResult, normalizeAxNodeIds, normalizeAxSource } from './ax-shared.js';
55
55
 
56
- const AX_EVENT_KINDS = ['prompt', 'assistant-message', 'tool-start', 'tool-result', 'failure', 'approval', 'steering'] as const;
56
+ const AX_EVENT_KINDS = [
57
+ 'prompt',
58
+ 'assistant-message',
59
+ 'tool-start',
60
+ 'tool-result',
61
+ 'failure',
62
+ 'approval',
63
+ 'steering',
64
+ ] as const;
57
65
  const AX_EVIDENCE_KINDS = ['logs', 'tool-result', 'screenshot', 'file', 'diff', 'test-output'] as const;
58
66
 
59
67
  // ── ax.event.record (canvas_record_ax_event) ──────────────────
@@ -80,7 +88,8 @@ const axEventRecordOperation = defineOperation<z.infer<typeof axEventRecordSchem
80
88
  },
81
89
  mcp: {
82
90
  toolName: 'canvas_record_ax_event',
83
- description: 'Record a normalized AX timeline event (prompt/assistant-message/tool-start/tool-result/failure/approval/steering). Timeline events persist for diagnostics and continuity but are not restored by snapshots.',
91
+ description:
92
+ 'Record a normalized AX timeline event (prompt/assistant-message/tool-start/tool-result/failure/approval/steering). Timeline events persist for diagnostics and continuity but are not restored by snapshots.',
84
93
  extraShape: {
85
94
  kind: z.enum(AX_EVENT_KINDS).describe('Normalized event kind.'),
86
95
  summary: z.string().describe('Short human-readable summary of the event.'),
@@ -136,7 +145,8 @@ const axEvidenceAddOperation = defineOperation<z.infer<typeof axEvidenceAddSchem
136
145
  },
137
146
  mcp: {
138
147
  toolName: 'canvas_add_evidence',
139
- description: 'Record an AX evidence item (logs/tool-result/screenshot/file/diff/test-output) on the timeline. Evidence persists for diagnostics and continuity but is not restored by snapshots; exposed via canvas://ax-timeline.',
148
+ description:
149
+ 'Record an AX evidence item (logs/tool-result/screenshot/file/diff/test-output) on the timeline. Evidence persists for diagnostics and continuity but is not restored by snapshots; exposed via canvas://ax-timeline.',
140
150
  extraShape: {
141
151
  kind: z.enum(AX_EVIDENCE_KINDS).describe('Evidence kind.'),
142
152
  title: z.string().describe('Short human-readable title for the evidence.'),
@@ -189,7 +199,8 @@ const axSteerOperation = defineOperation<z.infer<typeof axSteerSchema>, Record<s
189
199
  },
190
200
  mcp: {
191
201
  toolName: 'canvas_send_steering',
192
- description: 'Record a steering message: a user instruction from the surface to the active agent session. Persisted on the AX timeline and exposed via canvas://ax-timeline.',
202
+ description:
203
+ 'Record a steering message: a user instruction from the surface to the active agent session. Persisted on the AX timeline and exposed via canvas://ax-timeline.',
193
204
  extraShape: {
194
205
  message: z.string().describe('The steering instruction to deliver to the active agent session.'),
195
206
  source: AX_SOURCE_SHAPE,
@@ -228,7 +239,8 @@ const axTimelineGetOperation = defineOperation<z.infer<typeof axTimelineGetSchem
228
239
  },
229
240
  mcp: {
230
241
  toolName: 'canvas_get_ax_timeline',
231
- description: 'Read the bounded AX timeline: recent agent-events, evidence, and steering messages plus counts. Use this for diagnostics and session continuity.',
242
+ description:
243
+ 'Read the bounded AX timeline: recent agent-events, evidence, and steering messages plus counts. Use this for diagnostics and session continuity.',
232
244
  extraShape: {
233
245
  limit: z.number().optional().describe('Max rows per timeline table (default 50, max 200).'),
234
246
  },
@@ -238,9 +250,7 @@ const axTimelineGetOperation = defineOperation<z.infer<typeof axTimelineGetSchem
238
250
  // `limit` arrives as a number over MCP or a string over the HTTP query;
239
251
  // Number() normalizes both. Only a finite positive limit is forwarded.
240
252
  const limit = Number(input.limit ?? '');
241
- const timeline = canvasState.getAxTimeline(
242
- Number.isFinite(limit) && limit > 0 ? { limit } : {},
243
- );
253
+ const timeline = canvasState.getAxTimeline(Number.isFinite(limit) && limit > 0 ? { limit } : {});
244
254
  return { ok: true, ...timeline } as unknown as Record<string, unknown>;
245
255
  },
246
256
  });
@@ -266,11 +276,17 @@ const axDeliveryPendingOperation = defineOperation<z.infer<typeof axDeliveryPend
266
276
  },
267
277
  mcp: {
268
278
  toolName: 'canvas_claim_ax_delivery',
269
- description: 'Claim pending PMX AX deliveries for a consumer (adapterless delivery). Returns `pending` undelivered steering (mark each with canvas_mark_ax_delivery after acting) AND `pendingActivity`: open canvas-bound AX items awaiting the agent (open work items, pending approval gates / elicitations / mode requests) — typically created by the human in the browser. Both exclude items the consumer itself originated (loop prevention). `pending` defaults to oldest-first (FIFO, for ordered processing); pass `order:"newest"` to surface the human\'s LATEST in-canvas steering first when a small `limit` would otherwise bury it behind a stale backlog (report #68). pendingActivity is read-only here: resolve each via its own tool (canvas_resolve_approval / canvas_respond_elicitation / canvas_resolve_mode / canvas_update_work_item), not canvas_mark_ax_delivery.',
279
+ description:
280
+ 'Claim pending PMX AX deliveries for a consumer (adapterless delivery). Returns `pending` undelivered steering (mark each with canvas_mark_ax_delivery after acting) AND `pendingActivity`: open canvas-bound AX items awaiting the agent (open work items, pending approval gates / elicitations / mode requests) — typically created by the human in the browser. Both exclude items the consumer itself originated (loop prevention). `pending` defaults to oldest-first (FIFO, for ordered processing); pass `order:"newest"` to surface the human\'s LATEST in-canvas steering first when a small `limit` would otherwise bury it behind a stale backlog (report #68). pendingActivity is read-only here: resolve each via its own tool (canvas_resolve_approval / canvas_respond_elicitation / canvas_resolve_mode / canvas_update_work_item), not canvas_mark_ax_delivery.',
270
281
  extraShape: {
271
282
  consumer: z.string().optional().describe('Consumer/source label to exclude from results (e.g. copilot, mcp).'),
272
283
  limit: z.number().optional().describe('Max steering messages to return.'),
273
- order: z.enum(['newest', 'oldest']).optional().describe('Order of returned steering: "oldest" (FIFO, default) for ordered processing, or "newest" first to see the latest browser action when limited.'),
284
+ order: z
285
+ .enum(['newest', 'oldest'])
286
+ .optional()
287
+ .describe(
288
+ 'Order of returned steering: "oldest" (FIFO, default) for ordered processing, or "newest" first to see the latest browser action when limited.',
289
+ ),
274
290
  },
275
291
  // `consumer` is a loop-safety scope, not a source label — never defaulted.
276
292
  formatResult: axJsonResult,
@@ -285,9 +301,7 @@ const axDeliveryPendingOperation = defineOperation<z.infer<typeof axDeliveryPend
285
301
  // queries apply the same loop-safe consumer filter before the limit.
286
302
  const newest = input.order === 'newest';
287
303
  const scope = { ...(consumer ? { consumer } : {}), ...(limit ? { limit } : {}) };
288
- const pending = newest
289
- ? canvasState.getPendingSteeringForContext(scope)
290
- : canvasState.getPendingSteering(scope);
304
+ const pending = newest ? canvasState.getPendingSteeringForContext(scope) : canvasState.getPendingSteering(scope);
291
305
  // The MCP tool aggregated pendingActivity; one wire body now serves it over
292
306
  // HTTP too (documented broadening). Loop-safe: consumer scopes both queries.
293
307
  const pendingActivity = buildPendingAxActivity(canvasState.getAxState(), consumer);
@@ -352,7 +366,8 @@ const axCommandInvokeOperation = defineOperation<z.infer<typeof axCommandInvokeS
352
366
  },
353
367
  mcp: {
354
368
  toolName: 'canvas_invoke_command',
355
- description: 'Invoke a registry-gated PMX command intent (pmx.plan | pmx.execute | pmx.promote-context | pmx.summarize | pmx.review). Records a timeline event a host/agent can observe — NOT arbitrary execution; unknown names are rejected.',
369
+ description:
370
+ 'Invoke a registry-gated PMX command intent (pmx.plan | pmx.execute | pmx.promote-context | pmx.summarize | pmx.review). Records a timeline event a host/agent can observe — NOT arbitrary execution; unknown names are rejected.',
356
371
  extraShape: {
357
372
  name: z.string().describe('A command name from the PMX command registry.'),
358
373
  args: z.record(z.string(), z.unknown()).optional(),
@@ -365,11 +380,9 @@ const axCommandInvokeOperation = defineOperation<z.infer<typeof axCommandInvokeS
365
380
  if (typeof input.name !== 'string') {
366
381
  throw new OperationError('command requires a name.');
367
382
  }
368
- const event = canvasState.invokeCommand(
369
- input.name,
370
- isRecord(input.args) ? input.args : null,
371
- { source: normalizeAxSource(input.source, 'api') },
372
- );
383
+ const event = canvasState.invokeCommand(input.name, isRecord(input.args) ? input.args : null, {
384
+ source: normalizeAxSource(input.source, 'api'),
385
+ });
373
386
  // Allowlist gate: invokeCommand returns null for an unknown command name.
374
387
  if (!event) throw new OperationError(`Unknown command "${input.name}".`, 400);
375
388
  ctx.emit('ax-event-created', { event });