pmx-canvas 0.4.8 → 0.5.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 (212) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +154 -5
  2. package/.github/extensions/pmx-canvas/steering-delivery.mjs +79 -0
  3. package/CHANGELOG.md +156 -0
  4. package/Readme.md +72 -23
  5. package/dist/canvas/global.css +3071 -796
  6. package/dist/canvas/index.js +122 -104
  7. package/dist/json-render/index.css +1 -1
  8. package/dist/types/cli/agent.d.ts +1 -0
  9. package/dist/types/cli/commands/pump.d.ts +9 -0
  10. package/dist/types/client/App.d.ts +1 -1
  11. package/dist/types/client/canvas/AgentPresenceLayer.d.ts +11 -0
  12. package/dist/types/client/canvas/BarHint.d.ts +22 -0
  13. package/dist/types/client/canvas/CanvasNode.d.ts +5 -0
  14. package/dist/types/client/canvas/CanvasViewport.d.ts +4 -4
  15. package/dist/types/client/canvas/CommandBar.d.ts +10 -0
  16. package/dist/types/client/canvas/ConnectionBanner.d.ts +13 -0
  17. package/dist/types/client/canvas/ContextMenu.d.ts +6 -0
  18. package/dist/types/client/canvas/EdgeLayer.d.ts +2 -1
  19. package/dist/types/client/canvas/EmptyState.d.ts +11 -0
  20. package/dist/types/client/canvas/ExternalWriters.d.ts +5 -0
  21. package/dist/types/client/canvas/HumanPresenceLayer.d.ts +8 -0
  22. package/dist/types/client/canvas/Minimap.d.ts +12 -1
  23. package/dist/types/client/canvas/ScopeFenceLayer.d.ts +8 -0
  24. package/dist/types/client/canvas/SelectionBar.d.ts +6 -0
  25. package/dist/types/client/canvas/SessionPanel.d.ts +1 -0
  26. package/dist/types/client/canvas/SessionReceipt.d.ts +17 -0
  27. package/dist/types/client/canvas/TextPrompt.d.ts +25 -0
  28. package/dist/types/client/canvas/ToolRail.d.ts +21 -0
  29. package/dist/types/client/canvas/TopBar.d.ts +7 -0
  30. package/dist/types/client/canvas/canvas-area.d.ts +39 -0
  31. package/dist/types/client/canvas/create-in-view.d.ts +18 -0
  32. package/dist/types/client/canvas/import-files.d.ts +8 -0
  33. package/dist/types/client/canvas/kind-colors.d.ts +3 -0
  34. package/dist/types/client/canvas/use-focus-trap.d.ts +14 -0
  35. package/dist/types/client/canvas/use-now.d.ts +6 -0
  36. package/dist/types/client/icons.d.ts +11 -4
  37. package/dist/types/client/nodes/ExtAppFrame.d.ts +2 -0
  38. package/dist/types/client/nodes/GroupNode.d.ts +6 -1
  39. package/dist/types/client/nodes/MarkdownNode.d.ts +2 -0
  40. package/dist/types/client/nodes/inline-editor-commands.d.ts +3 -4
  41. package/dist/types/client/state/canvas-store.d.ts +49 -5
  42. package/dist/types/client/state/human-store.d.ts +26 -0
  43. package/dist/types/client/state/iframe-mode.d.ts +1 -0
  44. package/dist/types/client/state/intent-bridge.d.ts +37 -5
  45. package/dist/types/client/state/node-factory.d.ts +0 -1
  46. package/dist/types/client/state/presence-store.d.ts +78 -0
  47. package/dist/types/client/state/session-store.d.ts +176 -0
  48. package/dist/types/client/types.d.ts +2 -1
  49. package/dist/types/client/utils/platform.d.ts +2 -0
  50. package/dist/types/server/agent-presence.d.ts +192 -0
  51. package/dist/types/server/ax-gate-ttl.d.ts +6 -0
  52. package/dist/types/server/ax-interaction.d.ts +4 -10
  53. package/dist/types/server/ax-state-manager.d.ts +21 -2
  54. package/dist/types/server/ax-state.d.ts +49 -3
  55. package/dist/types/server/ax-wait.d.ts +13 -0
  56. package/dist/types/server/canvas-db.d.ts +9 -1
  57. package/dist/types/server/canvas-operations.d.ts +11 -0
  58. package/dist/types/server/canvas-state.d.ts +27 -13
  59. package/dist/types/server/canvas-validation.d.ts +0 -14
  60. package/dist/types/server/html-surface.d.ts +1 -1
  61. package/dist/types/server/human-presence.d.ts +40 -0
  62. package/dist/types/server/index.d.ts +29 -3
  63. package/dist/types/server/intent-registry.d.ts +7 -0
  64. package/dist/types/server/mutation-history.d.ts +15 -2
  65. package/dist/types/server/operations/invoker.d.ts +13 -2
  66. package/dist/types/server/operations/ops/ax-presence.d.ts +2 -0
  67. package/dist/types/server/operations/ops/human-presence.d.ts +3 -0
  68. package/dist/types/server/operations/registry.d.ts +11 -0
  69. package/dist/types/server/operations/types.d.ts +1 -1
  70. package/dist/types/server/scope-fence.d.ts +62 -0
  71. package/dist/types/server/server.d.ts +0 -12
  72. package/dist/types/shared/agent-presence.d.ts +130 -0
  73. package/dist/types/shared/approval-gates.d.ts +19 -0
  74. package/dist/types/shared/auto-arrange.d.ts +0 -1
  75. package/dist/types/shared/ax-kinds.d.ts +11 -0
  76. package/dist/types/shared/human-presence.d.ts +31 -0
  77. package/dist/types/shared/iframe-probe.d.ts +8 -0
  78. package/dist/types/shared/scope-fence.d.ts +21 -0
  79. package/docs/RELEASE.md +20 -0
  80. package/docs/ax-host-adapter-contract.md +30 -14
  81. package/docs/cli.md +37 -1
  82. package/docs/environment.md +3 -0
  83. package/docs/http-api.md +222 -5
  84. package/docs/mcp.md +6 -5
  85. package/docs/node-types.md +3 -1
  86. package/docs/plans/plan-009-tech-debt-backlog.md +13 -0
  87. package/docs/screenshot.png +0 -0
  88. package/docs/screenshots/multi-agent-live.png +0 -0
  89. package/docs/screenshots/workbench-live-dark.png +0 -0
  90. package/docs/screenshots/workbench-live-light.png +0 -0
  91. package/docs/sdk.md +1 -1
  92. package/package.json +4 -4
  93. package/skills/pmx-canvas/SKILL.md +112 -17
  94. package/skills/pmx-canvas/references/codex-app-adapter.md +28 -0
  95. package/skills/pmx-canvas/references/full-reference.md +55 -16
  96. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +41 -24
  97. package/skills/pmx-canvas-orchestration/SKILL.md +56 -2
  98. package/skills/pmx-canvas-testing/SKILL.md +17 -0
  99. package/src/cli/agent.ts +1 -0
  100. package/src/cli/commands/copilot.ts +15 -8
  101. package/src/cli/commands/nodes.ts +1 -19
  102. package/src/cli/commands/pump.ts +263 -0
  103. package/src/cli/index.ts +2 -5
  104. package/src/cli/shared.ts +1 -1
  105. package/src/client/App.tsx +209 -585
  106. package/src/client/canvas/AgentPresenceLayer.tsx +74 -0
  107. package/src/client/canvas/AttentionHistory.tsx +0 -10
  108. package/src/client/canvas/BarHint.tsx +49 -0
  109. package/src/client/canvas/CanvasNode.tsx +488 -136
  110. package/src/client/canvas/CanvasViewport.tsx +190 -94
  111. package/src/client/canvas/CommandBar.tsx +140 -0
  112. package/src/client/canvas/CommandPalette.tsx +133 -58
  113. package/src/client/canvas/ConnectionBanner.tsx +65 -0
  114. package/src/client/canvas/ContextMenu.tsx +88 -27
  115. package/src/client/canvas/ContextPinBar.tsx +2 -2
  116. package/src/client/canvas/EdgeLayer.tsx +121 -38
  117. package/src/client/canvas/EmptyState.tsx +93 -0
  118. package/src/client/canvas/ExpandedNodeOverlay.tsx +64 -118
  119. package/src/client/canvas/ExternalWriters.tsx +308 -0
  120. package/src/client/canvas/FocusFieldLayer.tsx +12 -1
  121. package/src/client/canvas/HumanPresenceLayer.tsx +45 -0
  122. package/src/client/canvas/IntentLayer.tsx +1 -1
  123. package/src/client/canvas/Minimap.tsx +117 -190
  124. package/src/client/canvas/ScopeFenceLayer.tsx +38 -0
  125. package/src/client/canvas/SelectionBar.tsx +175 -23
  126. package/src/client/canvas/SessionPanel.tsx +481 -0
  127. package/src/client/canvas/SessionReceipt.tsx +141 -0
  128. package/src/client/canvas/ShortcutOverlay.tsx +21 -6
  129. package/src/client/canvas/SnapshotPanel.tsx +166 -80
  130. package/src/client/canvas/TextPrompt.tsx +128 -0
  131. package/src/client/canvas/ToolRail.tsx +447 -0
  132. package/src/client/canvas/TopBar.tsx +308 -0
  133. package/src/client/canvas/auto-fit.ts +1 -5
  134. package/src/client/canvas/canvas-area.ts +74 -0
  135. package/src/client/canvas/create-in-view.ts +48 -0
  136. package/src/client/canvas/import-files.ts +64 -0
  137. package/src/client/canvas/kind-colors.ts +22 -0
  138. package/src/client/canvas/snap-guides.ts +1 -1
  139. package/src/client/canvas/use-focus-trap.ts +54 -0
  140. package/src/client/canvas/use-now.ts +17 -0
  141. package/src/client/canvas/use-pan-zoom.ts +13 -2
  142. package/src/client/icons.tsx +57 -18
  143. package/src/client/nodes/ExtAppFrame.tsx +86 -20
  144. package/src/client/nodes/GroupNode.tsx +9 -28
  145. package/src/client/nodes/InlineFormatBar.tsx +4 -3
  146. package/src/client/nodes/InlineMarkdownEditor.tsx +27 -0
  147. package/src/client/nodes/MarkdownNode.tsx +59 -24
  148. package/src/client/nodes/MdFormatBar.tsx +2 -4
  149. package/src/client/nodes/inline-editor-commands.ts +17 -6
  150. package/src/client/nodes/use-surface-frame.ts +3 -1
  151. package/src/client/state/attention-bridge.ts +3 -0
  152. package/src/client/state/canvas-store.ts +285 -90
  153. package/src/client/state/human-store.ts +175 -0
  154. package/src/client/state/iframe-mode.ts +33 -9
  155. package/src/client/state/intent-bridge.ts +75 -13
  156. package/src/client/state/node-factory.ts +2 -8
  157. package/src/client/state/presence-store.ts +168 -0
  158. package/src/client/state/session-store.ts +419 -0
  159. package/src/client/state/sse-bridge.ts +93 -30
  160. package/src/client/theme/global.css +3071 -796
  161. package/src/client/types.ts +3 -1
  162. package/src/client/utils/platform.ts +6 -1
  163. package/src/mcp/canvas-access.ts +2 -2
  164. package/src/server/agent-presence.ts +755 -0
  165. package/src/server/ax-context.ts +45 -0
  166. package/src/server/ax-gate-ttl.ts +61 -0
  167. package/src/server/ax-interaction.ts +29 -1
  168. package/src/server/ax-state-manager.ts +78 -6
  169. package/src/server/ax-state.ts +82 -15
  170. package/src/server/ax-wait.ts +33 -0
  171. package/src/server/canvas-db.ts +103 -33
  172. package/src/server/canvas-operations.ts +58 -14
  173. package/src/server/canvas-state.ts +131 -58
  174. package/src/server/canvas-validation.ts +3 -53
  175. package/src/server/demo-state.json +2 -70
  176. package/src/server/html-surface.ts +11 -2
  177. package/src/server/human-presence.ts +160 -0
  178. package/src/server/index.ts +111 -6
  179. package/src/server/intent-registry.ts +13 -0
  180. package/src/server/mutation-history.ts +47 -2
  181. package/src/server/operations/composites.ts +6 -3
  182. package/src/server/operations/http.ts +10 -1
  183. package/src/server/operations/index.ts +4 -0
  184. package/src/server/operations/invoker.ts +33 -6
  185. package/src/server/operations/ops/ax-presence.ts +62 -0
  186. package/src/server/operations/ops/ax-read.ts +8 -0
  187. package/src/server/operations/ops/ax-state.ts +106 -1
  188. package/src/server/operations/ops/ax-timeline.ts +54 -5
  189. package/src/server/operations/ops/ax-work.ts +46 -0
  190. package/src/server/operations/ops/batch.ts +3 -1
  191. package/src/server/operations/ops/edges.ts +88 -2
  192. package/src/server/operations/ops/groups.ts +4 -3
  193. package/src/server/operations/ops/human-presence.ts +35 -0
  194. package/src/server/operations/ops/json-render.ts +11 -1
  195. package/src/server/operations/ops/nodes.ts +77 -16
  196. package/src/server/operations/ops/query.ts +1 -0
  197. package/src/server/operations/ops/snapshots.ts +7 -4
  198. package/src/server/operations/registry.ts +196 -1
  199. package/src/server/operations/types.ts +1 -1
  200. package/src/server/scope-fence.ts +207 -0
  201. package/src/server/server.ts +177 -79
  202. package/src/server/trace-manager.ts +0 -2
  203. package/src/server/web-artifacts.ts +0 -1
  204. package/src/shared/agent-presence.ts +162 -0
  205. package/src/shared/approval-gates.ts +35 -0
  206. package/src/shared/auto-arrange.ts +121 -13
  207. package/src/shared/ax-kinds.ts +23 -0
  208. package/src/shared/human-presence.ts +30 -0
  209. package/src/shared/iframe-probe.ts +8 -0
  210. package/src/shared/scope-fence.ts +31 -0
  211. package/dist/types/client/canvas/DockedNode.d.ts +0 -4
  212. package/src/client/canvas/DockedNode.tsx +0 -165
