pmx-canvas 0.1.35 → 0.1.36

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.
@@ -16,6 +16,8 @@ export declare function resolveExtAppDisplayModeRequest(requestedMode: DisplayMo
16
16
  };
17
17
  export declare function sendExtAppBootstrapState(bridge: ExtAppBridgeNotifications, toolInput: Record<string, unknown>, toolResult: CallToolResult | undefined): Promise<void>;
18
18
  export declare function resolveExtAppSandbox(value: unknown): string;
19
+ export declare function buildExtAppAxBridgeScript(axToken: string, nodeId: string): string;
20
+ export declare function injectExtAppAxBridgeScript(html: string, axBridgeScript: string): string;
19
21
  export declare function resolveExtAppContainerDimensions(target: ExtAppHostDimensionsTarget | null | undefined, fallback: {
20
22
  width: number;
21
23
  height: number;
@@ -40,6 +40,11 @@ type ListModeRequestsResult = ReturnType<PmxCanvas['listModeRequests']>;
40
40
  type RequestModeInput = Parameters<PmxCanvas['requestMode']>[0];
41
41
  type RequestModeResult = ReturnType<PmxCanvas['requestMode']>;
42
42
  type ResolveModeRequestResult = ReturnType<PmxCanvas['resolveModeRequest']>;
43
+ type IngestActivityInput = Parameters<PmxCanvas['ingestActivity']>[0];
44
+ type IngestActivityResult = ReturnType<PmxCanvas['ingestActivity']>;
45
+ type AwaitApprovalResult = Awaited<ReturnType<PmxCanvas['awaitApproval']>>;
46
+ type AwaitElicitationResult = Awaited<ReturnType<PmxCanvas['awaitElicitation']>>;
47
+ type AwaitModeResult = Awaited<ReturnType<PmxCanvas['awaitMode']>>;
43
48
  type GetCommandRegistryResult = ReturnType<PmxCanvas['getCommandRegistry']>;
44
49
  type InvokeCommandResult = ReturnType<PmxCanvas['invokeCommand']>;
45
50
  type GetPolicyResult = ReturnType<PmxCanvas['getPolicy']>;
@@ -115,7 +120,9 @@ export interface CanvasAccess {
115
120
  }): Promise<FocusNodeResult>;
116
121
  fitView(options?: FitViewOptions): Promise<FitViewResult>;
117
122
  getAxState(): Promise<AxStateResult>;
118
- getAxContext(): Promise<AxContextResult>;
123
+ getAxContext(options?: {
124
+ consumer?: string;
125
+ }): Promise<AxContextResult>;
119
126
  setAxFocus(nodeIds: string[], options?: {
120
127
  source?: PmxAxSource;
121
128
  }): Promise<SetAxFocusResult>;
@@ -178,6 +185,18 @@ export interface CanvasAccess {
178
185
  resolution?: string;
179
186
  source?: PmxAxSource;
180
187
  }): Promise<ResolveModeRequestResult>;
188
+ ingestActivity(input: IngestActivityInput, options?: {
189
+ source?: PmxAxSource;
190
+ }): Promise<IngestActivityResult>;
191
+ awaitApproval(id: string, options?: {
192
+ timeoutMs?: number;
193
+ }): Promise<AwaitApprovalResult>;
194
+ awaitElicitation(id: string, options?: {
195
+ timeoutMs?: number;
196
+ }): Promise<AwaitElicitationResult>;
197
+ awaitMode(id: string, options?: {
198
+ timeoutMs?: number;
199
+ }): Promise<AwaitModeResult>;
181
200
  getCommandRegistry(): Promise<GetCommandRegistryResult>;
