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.
- package/CHANGELOG.md +148 -1
- package/Readme.md +13 -11
- package/dist/canvas/index.js +2 -2
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/daemon.d.ts +74 -0
- package/dist/types/cli/watch.d.ts +2 -2
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
- package/dist/types/client/canvas/Minimap.d.ts +1 -1
- package/dist/types/client/nodes/FileNode.d.ts +1 -1
- package/dist/types/client/nodes/ImageNode.d.ts +1 -1
- package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
- package/dist/types/client/nodes/PromptNode.d.ts +1 -1
- package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
- package/dist/types/mcp/canvas-access.d.ts +0 -7
- package/dist/types/server/ax-state-manager.d.ts +3 -2
- package/dist/types/server/bundled-skills.d.ts +3 -3
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/ext-app-lookup.d.ts +1 -3
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/composites.d.ts +29 -23
- package/dist/types/server/operations/http.d.ts +2 -1
- package/dist/types/server/operations/index.d.ts +1 -1
- package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
- package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
- package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
- package/dist/types/shared/ax-intent.d.ts +1 -1
- package/docs/RELEASE.md +23 -8
- package/docs/ax-host-adapter-contract.md +7 -7
- package/docs/ax-state-contract.md +7 -6
- package/docs/bun-webview-integration.md +5 -5
- package/docs/cli.md +1 -1
- package/docs/http-api.md +35 -3
- package/docs/mcp.md +121 -100
- package/docs/node-types.md +45 -35
- package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
- package/docs/plans/plan-008-registry-finish.md +2 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-06.md +1 -1
- package/docs/tech-debt-assessment-2026-07.md +135 -0
- package/package.json +5 -2
- package/skills/data-analysis/SKILL.md +3 -3
- package/skills/frontend-design/SKILL.md +3 -2
- package/skills/json-render-mcp/SKILL.md +4 -3
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/pmx-canvas/SKILL.md +12 -7
- package/skills/pmx-canvas/evals/evals.json +2 -2
- package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
- package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
- package/skills/pmx-canvas/references/full-reference.md +58 -56
- package/skills/pmx-canvas/references/html-primitives.md +3 -3
- package/skills/tufte-viz/SKILL.md +5 -4
- package/skills/web-artifacts-builder/SKILL.md +4 -4
- package/src/cli/agent.ts +1861 -1548
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +63 -326
- package/src/cli/watch.ts +2 -10
- package/src/client/App.tsx +48 -46
- package/src/client/canvas/AttentionHistory.tsx +11 -1
- package/src/client/canvas/CanvasNode.tsx +41 -29
- package/src/client/canvas/CanvasViewport.tsx +101 -66
- package/src/client/canvas/CommandPalette.tsx +61 -27
- package/src/client/canvas/ContextMenu.tsx +13 -20
- package/src/client/canvas/ContextPinBar.tsx +1 -5
- package/src/client/canvas/ContextPinHud.tsx +1 -6
- package/src/client/canvas/DockedNode.tsx +4 -4
- package/src/client/canvas/EdgeLayer.tsx +37 -36
- package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
- package/src/client/canvas/FocusFieldLayer.tsx +20 -22
- package/src/client/canvas/IntentLayer.tsx +31 -14
- package/src/client/canvas/Minimap.tsx +11 -16
- package/src/client/canvas/SelectionBar.tsx +4 -11
- package/src/client/canvas/ShortcutOverlay.tsx +3 -1
- package/src/client/canvas/SnapshotPanel.tsx +77 -95
- package/src/client/canvas/auto-fit.ts +15 -14
- package/src/client/canvas/snap-guides.ts +12 -12
- package/src/client/canvas/use-node-resize.ts +1 -5
- package/src/client/canvas/use-pan-zoom.ts +25 -26
- package/src/client/ext-app/bridge.ts +3 -12
- package/src/client/icons.tsx +63 -20
- package/src/client/nodes/ContextNode.tsx +14 -25
- package/src/client/nodes/ExtAppFrame.tsx +60 -39
- package/src/client/nodes/FileNode.tsx +74 -62
- package/src/client/nodes/GroupNode.tsx +4 -6
- package/src/client/nodes/HtmlNode.tsx +76 -46
- package/src/client/nodes/ImageNode.tsx +18 -27
- package/src/client/nodes/InlineFormatBar.tsx +4 -21
- package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
- package/src/client/nodes/LedgerNode.tsx +10 -4
- package/src/client/nodes/MarkdownNode.tsx +3 -10
- package/src/client/nodes/McpAppNode.tsx +26 -22
- package/src/client/nodes/MdFormatBar.tsx +10 -7
- package/src/client/nodes/PromptNode.tsx +23 -51
- package/src/client/nodes/ResponseNode.tsx +3 -13
- package/src/client/nodes/StatusNode.tsx +5 -9
- package/src/client/nodes/StatusSummary.tsx +2 -8
- package/src/client/nodes/WebpageNode.tsx +20 -14
- package/src/client/nodes/iframe-document-url.ts +25 -16
- package/src/client/nodes/image-warnings.ts +1 -7
- package/src/client/nodes/md-format.ts +20 -5
- package/src/client/state/attention-bridge.ts +4 -9
- package/src/client/state/attention-store.ts +1 -7
- package/src/client/state/canvas-store.ts +52 -36
- package/src/client/state/intent-bridge.ts +176 -112
- package/src/client/state/intent-store.ts +4 -1
- package/src/client/state/sse-bridge.ts +53 -70
- package/src/json-render/catalog.ts +12 -16
- package/src/json-render/charts/components.tsx +16 -20
- package/src/json-render/charts/extra-components.tsx +8 -16
- package/src/json-render/charts/extra-definitions.ts +1 -2
- package/src/json-render/charts/tufte-components.tsx +37 -20
- package/src/json-render/charts/tufte-definitions.ts +8 -2
- package/src/json-render/renderer/index.tsx +42 -22
- package/src/json-render/schema.ts +6 -3
- package/src/json-render/server.ts +33 -39
- package/src/mcp/canvas-access.ts +40 -121
- package/src/mcp/server.ts +206 -271
- package/src/server/agent-context.ts +63 -36
- package/src/server/ax-context.ts +7 -5
- package/src/server/ax-interaction.ts +176 -43
- package/src/server/ax-state-manager.ts +185 -41
- package/src/server/ax-state.ts +142 -47
- package/src/server/bundled-skills.ts +3 -3
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +177 -120
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +500 -102
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +155 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +20 -7
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +4 -20
- package/src/server/html-node-summary.ts +19 -10
- package/src/server/html-primitives.ts +326 -97
- package/src/server/html-surface.ts +6 -9
- package/src/server/image-source.ts +6 -4
- package/src/server/index.ts +321 -218
- package/src/server/intent-registry.ts +3 -6
- package/src/server/mcp-app-candidate.ts +5 -10
- package/src/server/mcp-app-host.ts +14 -38
- package/src/server/mcp-app-runtime.ts +12 -20
- package/src/server/mutation-history.ts +15 -5
- package/src/server/operations/composites.ts +38 -40
- package/src/server/operations/http.ts +5 -5
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +29 -36
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +102 -77
- package/src/server/operations/ops/ax-await.ts +17 -10
- package/src/server/operations/ops/ax-read.ts +347 -0
- package/src/server/operations/ops/ax-shared.ts +2 -7
- package/src/server/operations/ops/ax-state.ts +32 -14
- package/src/server/operations/ops/ax-timeline.ts +32 -19
- package/src/server/operations/ops/ax-work.ts +54 -37
- package/src/server/operations/ops/batch.ts +39 -14
- package/src/server/operations/ops/canvas-wire.ts +91 -0
- package/src/server/operations/ops/edges.ts +37 -25
- package/src/server/operations/ops/ext-app.ts +346 -0
- package/src/server/operations/ops/groups.ts +49 -20
- package/src/server/operations/ops/intent.ts +18 -12
- package/src/server/operations/ops/json-render.ts +239 -98
- package/src/server/operations/ops/nodes.ts +300 -111
- package/src/server/operations/ops/query.ts +62 -33
- package/src/server/operations/ops/snapshots.ts +35 -26
- package/src/server/operations/ops/validate.ts +2 -1
- package/src/server/operations/ops/viewport.ts +60 -16
- package/src/server/operations/ops/webview.ts +44 -18
- package/src/server/operations/registry.ts +2 -3
- package/src/server/operations/types.ts +7 -5
- package/src/server/operations/webview-runner.ts +1 -3
- package/src/server/placement.ts +8 -18
- package/src/server/server.ts +121 -1012
- package/src/server/spatial-analysis.ts +39 -25
- package/src/server/trace-manager.ts +3 -8
- package/src/server/web-artifacts.ts +23 -27
- package/src/server/webpage-node.ts +5 -13
- package/src/shared/auto-arrange.ts +12 -5
- package/src/shared/ax-intent.ts +1 -1
- package/src/shared/content-height-reporter.ts +8 -6
- package/src/shared/ext-app-tool-result.ts +2 -6
- package/src/shared/placement.ts +1 -4
- package/src/shared/semantic-attention.ts +39 -37
- 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:
|
|
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
|
|
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:
|
|
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
|
|
114
|
+
source: z
|
|
115
|
+
.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
|
|
108
116
|
.optional()
|
|
109
|
-
.describe(
|
|
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:
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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<
|
|
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:
|
|
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
|
|
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 = [
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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.
|
|
370
|
-
|
|
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 });
|