package/src/cli/agent.ts CHANGED
@@ -28,6 +28,7 @@ import './commands/query.js';
28
28
  import './commands/pins.js';
29
29
  import './commands/history.js';
30
30
  import './commands/ax.js';
31
+ import './commands/pump.js';
31
32
  import './commands/webview.js';
32
33
  import './commands/apps.js';
33
34
  import './commands/copilot.js';
@@ -1,7 +1,7 @@
1
1
  // Copilot adapter commands: copilot install-extension.
2
2
 
3
3
  import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
4
- import { dirname, join } from 'node:path';
4
+ import { basename, dirname, join } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { cmd, die, getStringFlag, output, parseFlags, showCommandHelp } from '../shared.js';
7
7
 
@@ -17,18 +17,23 @@ cmd(
17
17
  const { flags } = parseFlags(args);
18
18
  if (flags.help || flags.h) return showCommandHelp('copilot install-extension');
19
19
 
20
- const sourcePath = fileURLToPath(new URL('../../../.github/extensions/pmx-canvas/extension.mjs', import.meta.url));
21
- if (!existsSync(sourcePath)) {
22
- die('Bundled Copilot extension adapter not found.', `Expected at ${sourcePath}`);
20
+ const sourcePaths = [
21
+ fileURLToPath(new URL('../../../.github/extensions/pmx-canvas/extension.mjs', import.meta.url)),
22
+ fileURLToPath(new URL('../../../.github/extensions/pmx-canvas/steering-delivery.mjs', import.meta.url)),
23
+ ];
24
+ const missingSource = sourcePaths.find((sourcePath) => !existsSync(sourcePath));
25
+ if (missingSource) {
26
+ die('Bundled Copilot extension adapter not found.', `Expected at ${missingSource}`);
23
27
  }
24
28
 
25
29
  const targetPath =
26
30
  getStringFlag(flags, 'target') ?? join(process.cwd(), '.github', 'extensions', 'pmx-canvas', 'extension.mjs');
31
+ const targetPaths = sourcePaths.map((sourcePath) => join(dirname(targetPath), basename(sourcePath)));
27
32
  const dryRun = flags['dry-run'] === true;
28
- const targetExists = existsSync(targetPath);
33
+ const targetExists = targetPaths.some((candidate) => existsSync(candidate));
29
34
 
30
35
  if (dryRun) {
31
- output({ ok: true, dryRun: true, sourcePath, targetPath, targetExists, wrote: false });
36
+ output({ ok: true, dryRun: true, sourcePath: sourcePaths[0], targetPath, targetExists, wrote: false });
32
37
  return;
33
38
  }
34
39
 
@@ -37,7 +42,9 @@ cmd(
37
42
  }
38
43
 
39
44
  mkdirSync(dirname(targetPath), { recursive: true });
40
- copyFileSync(sourcePath, targetPath);
41
- output({ ok: true, dryRun: false, sourcePath, targetPath, wrote: true });
45
+ for (let index = 0; index < sourcePaths.length; index += 1) {
46
+ copyFileSync(sourcePaths[index], targetPaths[index]);
47
+ }
48
+ output({ ok: true, dryRun: false, sourcePath: sourcePaths[0], targetPath, wrote: true });
42
49
  },
43
50
  );