182
201
  invokeCommand(name: string, args?: Record<string, unknown> | null, options?: {
183
202
  source?: PmxAxSource;
@@ -23,4 +23,4 @@ export interface PmxAxSurfaceSnapshot {
23
23
  */
24
24
  export declare function buildCanvasAxSurfaceSnapshot(): PmxAxSurfaceSnapshot;
25
25
  export declare function buildCanvasAxPinnedContext(): PmxAxPinnedContext;
26
- export declare function buildCanvasAxContext(): PmxAxContext;
26
+ export declare function buildCanvasAxContext(consumer?: string): PmxAxContext;
@@ -135,6 +135,18 @@ export interface PmxAxPinnedContext {
135
135
  export interface PmxAxFocusContext extends PmxAxFocusState {
136
136
  nodes: AgentContextNode[];
137
137
  }
138
+ export interface PendingAxActivityItem {
139
+ kind: 'work-item' | 'approval-gate' | 'elicitation' | 'mode-request';
140
+ id: string;
141
+ title: string;
142
+ status: string;
143
+ nodeIds: string[];
144
+ source: PmxAxSource | null;
145
+ }
146
+ export interface PmxAxDeliveryContext {
147
+ pendingSteering: PmxAxSteeringMessage[];
148
+ pendingActivity: PendingAxActivityItem[];
149
+ }
138
150
  export interface PmxAxContext {
139
151
  version: 1;
140
152
  generatedAt: string;
@@ -142,6 +154,7 @@ export interface PmxAxContext {
142
154
  nodeCount: number;
143
155
  edgeCount: number;
144
156
  };
157
+ delivery: PmxAxDeliveryContext;
145
158
  pinned: PmxAxPinnedContext;
146
159
  focus: PmxAxFocusContext;
147
160
  workItems: PmxAxWorkItem[];
@@ -153,6 +166,20 @@ export interface PmxAxContext {
153
166
  timeline: PmxAxTimelineSummary;
154
167
  host: PmxAxHostCapability | null;
155
168
  }
169
+ /**
170
+ * Open, agent-actionable canvas-bound AX items (open work items + pending approval
171
+ * gates / elicitations / mode requests). Unlike steering (a directive routed through
172
+ * the claim/ack delivery queue), these are STATE the human curates in the browser —
173
+ * they fire `ax-state-changed` (so resource-subscribers are pushed canvas://ax-work),
174
+ * but an adapterless client that only POLLS the delivery surface never saw them.
175
+ * Optionally excludes items the consumer itself originated (loop prevention),
176
+ * mirroring getPendingSteering. Shared by the MCP delivery surface and the HTTP
177
+ * context lead block so the digest never drifts between the two.
178
+ */
179
+ export declare function buildPendingAxActivity(state: PmxAxState, consumer?: string): PendingAxActivityItem[];
180
+ export type PmxAxActivityKind = 'tool-start' | 'tool-result' | 'failure' | 'error' | 'session-start' | 'session-end' | 'command' | 'note';
181
+ export declare function isAxActivityKind(value: unknown): value is PmxAxActivityKind;
182
+ export declare function mapAxActivityKindToEventKind(kind: PmxAxActivityKind): PmxAxEventKind;
156
183
  export interface PmxAxCommandDescriptor {
157
184
  name: string;
158
185
  description: string;
@@ -264,6 +291,7 @@ export declare function createAxSteeringMessage(message: string, source: PmxAxSo
264
291
  export declare function normalizeAxState(input: unknown, validNodeIds?: Set<string>): PmxAxState;
265
292
  export declare function buildAxContext(input: {
266
293
  layout: CanvasLayout;
294
+ delivery: PmxAxDeliveryContext;
267
295
  pinned: PmxAxPinnedContext;
268
296
  focus: PmxAxFocusState;
269
297
  focusNodes: AgentContextNode[];
@@ -0,0 +1,23 @@
1
+ /** Hard ceiling on a single blocking wait, regardless of the requested timeout. */
2
+ export declare const AX_WAIT_MAX_MS = 120000;
3
+ export interface AxWaitResult<T> {
4
+ /** Latest value, or null if the item does not exist / vanished mid-wait. */
5
+ value: T | null;
6
+ /** True only when the item still exists and is still pending after the wait. */
7
+ pending: boolean;
8
+ }
9
+ /**
10
+ * Block until a canvas-bound AX item resolves (its status leaves `pending`), the
11
+ * timeout elapses, or the request aborts — the server side of report primitive D
12
+ * ("gates that actually gate"). Resolves immediately when the item is already
13
+ * resolved, missing, or `timeoutMs <= 0` (a plain single read). Subscribes to the
14
+ * `ax` change channel and always disposes the listener + timer.
15
+ */
16
+ export declare function waitForAxResolution<T extends {
17
+ status: string;
18
+ }>(opts: {
19
+ read: () => T | null;
20
+ isResolved: (value: T) => boolean;
21
+ timeoutMs: number;
22
+ signal?: AbortSignal;
23
+ }): Promise<AxWaitResult<T>>;
@@ -10,7 +10,7 @@
10
10
  * Legacy `.pmx-canvas/state.json` is auto-migrated on first boot.
11
11
  */
12
12
  import { type PersistedCanvasState, type CanvasTheme, type AxTimelineQuery } from './canvas-db.js';
13
- import { 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';
13
+ 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';
14
14
  export declare const PMX_CANVAS_DIR = ".pmx-canvas";
15
15
  export interface PersistedBlobRef {
16
16
  __pmxCanvasBlob: 'v1';
@@ -146,8 +146,13 @@ declare class CanvasStateManager {
146
146
  private _axHostCapability;
147
147
  private _workspaceRoot;
148
148
  private _changeListeners;
149
- /** Register a listener for state changes. Used by MCP server to emit resource notifications. */
150
- onChange(cb: (type: CanvasChangeType) => void): void;
149
+ /**
150
+ * Register a listener for state changes. Used by MCP server to emit resource
151
+ * notifications and by the blocking-wait endpoints to await an AX transition.
152
+ * Returns a disposer that unregisters the listener (callers that don't need it
153
+ * — e.g. the long-lived MCP subscription — may ignore the return value).
154
+ */
155
+ onChange(cb: (type: CanvasChangeType) => void): () => void;
151
156
  private notifyChange;
152
157
  private _mutationRecorder;
153
158
  private _suppressRecordingDepth;
@@ -346,6 +351,9 @@ declare class CanvasStateManager {
346
351
  resolution?: string;
347
352
  source?: PmxAxSource;
348
353
  }): PmxAxModeRequest | null;
354
+ getApproval(id: string): PmxAxApprovalGate | null;
355
+ getElicitation(id: string): PmxAxElicitation | null;
356
+ getModeRequest(id: string): PmxAxModeRequest | null;
349
357
  getCommandRegistry(): PmxAxCommandDescriptor[];
350
358
  /** Invoke a registry-gated PMX command intent — records a timeline event (no execution). */
351
359
  invokeCommand(name: string, args?: Record<string, unknown> | null, options?: {
@@ -385,6 +393,50 @@ declare class CanvasStateManager {
385
393
  source?: PmxAxSource;
386
394
  }): PmxAxSteeringMessage;
387
395
  markSteeringDelivered(id: string): boolean;
396
+ /**
397
+ * Ingest a normalized agent activity (a tool/session event a harness forwards)
398
+ * and apply kind-driven board reactions, so the agent's real work flows back into
399
+ * the board without it remembering to push each item (report primitive A — makes
400
+ * AX bidirectional). Always records a timeline event; then, unless the caller
401
+ * overrides/suppresses via `reactions`, applies defaults by kind/outcome:
402
+ * • failure | error | outcome==='failure' → work item (blocked) + review
403
+ * (finding/error, anchored to a valid nodeId else the `ref` file) + evidence (logs)
404
+ * • tool-result + outcome==='success' → evidence (tool-result)
405
+ * • everything else (tool-start, session-*, command, note) → event only
406
+ * A reaction value of `false` suppresses it; an object overrides its fields/forces it on.
407
+ */
408
+ ingestActivity(input: {
409
+ kind: PmxAxActivityKind;
410
+ title: string;
411
+ summary?: string | null;
412
+ outcome?: 'success' | 'failure';
413
+ ref?: string | null;
414
+ nodeIds?: string[];
415
+ data?: Record<string, unknown> | null;
416
+ reactions?: {
417
+ workItem?: false | {
418
+ status?: PmxAxWorkItemStatus;
419
+ detail?: string | null;
420
+ };
421
+ evidence?: false | {
422
+ kind?: PmxAxEvidenceKind;
423
+ body?: string | null;
424
+ };
425
+ review?: false | {
426
+ severity?: PmxAxReviewSeverity;
427
+ kind?: PmxAxReviewKind;
428
+ anchorType?: PmxAxReviewAnchorType;
429
+ nodeId?: string | null;
430
+ };
431
+ };
432
+ }, options?: {
433
+ source?: PmxAxSource;
434
+ }): {
435
+ event: PmxAxEvent;
436
+ workItem: PmxAxWorkItem | null;
437
+ evidence: PmxAxEvidence | null;
438
+ review: PmxAxReviewAnnotation | null;
439
+ };
388
440
  getAxEvents(q?: AxTimelineQuery): PmxAxEvent[];
389
441
  getAxEvidence(q?: AxTimelineQuery): PmxAxEvidence[];
390
442
  getAxSteering(q?: AxTimelineQuery & {
@@ -27,6 +27,13 @@ export declare function normalizeSurfaceTheme(value: string | null | undefined):
27
27
  * injected when the node's AX capabilities are enabled (opt-in for `html`), and
28
28
  * the server re-validates every interaction — so this is a convenience surface,
29
29
  * not a trust boundary.
30
+ *
31
+ * `emit` returns a Promise that resolves with the interaction result once the
32
+ * parent acks it (report #55 — built-in confirmation so a click no longer looks
33
+ * like "nothing happened"). Authors can also `window.PMX_AX.on('ack', cb)` or
34
+ * listen for the `pmx-ax-ack` CustomEvent. Resolves with an `ax-ack-timeout`
35
+ * result after 10s if no ack arrives (e.g. an older parent), so `await emit()`
36
+ * never hangs.
30
37
  */
31
38
  export declare function buildAxBridge(axToken: string, nodeId: string): string;
32
39
  /**
@@ -2,7 +2,7 @@ import { EventEmitter } from 'node:events';
2
2
  import { canvasState } from './canvas-state.js';
3
3
  import type { CanvasAnnotation, CanvasNodeState, CanvasEdge, CanvasLayout } from './canvas-state.js';
4
4
  import { type AxInteractionInput, type AxInteractionPublicResult } from './ax-interaction.js';
5
- import type { PmxAxApprovalGate, PmxAxCommandDescriptor, PmxAxContext, PmxAxElicitation, PmxAxEvent, PmxAxEvidence, PmxAxEvidenceKind, PmxAxFocusState, PmxAxHostCapability, PmxAxMode, PmxAxModeRequest, PmxAxPolicy, PmxAxReviewAnchorType, PmxAxReviewAnnotation, PmxAxReviewKind, PmxAxReviewRegion, PmxAxReviewSeverity, PmxAxReviewStatus, PmxAxSource, PmxAxState, PmxAxSteeringMessage, PmxAxWorkItem, PmxAxWorkItemStatus } from './ax-state.js';
5
+ import type { PmxAxActivityKind, PmxAxApprovalGate, PmxAxCommandDescriptor, PmxAxContext, PmxAxElicitation, PmxAxEvent, PmxAxEvidence, PmxAxEvidenceKind, PmxAxFocusState, PmxAxHostCapability, PmxAxMode, PmxAxModeRequest, PmxAxPolicy, PmxAxReviewAnchorType, PmxAxReviewAnnotation, PmxAxReviewKind, PmxAxReviewRegion, PmxAxReviewSeverity, PmxAxReviewStatus, PmxAxSource, PmxAxState, PmxAxSteeringMessage, PmxAxWorkItem, PmxAxWorkItemStatus } from './ax-state.js';
6
6
  import type { AxTimelineQuery } from './canvas-db.js';
7
7
  import { searchNodes } from './spatial-analysis.js';
8
8
  import { diffLayouts } from './mutation-history.js';
@@ -134,7 +134,9 @@ export declare class PmxCanvas extends EventEmitter {
134
134
  panned: boolean;
135
135
  } | null;
136
136
  getAxState(): PmxAxState;
137
- getAxContext(): PmxAxContext;
137
+ getAxContext(options?: {
138
+ consumer?: string;
139
+ }): PmxAxContext;
138
140
  setAxFocus(nodeIds: string[], options?: {
139
141
  source?: PmxAxSource;
140
142
  }): PmxAxFocusState;
@@ -253,6 +255,62 @@ export declare class PmxCanvas extends EventEmitter {
253
255
  resolution?: string;
254
256
  source?: PmxAxSource;
255
257
  }): PmxAxModeRequest | null;
258
+ ingestActivity(input: {
259
+ kind: PmxAxActivityKind;
260
+ title: string;
261
+ summary?: string | null;
262
+ outcome?: 'success' | 'failure';
263
+ ref?: string | null;
264
+ nodeIds?: string[];
265
+ data?: Record<string, unknown> | null;
266
+ reactions?: {
267
+ workItem?: false | {
268
+ status?: PmxAxWorkItemStatus;
269
+ detail?: string | null;
270
+ };
271
+ evidence?: false | {
272
+ kind?: PmxAxEvidenceKind;
273
+ body?: string | null;
274
+ };
275
+ review?: false | {
276
+ severity?: PmxAxReviewSeverity;
277
+ kind?: PmxAxReviewKind;
278
+ anchorType?: PmxAxReviewAnchorType;
279
+ nodeId?: string | null;
280
+ };
281
+ };
282
+ }, options?: {
283
+ source?: PmxAxSource;
284
+ }): {
285
+ event: PmxAxEvent;
286
+ workItem: PmxAxWorkItem | null;
287
+ evidence: PmxAxEvidence | null;
288
+ review: PmxAxReviewAnnotation | null;
289
+ };
290
+ getApproval(id: string): PmxAxApprovalGate | null;
291
+ getElicitation(id: string): PmxAxElicitation | null;
292
+ getModeRequest(id: string): PmxAxModeRequest | null;
293
+ awaitApproval(id: string, options?: {
294
+ timeoutMs?: number;
295
+ signal?: AbortSignal;
296
+ }): Promise<{
297
+ approvalGate: PmxAxApprovalGate | null;
298
+ pending: boolean;
299
+ }>;
300
+ awaitElicitation(id: string, options?: {
301
+ timeoutMs?: number;
302
+ signal?: AbortSignal;
303
+ }): Promise<{
304
+ elicitation: PmxAxElicitation | null;
305
+ pending: boolean;
306
+ }>;
307
+ awaitMode(id: string, options?: {
308
+ timeoutMs?: number;
309
+ signal?: AbortSignal;
310
+ }): Promise<{
311
+ modeRequest: PmxAxModeRequest | null;
312
+ pending: boolean;
313
+ }>;
256
314
  getCommandRegistry(): PmxAxCommandDescriptor[];
257
315
  invokeCommand(name: string, args?: Record<string, unknown> | null, options?: {
258
316
  source?: PmxAxSource;
@@ -0,0 +1,65 @@
1
+ # AX host-adapter contract
2
+
3
+ PMX Canvas owns the **AX data layer** — work items, approval gates, steering,
4
+ evidence, review annotations, elicitations, mode requests, the timeline, host
5
+ capabilities, and the tool/prompt policy — over HTTP and MCP. What makes AX
6
+ *interactive* on a given coding harness (GitHub Copilot, Codex, Claude Code, …) is
7
+ a thin **adapter** that wires PMX's neutral surfaces to that harness's lifecycle.
8
+
9
+ "Agnostic" means a documented interface plus PMX-side behavior plus one small
10
+ reference adapter per harness — not zero-adapter magic. The genuinely harness-owned
11
+ acts (waking a turn, per-turn context injection, forwarding native tool hooks,
12
+ native modals) still need a per-harness adapter; PMX owns everything on its side of
13
+ the line (queues, endpoints, schemas, the canvas-surface fallback).
14
+
15
+ ## The interface
16
+
17
+ Every adapter implements as much of this as its host allows; PMX provides the
18
+ surface each one binds to.
19
+
20
+ | Adapter method | PMX surface (owned) | Harness-owned part |
21
+ | --- | --- | --- |
22
+ | `pullContext()` | `GET /api/canvas/ax/context?consumer=<id>` · `canvas://ax-context` — full board **plus** a compact `delivery` lead block | When/where to inject it into the model's turn |
23
+ | `deliverSteer()` | `GET /api/canvas/ax/delivery/pending?consumer=<id>` · `canvas_claim_ax_delivery` → act → `POST …/delivery/<id>/mark` · `canvas_mark_ax_delivery` | Calling the host's native send/wake |
24
+ | `ingestActivity(event)` | `POST /api/canvas/ax/activity` · `canvas_ingest_activity` — board auto-reacts | Forwarding the host's tool/session hooks |
25
+ | `awaitGate(id)` | `GET /api/canvas/ax/{approval\|elicitation\|mode}/<id>?waitMs=` · `canvas_await_*` | Optionally surfacing a native modal; the agent must await PMX |
26
+ | `mirrorLog(event)` *(optional)* | `GET /api/canvas/ax/timeline` · `canvas://ax-timeline` | Writing AX events into the host's own chat/session log |
27
+
28
+ ## Steering is gated, not pushed (#54)
29
+
30
+ A board action (e.g. an `ax.steer` emit from a surface button) enqueues a steering
31
+ message; it does **not** wake the agent. It reaches the next turn only when:
32
+
33
+ 1. **The pin/focus gate is open.** A typical adapter injects `/api/canvas/ax/context`
34
+ only when something is pinned or focused (`pinned.count > 0 || focus.nodeIds.length > 0`).
35
+ A steering board must therefore stay pinned, or its button should also emit
36
+ `ax.focus.set` on the board node, to hold the gate open.
37
+ 2. **A human message fires the turn.** A sandbox button click cannot itself create a
38
+ new agent turn (an app-platform constraint). Any human prompt triggers the injection.
39
+ 3. **The agent acts, then acks.** Injected `pendingSteering` / `pendingActivity` is
40
+ *to-do*, not narration: act on it, then `canvas_mark_ax_delivery` the steering
41
+ (or resolve the work item / gate). Until acked, steering re-injects every gated turn.
42
+
43
+ The `delivery` lead block (`GET /api/canvas/ax/context?consumer=<id>`) is the
44
+ robustness hedge: it's compact and sits above the full dump, so an adapter can inject
45
+ it un-truncated even on a busy board where the full context is clipped.
46
+
47
+ ## The two primitives that close the loop
48
+
49
+ - **Activity ingestion (bidirectional board).** Before, AX was one-directional
50
+ (agent → board). With `ingestActivity`, the agent's *real work* flows back: a failed
51
+ tool becomes a blocked work item + a review finding + evidence without the agent
52
+ remembering to push it. Reactions are kind-driven and overridable per call.
53
+ - **Blocking gates (gates that actually gate).** Before, an approval gate was inert
54
+ data the agent had to poll. With `canvas_await_approval` (and the `?waitMs` HTTP
55
+ long-poll), the agent requests a gate then *blocks* until the human resolves it in
56
+ the browser — real human-in-the-loop control on any harness.
57
+
58
+ ## What stays harness-owned
59
+
60
+ Waking a turn, the exact per-turn injection timing, forwarding native tool hooks, and
61
+ native blocking modals are the host's job — PMX defines the neutral interface and owns
62
+ its side. Model/abort control (`setModel`, `abort`) is intentionally out of scope.
63
+
64
+ See [`docs/http-api.md`](http-api.md) and [`docs/mcp.md`](mcp.md) for the full surface,
65
+ and the per-harness notes under `skills/pmx-canvas/references/`.
package/docs/http-api.md CHANGED
@@ -46,8 +46,18 @@ curl -X POST http://localhost:4313/api/canvas/node \
46
46
  curl -X POST http://localhost:4313/api/canvas/node \
47
47
  -H "Content-Type: application/json" \
48
48
  -d '{"type":"html-primitive","kind":"choice-grid","title":"Options","data":{"items":[{"title":"Small patch","summary":"Least disruption."}]}}'
49
+
50
+ # Opt an html node into AX. Top-level `html` AND `axCapabilities` are accepted on
51
+ # POST add and PATCH update (and may also be nested under `data`).
52
+ curl -X POST http://localhost:4313/api/canvas/node \
53
+ -H "Content-Type: application/json" \
54
+ -d '{"type":"html","title":"AX board","html":"<p>steering board</p>","axCapabilities":{"enabled":true,"allowed":["ax.steer"]}}'
49
55
  ```
50
56
 
57
+ A node creation request must resolve a `type` — pass it in the body (`{ "type":
58
+ ... }`) or as a `?type=` query param. An empty / type-less body returns `400`
59
+ rather than silently creating a markdown node.
60
+
51
61
  ## Edges
52
62
 
53
63
  ```bash
@@ -177,7 +187,9 @@ curl http://localhost:4313/api/canvas/ax/host-capability
177
187
 
178
188
  Validation: `/ax/event` requires a valid `kind` + `summary` (400 otherwise);
179
189
  `/ax/evidence` requires `kind` + `title`; `/ax/steer`, `/ax/work`,
180
- `/ax/approval`, `/ax/review` require their primary field; `PATCH /ax/work/:id`
190
+ `/ax/approval`, `/ax/review` require their primary field; `POST`/`PATCH /ax/work`
191
+ reject an unknown `status` with 400 (the tokens are `todo`, `in-progress`,
192
+ `blocked`, `done`, `cancelled` — hyphens, not underscores); `PATCH /ax/work/:id`
181
193
  and `PATCH /ax/review/:id` return 404 for unknown IDs; approval resolve returns
182
194
  404 if the gate is missing or already resolved.
183
195
 
@@ -218,6 +230,24 @@ curl -X POST http://localhost:4313/api/canvas/ax/mode/<id>/resolve \
218
230
  -d '{"decision":"approved"}'
219
231
  curl http://localhost:4313/api/canvas/ax/mode
220
232
 
233
+ # Activity ingestion — forward an agent tool/session event; the board auto-reacts
234
+ # (kind-driven, overridable: failure → work item + review + evidence; tool-result
235
+ # + outcome:"success" → evidence). Set a reaction to false to suppress it.
236
+ curl -X POST http://localhost:4313/api/canvas/ax/activity \
237
+ -H "Content-Type: application/json" \
238
+ -d '{"kind":"failure","title":"tsc failed","summary":"type error in x.ts","nodeIds":["node-1"],"source":"api"}'
239
+
240
+ # Blocking gate read — read one gate, or long-poll with ?waitMs until the human
241
+ # resolves it in the browser (gates that actually gate). Returns { <primitive>, pending }.
242
+ curl "http://localhost:4313/api/canvas/ax/approval/<id>" # immediate read
243
+ curl "http://localhost:4313/api/canvas/ax/approval/<id>?waitMs=30000" # blocks ≤30s / until resolved
244
+ curl "http://localhost:4313/api/canvas/ax/elicitation/<id>?waitMs=30000"
245
+ curl "http://localhost:4313/api/canvas/ax/mode/<id>?waitMs=30000"
246
+
247
+ # Context — optional ?consumer= filters the compact, loop-safe `delivery` lead block
248
+ # (undelivered steering + open work/approvals it can act on) for per-turn injection.
249
+ curl "http://localhost:4313/api/canvas/ax/context?consumer=copilot"
250
+
221
251
  # Commands — list the registry, invoke a command (records a `command` agent-event)
222
252
  curl http://localhost:4313/api/canvas/ax/command
223
253
  curl -X POST http://localhost:4313/api/canvas/ax/command \
@@ -234,7 +264,9 @@ curl -X POST http://localhost:4313/api/canvas/ax/policy \
234
264
  Validation: `/ax/interaction` returns `{ ok: false, code }` (403 `ax-disabled` /
235
265
  `not-allowed`, 400 `invalid-payload` / `unknown-command`, 404 `unknown-node`);
236
266
  `/ax/command` rejects an unknown command name with 400; `/ax/elicitation/:id/respond`
237
- and `/ax/mode/:id/resolve` return 404 for unknown IDs.
267
+ and `/ax/mode/:id/resolve` return 404 for unknown IDs; `/ax/activity` requires a
268
+ valid `kind` + `title` (400 otherwise); the single-item gate GETs return 404 for
269
+ unknown IDs and clamp `?waitMs` to ≤120000.
238
270
 
239
271
  ## Diagrams (Excalidraw preset)
240
272
 
package/docs/mcp.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MCP reference
2
2
 
3
- PMX Canvas ships an MCP stdio server with **65 tools** + **14 core resources**,
3
+ PMX Canvas ships an MCP stdio server with **69 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.
@@ -72,6 +72,10 @@ searchable and readable in pinned/spatial context.
72
72
  | `canvas_respond_elicitation` | Respond to / resolve a pending elicitation |
73
73
  | `canvas_request_mode` | Request a workflow `mode-request` transition (plan/execute/autonomous) |
74
74
  | `canvas_resolve_mode` | Resolve a pending mode request |
75
+ | `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 |
76
+ | `canvas_await_approval` | Block until an approval gate resolves (human approves/rejects in the browser) or the timeout elapses (`timeoutMs` 0 = immediate read). Gates that actually gate |
77
+ | `canvas_await_elicitation` | Block until an elicitation is answered or the timeout elapses |
78
+ | `canvas_await_mode` | Block until a mode request resolves or the timeout elapses |
75
79
  | `canvas_invoke_command` | Invoke a registry command (`pmx.plan`, `pmx.execute`, `pmx.promote-context`, `pmx.summarize`, `pmx.review`); records a `command` agent-event, unknown names rejected |
76
80
  | `canvas_set_ax_policy` | Patch the canvas-bound tool/prompt policy (`tools.allowed\|excluded\|approvalRequired`, `prompt.systemAppend\|mode`); patches merge and are normalized |
77
81
  | `canvas_pin_nodes` | Pin nodes to include in agent context |
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmx-canvas",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "description": "Spatial canvas workbench for coding agents — infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
5
5
  "type": "module",
6
6
  "main": "./src/server/index.ts",
@@ -843,6 +843,12 @@ Eligible nodes can emit one normalized, validated AX interaction that maps onto
843
843
  AX operation — work item, evidence, approval, review, focus, steering, event,
844
844
  elicitation, or mode request. One envelope, many transports:
845
845
 
846
+ This is the **agent-native nodes** model: existing canvas node types become
847
+ interactive agent controls when their AX capabilities allow it. Do not describe
848
+ this as a separate node type; it is a capability layer on top of markdown,
849
+ status, HTML, json-render, graph, web-artifact, MCP app, and other supported
850
+ nodes.
851
+
846
852
  - **Endpoint:** `POST /api/canvas/ax/interaction` with
847
853
  `{ type, sourceNodeId, payload }` (MCP: `canvas_ax_interaction`; CLI:
848
854
  `pmx-canvas ax interaction`). Returns `{ ok, primitive }` or
@@ -857,10 +863,12 @@ elicitation, or mode request. One envelope, many transports:
857
863
  before submitting: `html` / `html-primitive` nodes (when opted in) call
858
864
  `window.PMX_AX.emit(type, payload)`; **json-render / graph** viewers forward a
859
865
  spec action named after an AX type (e.g. `on.press → { action:
860
- "ax.work.create", params }`, `sourceSurface: 'json-render'`); opted-in ext-app
861
- **`mcp-app`** nodes get the same `window.PMX_AX.emit` injected
862
- (`sourceSurface: 'mcp-app'`). The server re-validates capabilities regardless
863
- of transport bridges are convenience, not a trust boundary.
866
+ "ax.work.create", params }`, `sourceSurface: 'json-render'`); web-artifact
867
+ **`mcp-app`** nodes use the same parent bridge; external MCP app frames
868
+ (`mode: "ext-app"`) can emit through an injected `window.PMX_AX.emit` with
869
+ Promise acknowledgements, but do not get the read-state bridge. The server
870
+ re-validates capabilities regardless of transport — bridges are convenience,
871
+ not a trust boundary.
864
872
  - **Delivery (adapterless):** `canvas://ax-pending-steering` /
865
873
  `canvas_claim_ax_delivery` return two things, both loop-safe (a consumer never
866
874
  receives items it originated):
@@ -877,6 +885,26 @@ elicitation, or mode request. One envelope, many transports:
877
885
  live. Clients that **poll** instead should poll `canvas_claim_ax_delivery` —
878
886
  `pendingActivity` is how non-steering browser changes reach them. Only steering
879
887
  flows through the claim/ack queue.
888
+ - **Steering is gated, not pushed.** A surface button that emits `ax.steer`
889
+ enqueues a steer — it does NOT wake the agent. With a prompt-injecting host
890
+ adapter (e.g. Copilot), it reaches the next turn only when (1) the **pin/focus
891
+ gate is open** (something pinned or focused — so keep a steering board pinned, or
892
+ have its button also emit `ax.focus.set` on itself), (2) a **human message** fires
893
+ the turn, and (3) the agent **acts then acks** (`canvas_mark_ax_delivery`), or the
894
+ steer re-injects every gated turn. `GET /api/canvas/ax/context?consumer=<id>` adds
895
+ a compact, loop-safe `delivery: { pendingSteering, pendingActivity }` lead block an
896
+ adapter can inject un-truncated, so steering survives the full-context char clip.
897
+ - **Activity ingestion (bidirectional board):** a host adapter forwards the agent's
898
+ tool/session events with `canvas_ingest_activity` (HTTP `POST /api/canvas/ax/activity`)
899
+ and the board auto-reacts — `failure`/`error` (or `outcome:"failure"`) → a blocked
900
+ work item + a review finding + `logs` evidence; `tool-result` + `outcome:"success"` →
901
+ `tool-result` evidence; everything else records a timeline event only. Override or
902
+ suppress per call via `reactions` (`{ workItem: false }`, `{ review: { severity } }`, …).
903
+ - **Blocking gates (gates that actually gate):** after requesting an approval /
904
+ elicitation / mode, `canvas_await_approval` / `canvas_await_elicitation` /
905
+ `canvas_await_mode` (HTTP `GET /api/canvas/ax/<kind>/<id>?waitMs=`) BLOCK until the
906
+ human resolves it in the browser or the timeout elapses (`timeoutMs` 0 = immediate
907
+ read; ≤120000). Use this to pause real work on a human decision instead of polling.
880
908
  - **Elicitation / mode:** request structured human input
881
909
  (`canvas_request_elicitation` → `canvas_respond_elicitation`) or a workflow
882
910
  mode transition (`canvas_request_mode` → `canvas_resolve_mode`); both are
@@ -916,8 +944,10 @@ AX interactions are gated per node type. The lists below are each type's **ceili
916
944
  | `webpage` | `ax.evidence.add`, `ax.review.add`, `ax.focus.set`, `ax.event.record` |
917
945
  | `group` | `ax.focus.set`, `ax.work.create`, `ax.command.invoke`, `ax.event.record` |
918
946
 
919
- **Opt-in** — set `data.axCapabilities.enabled = true` (MCP: pass `axCapabilities` to
920
- `canvas_add_html_node` or `canvas_update_node`; HTTP: nest under `data`):
947
+ **Opt-in** — set `axCapabilities.enabled = true` (MCP: pass `axCapabilities` to
948
+ `canvas_add_html_node` / `canvas_update_node`. HTTP: `axCapabilities` **and** the
949
+ `html` body are accepted **top-level on both `POST /api/canvas/node` and
950
+ `PATCH /api/canvas/node/<id>`**, or nested under `data` — both work, top-level wins):
921
951
 
922
952
  | Node type | Allowed AX interaction types |
923
953
  |-----------|------------------------------|
@@ -928,7 +958,9 @@ AX interactions are gated per node type. The lists below are each type's **ceili
928
958
  only, no human-facing emit.
929
959
 
930
960
  The 11 interaction types and what they create: `ax.work.create` / `ax.work.update`
931
- (work-queue items, status todoin-progressblocked→done→cancelled), `ax.evidence.add`
961
+ (work-queue items; status is exactly one of `todo`, `in-progress`, `blocked`, `done`,
962
+ `cancelled` — **hyphens, not underscores**; `POST`/`PATCH /api/canvas/ax/work` reject an
963
+ unknown token like `in_progress` with `400`), `ax.evidence.add`
932
964
  (timeline evidence), `ax.review.add` (review annotation), `ax.focus.set` (agent focus
933
965
  pointer), `ax.steer` (a steering message delivered to the agent), `ax.approval.request`
934
966
  (approval gate), `ax.elicitation.request` (structured human input), `ax.mode.request`
@@ -949,6 +981,12 @@ state. The read side mirrors the write side:
949
981
  - **Emit (write):** in `html`, call `window.PMX_AX.emit("ax.work.create", { title })`;
950
982
  in `json-render`, bind a control action named after the AX type
951
983
  (`on: { press: { action: "ax.work.create", params: { title } } }`).
984
+ - **Confirm (#55):** for `html` / `html-primitive` and PMX_AX-enabled `mcp-app`
985
+ surfaces, `emit` returns a Promise that resolves with the result once the
986
+ canvas acks it, so a button can self-confirm: `const r = await
987
+ window.PMX_AX.emit(...); if (r.ok) showQueued();`. You can also
988
+ `window.PMX_AX.on('ack', cb)` or listen for the `pmx-ax-ack` event. (Falls back
989
+ to an `ax-ack-timeout` result after 10s, so `await` never hangs.)
952
990
  - **Reflect (read):** the canvas seeds the surface with a compact AX snapshot at
953
991
  load (the same shape as `GET /api/canvas/ax/surface-snapshot`) and live-updates it
954
992
  as AX state changes. Works on all three authored surface types:
@@ -966,11 +1004,15 @@ state. The read side mirrors the write side:
966
1004
  Minimal html work board (drop-in via `canvas_add_html_node`, `axCapabilities.enabled: true`):
967
1005
 
968
1006
  ```html
969
- <button onclick="window.PMX_AX.emit('ax.work.create',{title:'New task'})">+ Task</button>
1007
+ <button id="add">+ Task</button> <span id="ok"></span>
970
1008
  <ul id="q"></ul>
971
1009
  <script>
972
1010
  function render(s){ document.getElementById('q').innerHTML =
973
1011
  ((s&&s.workItems)||[]).map(w => '<li>['+w.status+'] '+w.title+'</li>').join(''); }
1012
+ document.getElementById('add').onclick = async () => {
1013
+ const r = await window.PMX_AX.emit('ax.work.create',{title:'New task'});
1014
+ document.getElementById('ok').textContent = r && r.ok ? 'queued ✓' : 'failed'; // #55 self-confirm
1015
+ };
974
1016
  render(window.PMX_AX && window.PMX_AX.state);
975
1017
  window.addEventListener('pmx-ax-update', e => render(e.detail));
976
1018
  </script>
@@ -86,10 +86,24 @@ against `canvas_set_ax_focus`.
86
86
  Codex agents should treat PMX AX context as host-native working context:
87
87
 
88
88
  - `canvas://pinned-context` is the explicit human-curated node set.
89
- - `canvas://ax-context` combines pins, focus, and surface metadata.
89
+ - `canvas://ax-context` combines pins, focus, and surface metadata, plus a compact
90
+ loop-safe `delivery: { pendingSteering, pendingActivity }` lead block
91
+ (`GET /api/canvas/ax/context?consumer=codex` filters out Codex-originated items).
90
92
  - `canvas_get_ax` returns both persisted AX state and agent-ready context.
91
93
  - Focus is a current attention target, not a command to ignore the rest of the repository.
92
94
 
95
+ The adapterless MCP+Browser path is poll-based: there is no automatic prompt injection,
96
+ so a board click does not wake the current turn. Codex agents poll
97
+ `canvas_claim_ax_delivery` (steering + `pendingActivity`) and act/ack explicitly. The
98
+ loop-closing surfaces work over MCP today even without a dedicated extension:
99
+
100
+ - **Self-report work** with `canvas_ingest_activity` (the board auto-reacts: a failed
101
+ tool → a blocked work item + review + evidence). Automatic forwarding of Codex's own
102
+ tool hooks would need a Codex adapter; manual ingestion works now.
103
+ - **Block on a decision** with `canvas_await_approval` / `canvas_await_elicitation` /
104
+ `canvas_await_mode` (they long-poll PMX until the human resolves the gate in the
105
+ Browser or the timeout elapses) instead of looping on `canvas_get_ax`.
106
+
93
107
  ## Live-Test Checklist
94
108
 
95
109
  1. Open `http://127.0.0.1:4313/workbench` in the Codex in-app Browser first so the user can see