@@ -124,7 +124,6 @@ function summarizeNodeResult(node: Record<string, unknown>): Record<string, unkn
124
124
  ...(node.size !== undefined ? { size: node.size } : {}),
125
125
  ...(node.collapsed !== undefined ? { collapsed: node.collapsed } : {}),
126
126
  ...(node.pinned !== undefined ? { pinned: node.pinned } : {}),
127
- ...(node.dockPosition !== undefined ? { dockPosition: node.dockPosition } : {}),
128
127
  ...(node.path !== undefined ? { path: node.path } : {}),
129
128
  ...(node.url !== undefined ? { url: node.url } : {}),
130
129
  ...(node.provenance !== undefined ? { provenance: node.provenance } : {}),
@@ -827,8 +826,6 @@ cmd(
827
826
  'pmx-canvas node update <node-id> --spec-file ./dashboard.json',
828
827
  'pmx-canvas node update <graph-id> --data-file ./metrics.json --chart-height 420',
829
828
  'pmx-canvas node update <node-id> --pinned true',
830
- 'pmx-canvas node update <node-id> --dock-position right',
831
- 'pmx-canvas node update <node-id> --dock-position none # undock back to the canvas',
832
829
  'pmx-canvas node update <node-id> --lock-arrange',
833
830
  'pmx-canvas node update <node-id> --width 840 --full # full node payload instead of the compact envelope',
834
831
  ],
@@ -910,25 +907,10 @@ cmd(
910
907
 
911
908
  if (pinned !== undefined) body.pinned = pinned;
912
909
 
913
- // --dock-position left|right|none : dock a node into the top HUD or undock it.
914
- // `none`/`null`/empty map to JS null (undock). Assigned with a !== undefined
915
- // guard so the null survives JSON.stringify to the server (which accepts a
916
- // top-level dockPosition: null). HTTP PATCH already supports this; this is the
917
- // CLI path the report (#40) found missing.
918
- const dockRaw = getStringFlag(flags, 'dock-position', 'dockPosition');
919
- let dockPosition: 'left' | 'right' | null | undefined;
920
- if (dockRaw !== undefined) {
921
- const v = dockRaw.trim().toLowerCase();
922
- if (v === 'left' || v === 'right') dockPosition = v;
923
- else if (v === 'none' || v === 'null' || v === '') dockPosition = null;
924
- else die(`Invalid --dock-position "${dockRaw}".`, 'Use left, right, or none (to undock).');
925
- }
926
- if (dockPosition !== undefined) body.dockPosition = dockPosition;
927
-
928
910
  if (Object.keys(body).length === 0) {
929
911
  die(
930
912
  'No updates specified',
931
- 'Use --title, --content, --x, --y, --width, --height, --strict-size, --pinned, --dock-position, trace fields, --lock-arrange, --unlock-arrange, or --stdin',
913
+ 'Use --title, --content, --x, --y, --width, --height, --strict-size, --pinned, trace fields, --lock-arrange, --unlock-arrange, or --stdin',
932
914
  );
933
915
  }
934
916
 
@@ -0,0 +1,263 @@
1
+ /**
2
+ * `pmx-canvas pump` — the generic reactive steering loop for hosts without a
3
+ * native adapter (Amp, Codex, any CLI agent). Long-polls the per-consumer
4
+ * delivery claim and hands each steer to a shell command; the command's exit
5
+ * code gates the per-consumer mark. This is the same design as the Copilot
6
+ * extension's delivery pump (send-then-mark, awaiting-mark memory, startup
7
+ * backlog watermark, old-server backoff) with the "send into the host" step
8
+ * generalized to "run your command".
9
+ *
10
+ * Safety: the steer text reaches the command as env (PMX_STEER_MESSAGE) and
11
+ * stdin — never spliced into the shell string — so hostile steer content
12
+ * cannot inject into the command line. `{message}` in the template expands to
13
+ * the SHELL VARIABLE reference "$PMX_STEER_MESSAGE" (quoted), not the text.
14
+ * On Windows that guarantee does not hold — cmd.exe expands `%VAR%` and then
15
+ * re-parses the result — so the placeholders are refused there and the command
16
+ * takes the message from stdin instead. See `renderExecTemplate`.
17
+ */
18
+ import { spawn } from 'node:child_process';
19
+ import {
20
+ cmd,
21
+ getBaseUrl,
22
+ getStringFlag,
23
+ invokeOperation,
24
+ parseFlags,
25
+ requireFlag,
26
+ showCommandHelp,
27
+ } from '../shared.js';
28
+
29
+ interface PendingSteer {
30
+ id: string;
31
+ message: string;
32
+ source?: string | null;
33
+ target?: string | null;
34
+ createdAt?: string | null;
35
+ }
36
+
37
+ async function claimPending(consumer: string, waitMs: number): Promise<{ pending: PendingSteer[]; tookMs: number }> {
38
+ const started = Date.now();
39
+ const url = `${getBaseUrl()}/api/canvas/ax/delivery/pending?consumer=${encodeURIComponent(consumer)}&limit=1&order=oldest${waitMs > 0 ? `&waitMs=${waitMs}` : ''}`;
40
+ const res = await fetch(url, { signal: AbortSignal.timeout(waitMs + 15_000) });
41
+ if (!res.ok) throw new Error(`pending claim failed: HTTP ${res.status}`);
42
+ const body = (await res.json()) as { pending?: PendingSteer[] };
43
+ return { pending: Array.isArray(body.pending) ? body.pending : [], tookMs: Date.now() - started };
44
+ }
45
+
46
+ async function markDelivered(id: string, consumer: string): Promise<boolean> {
47
+ const result = (await invokeOperation('ax.delivery.mark', { id, consumer })) as { delivered?: boolean };
48
+ return result.delivered === true;
49
+ }
50
+
51
+ /**
52
+ * Windows has no `sh`, so the exec ran nowhere there. cmd.exe can host the
53
+ * stdin pattern, but NOT the `{message}` / `{id}` placeholders: those expand to
54
+ * an env-var reference so hostile steer content is never spliced into the
55
+ * command string, and cmd.exe expands `%VAR%` before re-parsing — a message
56
+ * containing `& del …` would run. Refuse the placeholders there instead of
57
+ * silently reintroducing the injection the substitution exists to prevent.
58
+ */
59
+ export function renderExecTemplate(template: string, windows = process.platform === 'win32'): string {
60
+ if (windows) {
61
+ if (template.includes('{message}') || template.includes('{id}')) {
62
+ throw new Error(
63
+ 'pmx-canvas pump: {message} and {id} are not supported on Windows — cmd.exe re-parses expanded variables, so a steer could inject commands. Read the message from stdin instead (the exec receives it), or use the PMX_STEER_MESSAGE / PMX_STEER_ID environment variables inside your own script.',
64
+ );
65
+ }
66
+ return template;
67
+ }
68
+ return template.replaceAll('{message}', '"$PMX_STEER_MESSAGE"').replaceAll('{id}', '"$PMX_STEER_ID"');
69
+ }
70
+
71
+ function runExec(template: string, steer: PendingSteer, consumer: string): Promise<number> {
72
+ const rendered = renderExecTemplate(template);
73
+ return new Promise((resolve) => {
74
+ // `shell: true` picks sh on POSIX and ComSpec/cmd.exe on Windows, and sets
75
+ // windowsVerbatimArguments itself — hand-rolling the cmd.exe form escaped
76
+ // the quotes inside the command, so `bun "C:\path\x.mjs"` arrived as
77
+ // `""C:\path\x.mjs""`. Let Node own the platform quoting.
78
+ const child = spawn(rendered, {
79
+ shell: true,
80
+ stdio: ['pipe', 'inherit', 'inherit'],
81
+ env: {
82
+ ...process.env,
83
+ PMX_STEER_MESSAGE: steer.message,
84
+ PMX_STEER_ID: steer.id,
85
+ PMX_STEER_SOURCE: steer.source ?? '',
86
+ // Delivery envelope (round-2 review): the command can tell the host
87
+ // WHO steered, whether it was addressed or a broadcast, and how old
88
+ // the steer is — without re-querying the board.
89
+ PMX_STEER_TARGET: steer.target ?? 'ALL',
90
+ PMX_STEER_CREATED_AT: steer.createdAt ?? '',
91
+ PMX_STEER_CONSUMER: consumer,
92
+ },
93
+ });
94
+ child.stdin.write(steer.message);
95
+ child.stdin.end();
96
+ child.on('close', (code) => resolve(code ?? 1));
97
+ child.on('error', () => resolve(1));
98
+ });
99
+ }
100
+
101
+ async function setPresence(consumer: string, label: string, parent: string | null, patch: Record<string, unknown>) {
102
+ try {
103
+ await invokeOperation('ax.presence.set', {
104
+ source: 'sdk',
105
+ agentId: consumer,
106
+ label,
107
+ ...(parent ? { parentAgentId: parent } : {}),
108
+ ...patch,
109
+ });
110
+ } catch {
111
+ // presence is best-effort — the pump must survive a rolling daemon
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Settle back to idle ONLY when the visible phase is still the pump's own
117
+ * `tooling` marker. The host the pump feeds may have set a richer explicit
118
+ * phase (thinking, waiting-approval) while handling the steer — an
119
+ * unconditional idle stomped it and active work read as Idle (0.5.0
120
+ * readiness, Copilot finding).
121
+ */
122
+ async function settlePresence(consumer: string, label: string, parent: string | null): Promise<void> {
123
+ try {
124
+ const res = await fetch(`${getBaseUrl()}/api/canvas/ax/presence`, { signal: AbortSignal.timeout(5_000) });
125
+ if (res.ok) {
126
+ const body = (await res.json()) as {
127
+ presences?: Array<{ sessionId?: string; phase?: string; detail?: string | null }>;
128
+ };
129
+ const own = body.presences?.find((p) => p.sessionId === consumer);
130
+ const ownToolingMarker = own?.phase === 'tooling' && (own.detail ?? '').startsWith('steer: ');
131
+ if (own && !ownToolingMarker && own.phase !== 'idle') return; // richer phase belongs to the host
132
+ }
133
+ } catch {
134
+ // presence read failed — fall through and settle; better idle than stuck tooling
135
+ }
136
+ await setPresence(consumer, label, parent, { phase: 'idle', detail: null });
137
+ }
138
+
139
+ cmd(
140
+ 'pump',
141
+ 'Reactive steering loop for CLI agents (Amp, Codex CLI): long-poll claims for a consumer and run a command per steer',
142
+ [
143
+ `pmx-canvas pump --consumer codex-cli --exec 'codex exec --full-auto {message}'`,
144
+ `pmx-canvas pump --consumer amp --exec 'amp -x {message}' --parent claude-code`,
145
+ `pmx-canvas pump --consumer testbot --exec 'cat' --once`,
146
+ ],
147
+ async (args) => {
148
+ const { flags } = parseFlags(args, { boolFlags: ['once', 'help', 'h'] });
149
+ // Help before required-flag validation — `pump --help` died on
150
+ // "Missing required flag: --consumer" (0.5.0 Codex finding W).
151
+ if (flags.help || flags.h) return showCommandHelp('pump');
152
+ const consumer = requireFlag(
153
+ flags,
154
+ 'consumer',
155
+ `pmx-canvas pump --consumer <key> --exec '<command with {message}>'`,
156
+ );
157
+ const execTemplate = requireFlag(
158
+ flags,
159
+ 'exec',
160
+ `pmx-canvas pump --consumer <key> --exec '<command with {message}>'`,
161
+ );
162
+ const once = flags.once === true;
163
+ const waitMsRaw = Number(getStringFlag(flags, 'wait-ms') ?? 120_000);
164
+ const waitMs = Number.isFinite(waitMsRaw) && waitMsRaw >= 0 ? Math.min(waitMsRaw, 120_000) : 120_000;
165
+ const label = getStringFlag(flags, 'label') ?? consumer;
166
+ const parent = getStringFlag(flags, 'parent') ?? null;
167
+ const deliverBacklog = getStringFlag(flags, 'backlog') === 'deliver';
168
+ const retryDelayRaw = Number(getStringFlag(flags, 'retry-delay-ms') ?? 5_000);
169
+ const retryDelayMs = Number.isFinite(retryDelayRaw) && retryDelayRaw >= 0 ? retryDelayRaw : 5_000;
170
+
171
+ // Startup watermark: steers queued BEFORE the pump existed are history,
172
+ // not instructions — mark them silently unless --backlog=deliver.
173
+ if (!deliverBacklog) {
174
+ const { pending } = await claimPending(consumer, 0);
175
+ for (const steer of pending) await markDelivered(steer.id, consumer);
176
+ let swept = pending.length;
177
+ // pending is capped at limit=1 — drain the rest.
178
+ while (swept > 0) {
179
+ const next = await claimPending(consumer, 0);
180
+ if (next.pending.length === 0) break;
181
+ for (const steer of next.pending) await markDelivered(steer.id, consumer);
182
+ swept = next.pending.length;
183
+ }
184
+ }
185
+
186
+ // Park via the guard: a pump RESTART while its host is mid-work must not
187
+ // knock an explicit phase back to idle either.
188
+ await settlePresence(consumer, label, parent);
189
+ console.log(`[pump] ${consumer} parked on ${getBaseUrl()} (waitMs=${waitMs}${once ? ', once' : ''})`);
190
+
191
+ const awaitingMark = new Set<string>();
192
+ const failures = new Map<string, number>();
193
+ let stopped = false;
194
+ let fastEmpties = 0;
195
+ process.on('SIGINT', () => {
196
+ stopped = true;
197
+ console.log('[pump] stopping');
198
+ process.exit(0);
199
+ });
200
+
201
+ while (!stopped) {
202
+ let pending: PendingSteer[];
203
+ let tookMs: number;
204
+ try {
205
+ ({ pending, tookMs } = await claimPending(consumer, waitMs));
206
+ } catch (error) {
207
+ // Daemon roll or network blip: back off and re-park, never exit.
208
+ console.error(`[pump] claim retrying: ${error instanceof Error ? error.message : String(error)}`);
209
+ await new Promise((resolve) => setTimeout(resolve, 10_000));
210
+ continue;
211
+ }
212
+ if (pending.length === 0) {
213
+ // Old-server guard: a 0.4.8 daemon ignores waitMs, returning instantly —
214
+ // without this the loop would spin hot.
215
+ fastEmpties = tookMs < 1_000 ? fastEmpties + 1 : 0;
216
+ if (fastEmpties >= 2) await new Promise((resolve) => setTimeout(resolve, 10_000));
217
+ continue;
218
+ }
219
+ fastEmpties = 0;
220
+ const steer = pending[0]!;
221
+
222
+ // Awaiting-mark memory: the exec already ran for this id — a failed mark
223
+ // must not re-run the command (the duplicate-send race).
224
+ if (awaitingMark.has(steer.id)) {
225
+ if (await markDelivered(steer.id, consumer)) awaitingMark.delete(steer.id);
226
+ continue;
227
+ }
228
+
229
+ await setPresence(consumer, label, parent, { phase: 'tooling', detail: `steer: ${steer.message.slice(0, 60)}` });
230
+ const age = steer.createdAt
231
+ ? `, age ${Math.max(0, Math.round((Date.now() - Date.parse(steer.createdAt)) / 1000))}s`
232
+ : '';
233
+ console.log(
234
+ `[pump] steer ${steer.id} from ${steer.source ?? '?'} → ${steer.target ?? 'ALL'}${age}: ${steer.message.slice(0, 120)}`,
235
+ );
236
+ const code = await runExec(execTemplate, steer, consumer);
237
+ if (code === 0) {
238
+ awaitingMark.add(steer.id);
239
+ if (await markDelivered(steer.id, consumer)) awaitingMark.delete(steer.id);
240
+ failures.delete(steer.id);
241
+ await settlePresence(consumer, label, parent);
242
+ if (once) return;
243
+ } else {
244
+ const count = (failures.get(steer.id) ?? 0) + 1;
245
+ failures.set(steer.id, count);
246
+ console.error(`[pump] exec exited ${code} for ${steer.id} (attempt ${count}/3)`);
247
+ if (count >= 3) {
248
+ // A failed host injection is still pending work; only successful execs may mark delivery.
249
+ failures.delete(steer.id);
250
+ console.error(`[pump] stopped after three failures for ${steer.id} — steer remains pending`);
251
+ await setPresence(consumer, label, parent, {
252
+ phase: 'idle',
253
+ detail: `delivery failed: ${steer.id} remains pending`,
254
+ });
255
+ throw new Error(`Pump delivery failed three times for ${steer.id}; steer remains pending.`);
256
+ } else {
257
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
258
+ }
259
+ await settlePresence(consumer, label, parent);
260
+ }
261
+ }
262
+ },
263
+ );
package/src/cli/index.ts CHANGED
@@ -8,7 +8,6 @@ import { runAgentCli } from './agent.js';
8
8
  import { isCanvasTheme } from '../shared/themes.js';
9
9
  import { createCanvas } from '../server/index.js';
10
10
  import { seedDemoCanvas } from '../server/demo.js';
11
- import { ensureDefaultDockedNodes } from '../server/server.js';
12
11
 
13
12
  const args = process.argv.slice(2);
14
13
 
@@ -45,6 +44,7 @@ const AGENT_COMMANDS = new Set([
45
44
  'screenshot',
46
45
  'pin',
47
46
  'ax',
47
+ 'pump',
48
48
  'undo',
49
49
  'redo',
50
50
  'history',
@@ -314,6 +314,7 @@ Agent CLI (works against running server):
314
314
  code-graph File dependencies
315
315
  spatial Spatial analysis
316
316
  watch Semantic watch stream
317
+ pump Reactive steering loop for CLI agents (Amp, Codex CLI)
317
318
  smoke Check that a running canvas works here
318
319
  skills sync Sync installed copies of the bundled skills
319
320
  copilot install-extension Install the GitHub Copilot app extension
@@ -404,10 +405,6 @@ Examples:
404
405
 
405
406
  if (demo && canvas.getLayout().nodes.length === 0) {
406
407
  seedDemoCanvas();
407
- } else if (!demo) {
408
- // First-run only: dock a status (left) + context (right) widget by default so
409
- // a fresh canvas isn't empty. No-op once the workspace has saved state.
410
- ensureDefaultDockedNodes();
411
408
  }
412
409
 
413
410
  console.log(`\n PMX Canvas running at http://localhost:${canvas.port}`);
package/src/cli/shared.ts CHANGED
@@ -118,7 +118,7 @@ function output(data: unknown): void {
118
118
  async function invokeOperation(name: string, input: Record<string, unknown>): Promise<unknown> {
119
119
  const base = getBaseUrl();
120
120
  try {
121
- return await new HttpOperationInvoker(base).invoke(name, input);
121
+ return await new HttpOperationInvoker(base, 'cli').invoke(name, input);
122
122
  } catch (error) {
123
123
  if (error instanceof OperationError) {
124
124
  die(error.message);