agent-tempo 1.2.0 → 1.4.0

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 (281) hide show
  1. package/CLAUDE.md +253 -219
  2. package/LICENSE +21 -21
  3. package/README.md +293 -289
  4. package/assets/icon-dark.svg +9 -9
  5. package/assets/icon.svg +9 -9
  6. package/assets/logo-dark.svg +11 -11
  7. package/assets/logo-light.svg +11 -11
  8. package/dashboard/README.md +91 -91
  9. package/dashboard/dist/assets/{index-D6Xyje_n.js → index-jmYe6rmS.js} +2 -2
  10. package/dashboard/dist/assets/index-jmYe6rmS.js.map +1 -0
  11. package/dashboard/dist/index.html +20 -20
  12. package/dashboard/package.json +47 -47
  13. package/dist/activities/outbox.d.ts +30 -1
  14. package/dist/activities/outbox.js +96 -3
  15. package/dist/adapters/base.js +5 -0
  16. package/dist/adapters/copilot/adapter.js +12 -1
  17. package/dist/adapters/index.d.ts +1 -1
  18. package/dist/adapters/index.js +7 -0
  19. package/dist/adapters/pi/adapter.d.ts +2 -0
  20. package/dist/adapters/pi/adapter.js +43 -0
  21. package/dist/adapters/pi/index.d.ts +16 -0
  22. package/dist/adapters/pi/index.js +10 -0
  23. package/dist/cli/global-wrapper.d.ts +19 -0
  24. package/dist/cli/global-wrapper.js +169 -0
  25. package/dist/cli/help-text.js +97 -97
  26. package/dist/cli/startup.js +11 -0
  27. package/dist/cli/upgrade-command.js +81 -81
  28. package/dist/cli.js +12 -0
  29. package/dist/client/core.js +9 -2
  30. package/dist/client/interface.d.ts +6 -0
  31. package/dist/config.d.ts +79 -0
  32. package/dist/config.js +74 -0
  33. package/dist/daemon.js +37 -1
  34. package/dist/http/aggregate.d.ts +22 -1
  35. package/dist/http/aggregate.js +41 -0
  36. package/dist/http/auth.d.ts +94 -8
  37. package/dist/http/auth.js +93 -9
  38. package/dist/http/body.d.ts +4 -1
  39. package/dist/http/body.js +6 -3
  40. package/dist/http/event-bus.js +1 -0
  41. package/dist/http/event-types.d.ts +34 -2
  42. package/dist/http/event-types.js +1 -0
  43. package/dist/http/gate-audit.d.ts +12 -0
  44. package/dist/http/gate-audit.js +95 -0
  45. package/dist/http/gate-registry.d.ts +167 -0
  46. package/dist/http/gate-registry.js +163 -0
  47. package/dist/http/gate-routes.d.ts +48 -0
  48. package/dist/http/gate-routes.js +102 -0
  49. package/dist/http/ingest-registry.d.ts +30 -0
  50. package/dist/http/ingest-registry.js +108 -0
  51. package/dist/http/inner-loop-routes.d.ts +66 -0
  52. package/dist/http/inner-loop-routes.js +182 -0
  53. package/dist/http/inner-loop.d.ts +92 -0
  54. package/dist/http/inner-loop.js +155 -0
  55. package/dist/http/server.d.ts +38 -3
  56. package/dist/http/server.js +211 -6
  57. package/dist/http/snapshot.d.ts +6 -0
  58. package/dist/http/snapshot.js +6 -0
  59. package/dist/pi/cue-pump.d.ts +61 -0
  60. package/dist/pi/cue-pump.js +95 -0
  61. package/dist/pi/extension.d.ts +45 -0
  62. package/dist/pi/extension.js +407 -0
  63. package/dist/pi/gate-client.d.ts +54 -0
  64. package/dist/pi/gate-client.js +136 -0
  65. package/dist/pi/headless.d.ts +85 -0
  66. package/dist/pi/headless.js +224 -0
  67. package/dist/pi/index.d.ts +28 -0
  68. package/dist/pi/index.js +43 -0
  69. package/dist/pi/inner-loop-client.d.ts +67 -0
  70. package/dist/pi/inner-loop-client.js +164 -0
  71. package/dist/pi/inner-loop-publisher.d.ts +187 -0
  72. package/dist/pi/inner-loop-publisher.js +236 -0
  73. package/dist/pi/lazy-proxy.d.ts +37 -0
  74. package/dist/pi/lazy-proxy.js +55 -0
  75. package/dist/pi/mission-control/actions.d.ts +48 -0
  76. package/dist/pi/mission-control/actions.js +98 -0
  77. package/dist/pi/mission-control/board.d.ts +53 -0
  78. package/dist/pi/mission-control/board.js +104 -0
  79. package/dist/pi/mission-control/extension.d.ts +44 -0
  80. package/dist/pi/mission-control/extension.js +251 -0
  81. package/dist/pi/mission-control/index.d.ts +15 -0
  82. package/dist/pi/mission-control/index.js +32 -0
  83. package/dist/pi/mission-control/inner-tail.d.ts +48 -0
  84. package/dist/pi/mission-control/inner-tail.js +76 -0
  85. package/dist/pi/mission-control/pi-ui.d.ts +43 -0
  86. package/dist/pi/mission-control/pi-ui.js +10 -0
  87. package/dist/pi/mission-control/render.d.ts +6 -0
  88. package/dist/pi/mission-control/render.js +95 -0
  89. package/dist/pi/phase-driver.d.ts +74 -0
  90. package/dist/pi/phase-driver.js +122 -0
  91. package/dist/pi/pi-types.d.ts +208 -0
  92. package/dist/pi/pi-types.js +21 -0
  93. package/dist/pi/probe.d.ts +80 -0
  94. package/dist/pi/probe.js +154 -0
  95. package/dist/pi/render-tools.d.ts +17 -0
  96. package/dist/pi/render-tools.js +51 -0
  97. package/dist/pi/reset-pump.d.ts +47 -0
  98. package/dist/pi/reset-pump.js +85 -0
  99. package/dist/pi/tool-capability.d.ts +60 -0
  100. package/dist/pi/tool-capability.js +156 -0
  101. package/dist/pi/workflow-client.d.ts +158 -0
  102. package/dist/pi/workflow-client.js +289 -0
  103. package/dist/pi/zod-to-typebox.d.ts +74 -0
  104. package/dist/pi/zod-to-typebox.js +191 -0
  105. package/dist/scripts/verify-daemon-isolation-guard.js +24 -24
  106. package/dist/server-tools.d.ts +2 -0
  107. package/dist/server-tools.js +50 -46
  108. package/dist/server.js +4 -0
  109. package/dist/spawn.d.ts +55 -0
  110. package/dist/spawn.js +84 -12
  111. package/dist/tools/agent-types.d.ts +2 -2
  112. package/dist/tools/agent-types.js +22 -17
  113. package/dist/tools/attachment-info.d.ts +2 -2
  114. package/dist/tools/attachment-info.js +38 -33
  115. package/dist/tools/broadcast.d.ts +2 -2
  116. package/dist/tools/broadcast.js +69 -64
  117. package/dist/tools/cancel-stage.d.ts +2 -2
  118. package/dist/tools/cancel-stage.js +20 -15
  119. package/dist/tools/clear-state.d.ts +2 -2
  120. package/dist/tools/clear-state.js +25 -20
  121. package/dist/tools/coat-check-evict.d.ts +2 -2
  122. package/dist/tools/coat-check-evict.js +30 -25
  123. package/dist/tools/coat-check-get.d.ts +2 -2
  124. package/dist/tools/coat-check-get.js +39 -34
  125. package/dist/tools/coat-check-list.d.ts +2 -2
  126. package/dist/tools/coat-check-list.js +48 -43
  127. package/dist/tools/coat-check-put.d.ts +2 -2
  128. package/dist/tools/coat-check-put.js +41 -36
  129. package/dist/tools/cue.d.ts +2 -2
  130. package/dist/tools/cue.js +57 -52
  131. package/dist/tools/descriptor.d.ts +72 -0
  132. package/dist/tools/descriptor.js +39 -0
  133. package/dist/tools/destroy.d.ts +2 -2
  134. package/dist/tools/destroy.js +153 -148
  135. package/dist/tools/ensemble.d.ts +2 -2
  136. package/dist/tools/ensemble.js +71 -66
  137. package/dist/tools/evaluate-gate.d.ts +2 -2
  138. package/dist/tools/evaluate-gate.js +33 -27
  139. package/dist/tools/fetch-state.d.ts +2 -2
  140. package/dist/tools/fetch-state.js +43 -38
  141. package/dist/tools/gates.d.ts +2 -2
  142. package/dist/tools/gates.js +39 -34
  143. package/dist/tools/hosts.d.ts +2 -2
  144. package/dist/tools/hosts.js +25 -20
  145. package/dist/tools/listen.d.ts +2 -2
  146. package/dist/tools/listen.js +23 -18
  147. package/dist/tools/load-lineup.d.ts +2 -2
  148. package/dist/tools/load-lineup.js +324 -319
  149. package/dist/tools/migrate.d.ts +2 -2
  150. package/dist/tools/migrate.js +45 -40
  151. package/dist/tools/pause.d.ts +2 -2
  152. package/dist/tools/pause.js +34 -29
  153. package/dist/tools/play.d.ts +2 -2
  154. package/dist/tools/play.js +53 -48
  155. package/dist/tools/quality-gate.d.ts +2 -2
  156. package/dist/tools/quality-gate.js +26 -21
  157. package/dist/tools/recall.d.ts +2 -2
  158. package/dist/tools/recall.js +32 -27
  159. package/dist/tools/recruit.d.ts +2 -2
  160. package/dist/tools/recruit.js +325 -256
  161. package/dist/tools/release.d.ts +2 -2
  162. package/dist/tools/release.js +85 -80
  163. package/dist/tools/report.d.ts +2 -2
  164. package/dist/tools/report.js +28 -23
  165. package/dist/tools/reset.d.ts +3 -0
  166. package/dist/tools/reset.js +51 -0
  167. package/dist/tools/restart.d.ts +2 -2
  168. package/dist/tools/restart.js +51 -46
  169. package/dist/tools/restore.d.ts +2 -2
  170. package/dist/tools/restore.js +76 -71
  171. package/dist/tools/save-lineup.d.ts +2 -2
  172. package/dist/tools/save-lineup.js +32 -27
  173. package/dist/tools/save-state.d.ts +2 -2
  174. package/dist/tools/save-state.js +43 -38
  175. package/dist/tools/schedule.d.ts +2 -2
  176. package/dist/tools/schedule.js +133 -128
  177. package/dist/tools/schedules.d.ts +2 -2
  178. package/dist/tools/schedules.js +41 -36
  179. package/dist/tools/set-ensemble-description.d.ts +2 -2
  180. package/dist/tools/set-ensemble-description.js +26 -21
  181. package/dist/tools/set-name.d.ts +2 -2
  182. package/dist/tools/set-name.js +38 -33
  183. package/dist/tools/set-part.d.ts +2 -2
  184. package/dist/tools/set-part.js +20 -15
  185. package/dist/tools/shutdown.d.ts +2 -2
  186. package/dist/tools/shutdown.js +39 -34
  187. package/dist/tools/stage.d.ts +2 -2
  188. package/dist/tools/stage.js +28 -23
  189. package/dist/tools/stages.d.ts +2 -2
  190. package/dist/tools/stages.js +36 -31
  191. package/dist/tools/unschedule.d.ts +2 -2
  192. package/dist/tools/unschedule.js +30 -25
  193. package/dist/tools/who-am-i.d.ts +2 -2
  194. package/dist/tools/who-am-i.js +36 -31
  195. package/dist/tools/worktree.d.ts +2 -2
  196. package/dist/tools/worktree.js +134 -129
  197. package/dist/tui/index.js +6 -6
  198. package/dist/types.d.ts +47 -2
  199. package/dist/types.js +1 -1
  200. package/dist/utils/default-part.js +1 -0
  201. package/dist/utils/grpc-shutdown-guard.d.ts +52 -0
  202. package/dist/utils/grpc-shutdown-guard.js +88 -0
  203. package/dist/utils/sdk-probe.d.ts +23 -0
  204. package/dist/utils/sdk-probe.js +46 -7
  205. package/dist/worker.d.ts +3 -1
  206. package/dist/worker.js +6 -2
  207. package/dist/workflows/session.js +70 -2
  208. package/dist/workflows/signals.d.ts +32 -2
  209. package/dist/workflows/signals.js +25 -2
  210. package/examples/agents/tempo-composer.md +56 -56
  211. package/examples/agents/tempo-conductor.md +117 -117
  212. package/examples/agents/tempo-critic.md +73 -73
  213. package/examples/agents/tempo-improv.md +74 -74
  214. package/examples/agents/tempo-liner.md +75 -75
  215. package/examples/agents/tempo-roadie.md +61 -61
  216. package/examples/agents/tempo-soloist.md +71 -71
  217. package/examples/agents/tempo-tuner.md +94 -94
  218. package/examples/ensembles/tempo-big-band.yaml +146 -146
  219. package/examples/ensembles/tempo-dev-team.yaml +58 -58
  220. package/examples/ensembles/tempo-headless-jam.yaml +77 -77
  221. package/examples/ensembles/tempo-jam-session.yaml +41 -41
  222. package/examples/ensembles/tempo-mock-jam.yaml +79 -79
  223. package/examples/ensembles/tempo-review-squad.yaml +32 -32
  224. package/package.json +176 -173
  225. package/packaging/launchd/com.agent.tempo.plist +46 -46
  226. package/packaging/systemd/agent-tempo.service +32 -32
  227. package/packaging/windows/install-task.ps1 +71 -71
  228. package/scenarios/conductor-recruit-mock.yaml +33 -33
  229. package/scenarios/echo-roundtrip.yaml +15 -15
  230. package/scenarios/multi-player-handoff.yaml +38 -38
  231. package/scenarios/recruit-cascade.yaml +38 -38
  232. package/scenarios/two-player-conversation.yaml +33 -33
  233. package/workflow-bundle.js +97 -6
  234. package/dashboard/dist/assets/index-D6Xyje_n.js.map +0 -1
  235. package/dist/activities/claude-stop.d.ts +0 -21
  236. package/dist/activities/claude-stop.js +0 -94
  237. package/dist/channel.d.ts +0 -3
  238. package/dist/channel.js +0 -48
  239. package/dist/copilot-bridge.d.ts +0 -22
  240. package/dist/copilot-bridge.js +0 -565
  241. package/dist/scripts/258-spotcheck.js +0 -303
  242. package/dist/tools/detach.d.ts +0 -4
  243. package/dist/tools/detach.js +0 -45
  244. package/dist/tools/encore.d.ts +0 -4
  245. package/dist/tools/encore.js +0 -31
  246. package/dist/tools/helpers.d.ts +0 -21
  247. package/dist/tools/helpers.js +0 -25
  248. package/dist/tools/pause-ensemble.d.ts +0 -4
  249. package/dist/tools/pause-ensemble.js +0 -58
  250. package/dist/tools/resume-ensemble.d.ts +0 -4
  251. package/dist/tools/resume-ensemble.js +0 -79
  252. package/dist/tools/stop.d.ts +0 -4
  253. package/dist/tools/stop.js +0 -29
  254. package/dist/tui/client.d.ts +0 -6
  255. package/dist/tui/client.js +0 -9
  256. package/dist/tui/components/ActivityLog.d.ts +0 -16
  257. package/dist/tui/components/ActivityLog.js +0 -36
  258. package/dist/tui/components/CommandOverlay.d.ts +0 -15
  259. package/dist/tui/components/CommandOverlay.js +0 -34
  260. package/dist/tui/components/ConductorChat.d.ts +0 -16
  261. package/dist/tui/components/ConductorChat.js +0 -32
  262. package/dist/tui/components/EnsembleListView.d.ts +0 -14
  263. package/dist/tui/components/EnsembleListView.js +0 -32
  264. package/dist/tui/components/EnsemblePanel.d.ts +0 -12
  265. package/dist/tui/components/EnsemblePanel.js +0 -40
  266. package/dist/tui/components/InputBar.d.ts +0 -13
  267. package/dist/tui/components/InputBar.js +0 -58
  268. package/dist/tui/components/ScheduleOverlay.d.ts +0 -13
  269. package/dist/tui/components/ScheduleOverlay.js +0 -113
  270. package/dist/tui/components/TopBar.d.ts +0 -12
  271. package/dist/tui/components/TopBar.js +0 -15
  272. package/dist/tui/core-api.d.ts +0 -26
  273. package/dist/tui/core-api.js +0 -67
  274. package/dist/tui/hooks/useEnsembleDiscovery.d.ts +0 -3
  275. package/dist/tui/hooks/useEnsembleDiscovery.js +0 -30
  276. package/dist/tui/hooks/useMaestroPoller.d.ts +0 -3
  277. package/dist/tui/hooks/useMaestroPoller.js +0 -36
  278. package/dist/tui/hooks/useSendCommand.d.ts +0 -7
  279. package/dist/tui/hooks/useSendCommand.js +0 -29
  280. package/dist/utils/bg-preflight.d.ts +0 -25
  281. package/dist/utils/bg-preflight.js +0 -154
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CuePump = void 0;
4
+ const DEFAULT_POLL_MS = 1_000;
5
+ const log = (...args) => {
6
+ // eslint-disable-next-line no-console
7
+ console.error('[agent-tempo:pi]', ...args);
8
+ };
9
+ class CuePump {
10
+ source;
11
+ resolveSession;
12
+ intervalMs;
13
+ timer = null;
14
+ draining = false;
15
+ constructor(opts) {
16
+ this.source = opts.source;
17
+ this.resolveSession = opts.resolveSession;
18
+ this.intervalMs = opts.intervalMs ?? DEFAULT_POLL_MS;
19
+ }
20
+ start() {
21
+ if (this.timer)
22
+ return;
23
+ this.timer = setInterval(() => {
24
+ this.tick().catch((err) => log('cue-pump tick failed:', err));
25
+ }, this.intervalMs);
26
+ if (typeof this.timer.unref === 'function')
27
+ this.timer.unref();
28
+ }
29
+ stop() {
30
+ if (this.timer) {
31
+ clearInterval(this.timer);
32
+ this.timer = null;
33
+ }
34
+ }
35
+ /**
36
+ * One poll cycle: fetch pending cues, inject each into the live session, ack
37
+ * the ones successfully injected. Re-entrancy guarded so a slow tick never
38
+ * overlaps the next interval.
39
+ */
40
+ async tick() {
41
+ if (this.draining)
42
+ return;
43
+ this.draining = true;
44
+ try {
45
+ const pending = await this.source.fetchPending();
46
+ if (pending.length === 0)
47
+ return;
48
+ const session = this.resolveSession();
49
+ if (!session) {
50
+ // No live session yet — leave cues queued; next tick retries.
51
+ return;
52
+ }
53
+ const delivered = [];
54
+ for (const msg of pending) {
55
+ try {
56
+ await this.injectCue(session, msg);
57
+ delivered.push(msg.id);
58
+ }
59
+ catch (err) {
60
+ log(`failed to inject cue ${msg.id}:`, err);
61
+ // Stop on first failure — preserve ordering; retry next tick.
62
+ break;
63
+ }
64
+ }
65
+ await this.source.ackDelivered(delivered);
66
+ }
67
+ finally {
68
+ this.draining = false;
69
+ }
70
+ }
71
+ /**
72
+ * Inject one cue into the live session (D10 — see file header). Operator cues
73
+ * `steer` (same-turn priority); peer cues `followUp` (queue). `triggerTurn` is
74
+ * always set: a no-op mid-turn, the required cold-idle wake otherwise.
75
+ */
76
+ async injectCue(session, msg) {
77
+ const content = msg.from ? `[cue from ${msg.from}] ${msg.text}` : msg.text;
78
+ // LOAD-BEARING Pi-runtime invariant (D10) — confirmed sound through Pi 0.78.x
79
+ // (researcher-cited; a D6 "behaviors-to-revalidate-on-bump" item):
80
+ // peer cue = { deliverAs: 'followUp', triggerTurn: true } → QUEUES; drains
81
+ // when the agent goes idle, NEVER preempts a running turn. triggerTurn only
82
+ // wakes a cold-idle session (followUp alone won't start one); it is a no-op
83
+ // while a turn is in flight.
84
+ // operator cue = { deliverAs: 'steer', triggerTurn: true } → same-turn PRIORITY:
85
+ // injected after the current tool batch, before the next LLM call. NOT a hard
86
+ // mid-tool abort (only RPC abort / AbortSignal hard-interrupts a running tool).
87
+ // The guarantee this comment protects: a future Pi version MUST keep followUp
88
+ // non-interrupting AND triggerTurn a no-op-while-busy. If that regresses, peer
89
+ // cues silently become preemptions, defeating operator-vs-peer. Not unit-testable
90
+ // here (the session is mocked) — locked by researcher confirmation + the D6 Pi
91
+ // version floor (≥ #2860 + #5115) + a real-Pi mid-turn integration smoke.
92
+ await session.sendCustomMessage({ customType: 'cue', content, display: true }, { deliverAs: msg.isMaestro ? 'steer' : 'followUp', triggerTurn: true });
93
+ }
94
+ }
95
+ exports.CuePump = CuePump;
@@ -0,0 +1,45 @@
1
+ import type { Client } from '@temporalio/client';
2
+ import { type Config } from '../config';
3
+ import type { ExtensionAPI, PiAgentSession } from './pi-types';
4
+ /** Runtime mode. Headless = recruited unsupervised player (MD-C gate active). */
5
+ export type PiExtensionMode = 'interactive' | 'headless';
6
+ export type PiToolAccess = 'restricted' | 'standard' | 'full';
7
+ export interface PiExtensionOptions {
8
+ /** Default `'interactive'`. Headless installs the MD-C tool_call gate. */
9
+ mode?: PiExtensionMode;
10
+ /** MD-C tool-class policy (headless only). Default `'restricted'`. */
11
+ toolAccess?: PiToolAccess;
12
+ }
13
+ /**
14
+ * Build the Pi extension factory. `mode='headless'` installs the MD-C tool_call
15
+ * gate; `mode='interactive'` (default) does not (the human owns their machine).
16
+ */
17
+ export declare function createPiExtension(options?: PiExtensionOptions): (pi: ExtensionAPI) => void;
18
+ /**
19
+ * RELIABLE detach for the headless exit sequence (Phase 3a). Headless owns its
20
+ * exit loop, so — unlike interactive's best-effort `quit` path — it can AWAIT a
21
+ * clean detach before disposing the SDK session. Ordering (architect ruling):
22
+ * stopHeartbeat → requestDetach → adapterExited (all inside `wf.detach`) → unmap.
23
+ * The caller then calls `session.dispose()`; the dispose-fired `session_shutdown`
24
+ * finds no mapped runtime → no-op (avoids double-detach). Detaches every runtime
25
+ * in the process (headless = one player per process).
26
+ */
27
+ export declare function detachAllPiRuntimesForExit(): Promise<void>;
28
+ /**
29
+ * Headless-only: wire the live Pi SDK session onto a runtime so the cue pump can
30
+ * inject into it. The interactive CLI's `session_start` payload carries
31
+ * `session`, but the headless SDK's DEFAULT session_start payload does NOT (it's
32
+ * `{ type, reason }`) — so `attachOrRebind` sets `rt.session = null` and the cue
33
+ * pump's `resolveSession` returns null (every cue is dropped). The headless entry
34
+ * HOLDS the session from `createAgentSession`, so it calls this after
35
+ * `bindExtensions` (by which point the runtime exists + has claimed) to set it.
36
+ * (3a live smoke — devops.)
37
+ */
38
+ export declare function setRuntimeSession(workflowId: string, session: PiAgentSession): void;
39
+ /** Override the Temporal connection factory (inject a fake Client). */
40
+ export declare function __setPiClientFactoryForTests(factory: (config: Config) => Promise<Client>): void;
41
+ /** Stop timers, clear the per-player runtime map + shared-client singletons + factory. */
42
+ export declare function __resetPiRuntimesForTests(): void;
43
+ /** Default export — interactive-mode extension (the human `pi` CLI entry). */
44
+ declare const piExtension: (pi: ExtensionAPI) => void;
45
+ export default piExtension;
@@ -0,0 +1,407 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createPiExtension = createPiExtension;
37
+ exports.detachAllPiRuntimesForExit = detachAllPiRuntimesForExit;
38
+ exports.setRuntimeSession = setRuntimeSession;
39
+ exports.__setPiClientFactoryForTests = __setPiClientFactoryForTests;
40
+ exports.__resetPiRuntimesForTests = __resetPiRuntimesForTests;
41
+ /**
42
+ * agent-tempo Pi extension — interactive (Phase 2) + headless (Phase 3a) runtime.
43
+ *
44
+ * createPiExtension({ mode, toolAccess }) → (pi: ExtensionAPI) => void
45
+ * export default = createPiExtension() (interactive)
46
+ *
47
+ * Registers the FULL agent-tempo tool surface natively on Pi (shared
48
+ * transport-neutral descriptors + `renderToPi`), drives the attachment phase
49
+ * from Pi lifecycle events, holds an attachment lease + heartbeat, and pumps
50
+ * cues into the live session. The SAME extension runs interactive (behind a
51
+ * human `pi` CLI) and headless (injected into `createAgentSession` by the daemon
52
+ * — Phase 3a); `mode` is the only behavioural discriminator (it gates the MD-C
53
+ * tool_call enforcement, which applies to unsupervised headless players only).
54
+ *
55
+ * ── Module-scope singleton (CRITICAL — researcher finding) ──
56
+ * Pi REBUILDS the extension instance on every SessionManager switch, so
57
+ * per-INSTANCE state does NOT survive. Everything that must survive — the
58
+ * Temporal `Client`, the fixed `workflowId`, the pinned handle, the heartbeat
59
+ * timer, the cue pump, the current-session pointer — lives in a MODULE-SCOPE
60
+ * singleton (`runtimes`, keyed by workflowId; one entry interactive, N for the
61
+ * headless daemon — D12a). The rebuilt instance RE-BINDS; it never recreates.
62
+ *
63
+ * ── Teardown (Option C — reason-discriminated) ──
64
+ * `session_shutdown` carries `reason` {quit|reload|new|resume|fork}. We detach
65
+ * ONLY on a clean `quit`; switch/unknown reasons → rebind (no detach). The
66
+ * `quit` detach is best-effort; the MD-A lease reaper is the permanent floor.
67
+ * Headless owns its exit sequence, so it uses {@link detachAllPiRuntimesForExit}
68
+ * for RELIABLE detach (await adapterExited) before disposing the SDK session.
69
+ *
70
+ * Determinism boundary: this module (and all of src/pi/) is CLIENT-SIDE only.
71
+ */
72
+ const os = __importStar(require("os"));
73
+ const crypto = __importStar(require("crypto"));
74
+ const config_1 = require("../config");
75
+ const server_tools_1 = require("../server-tools");
76
+ const phase_driver_1 = require("./phase-driver");
77
+ const workflow_client_1 = require("./workflow-client");
78
+ const cue_pump_1 = require("./cue-pump");
79
+ const reset_pump_1 = require("./reset-pump");
80
+ const render_tools_1 = require("./render-tools");
81
+ const lazy_proxy_1 = require("./lazy-proxy");
82
+ const probe_1 = require("./probe");
83
+ const inner_loop_publisher_1 = require("./inner-loop-publisher");
84
+ const inner_loop_client_1 = require("./inner-loop-client");
85
+ const gate_client_1 = require("./gate-client");
86
+ const tool_capability_1 = require("./tool-capability");
87
+ const gate_registry_1 = require("../http/gate-registry");
88
+ const log = (...args) => {
89
+ // eslint-disable-next-line no-console
90
+ console.error('[agent-tempo:pi]', ...args);
91
+ };
92
+ const nowIso = () => new Date().toISOString();
93
+ const PI_AGENT_TYPE = 'claude'; // Pi is not yet a first-class AgentType.
94
+ // MD-C shell/exec tool-class membership is owned by `tool-capability.ts`
95
+ // (`classify(name) === 'exec'`, content signed off by tempo-security). F1
96
+ // import-refactor (3d): this REPLACES the former local `SHELL_TOOL_NAMES` set —
97
+ // the canonical EXEC_TOOLS set is a SUPERSET that also blocks
98
+ // powershell/pwsh/cmd/run, closing the gap the local list left open. Single
99
+ // source of truth: never re-declare a shell denylist here.
100
+ // ── Module-scope Temporal Client singleton (D12a: one Client per OS process) ──
101
+ let sharedClientPromise = null;
102
+ let connectedClient = null;
103
+ function getSharedClient(config) {
104
+ if (!sharedClientPromise) {
105
+ sharedClientPromise = workflow_client_1.PiWorkflowClient.connect(config)
106
+ .then((c) => { connectedClient = c; return c; })
107
+ .catch((err) => { sharedClientPromise = null; log('Temporal connect failed:', err); throw err; });
108
+ }
109
+ return sharedClientPromise;
110
+ }
111
+ /** Connection factory; overridable via `__setPiClientFactoryForTests`. */
112
+ let clientFactory = getSharedClient;
113
+ /** One runtime per player, keyed by fixed workflowId. Survives instance rebuilds. */
114
+ const runtimes = new Map();
115
+ /**
116
+ * Build the Pi extension factory. `mode='headless'` installs the MD-C tool_call
117
+ * gate; `mode='interactive'` (default) does not (the human owns their machine).
118
+ */
119
+ function createPiExtension(options = {}) {
120
+ const mode = options.mode ?? 'interactive';
121
+ const toolAccess = options.toolAccess ?? 'restricted';
122
+ return function piExtension(pi) {
123
+ const probe = (0, probe_1.probePi)();
124
+ if (!probe.available)
125
+ log('WARNING:', probe.reason);
126
+ const config = (0, config_1.getConfig)();
127
+ const isConductor = process.env[config_1.ENV.CONDUCTOR] === '1' || process.env[config_1.ENV.CONDUCTOR] === 'true';
128
+ // Identity — FIXED workflowId for the process lifetime. `currentPlayerId` is
129
+ // the mutable DISPLAY id (set_name updates it); the workflowId never repoints.
130
+ let currentPlayerId = process.env[config_1.ENV.PLAYER_NAME] || `pi-${process.pid}`;
131
+ const workflowId = (0, config_1.sessionWorkflowId)(config.ensemble, currentPlayerId);
132
+ // 3c — the inner-loop URL + ingest token are keyed to the player's FIXED
133
+ // identity (the daemon minted the token for sessionWorkflowId(ensemble,
134
+ // <recruit name>)). `currentPlayerId` is mutable (set_name), so capture the
135
+ // original here — the publisher's HTTP client URL must match the workflowId.
136
+ const fixedPlayerId = currentPlayerId;
137
+ // Kick off (or reuse) the module-scope shared connection.
138
+ void clientFactory(config);
139
+ // ── D11 lazy proxies: resolve MODULE-SCOPE state per call (instance-independent) ──
140
+ const clientProxy = (0, lazy_proxy_1.createLazyProxy)(() => connectedClient, 'Temporal client');
141
+ const handleProxy = (0, lazy_proxy_1.createLazyProxy)(() => runtimes.get(workflowId)?.wf.handle ?? null, 'workflow handle');
142
+ // ── Register the FULL tool surface on THIS instance's `pi` ──
143
+ const toolOpts = {
144
+ client: clientProxy,
145
+ config,
146
+ getPlayerId: () => currentPlayerId,
147
+ setPlayerId: (id) => { currentPlayerId = id; },
148
+ handle: handleProxy,
149
+ workflowId,
150
+ ownAgentType: PI_AGENT_TYPE,
151
+ isConductor,
152
+ };
153
+ (0, render_tools_1.renderToPi)(pi, (0, server_tools_1.buildAllTempoTools)(toolOpts));
154
+ log(`registered tools (player=${currentPlayerId}, conductor=${isConductor}, mode=${mode})`);
155
+ // ── MD-C tool-access gate (HEADLESS ONLY) ──
156
+ // Interactive Pi = a human owns their machine → no gate. Headless = recruited
157
+ // unsupervised → MD-C governs tool access. TOOL-CLASS CHECK FIRST: shell/exec
158
+ // tools are HARD-BLOCKED at toolAccess='restricted' (the safe unsupervised
159
+ // default) regardless of any later gate logic. The supervised gate (3d) slots
160
+ // in AFTER this MD-C floor — for now, anything MD-C permits is allowed.
161
+ if (mode === 'headless') {
162
+ // 3d MD-G — the operator gate's two loopback clients, keyed to the FIXED
163
+ // player identity (matches the ingest token + workflowId). `gateInner`
164
+ // emits the gate_pending frame (the daemon's ingest side-effect registers
165
+ // the pending) and reports presence {subscribers, gateArmed}; `gateClient`
166
+ // polls the daemon for the operator's decision. Both no-op without the
167
+ // ingest token (so this is inert for a manually-launched headless Pi).
168
+ const gateInner = new inner_loop_client_1.InnerLoopHttpClient({ ensemble: config.ensemble, playerId: fixedPlayerId });
169
+ const gateClient = new gate_client_1.GateClient({ ensemble: config.ensemble, playerId: fixedPlayerId });
170
+ pi.on('tool_call', async (event, ctx) => {
171
+ const cls = (0, tool_capability_1.classify)(event.toolName);
172
+ // 1) MD-C tool-class FLOOR (fires FIRST). classify()==='exec' is the
173
+ // canonical EXEC set (F1) — a SUPERSET that hard-blocks
174
+ // powershell/pwsh/cmd/run at restricted. HARD-block, never gated.
175
+ if (cls === 'exec' && toolAccess === 'restricted') {
176
+ log(`MD-C: blocked '${event.toolName}' (toolAccess=restricted)`);
177
+ return {
178
+ block: true,
179
+ reason: `toolAccess=restricted: shell/exec tools are disabled for this headless Pi player`,
180
+ };
181
+ }
182
+ // 2) MD-G OPERATOR GATE — engage for any non-low-risk tool WHEN an
183
+ // operator is armed AND present (both read from the short-poll
184
+ // cached presence). low-risk bypasses; unknown→high-blast is gated-
185
+ // when-armed (R2). The await resolves on the operator's decision,
186
+ // the daemon's 45s auto-allow, ctx.signal cancel, or the bounded
187
+ // poll deadline — all FAIL-OPEN except an explicit deny.
188
+ if (cls !== 'low-risk' && gateInner.gateArmed(workflowId) && gateInner.subscriberCount(workflowId) > 0) {
189
+ const requestId = crypto.randomUUID();
190
+ gateInner.publish(workflowId, {
191
+ type: 'inner.gate_pending',
192
+ requestId,
193
+ tool: event.toolName,
194
+ argsSummary: (0, inner_loop_publisher_1.truncateSummary)(event.input, 2048),
195
+ classification: cls, // low-risk already returned above
196
+ timeoutMs: gate_registry_1.GATE_AUTO_ALLOW_MS,
197
+ ts: Date.now(),
198
+ });
199
+ const effect = await gateClient.awaitDecision(requestId, { signal: ctx?.signal });
200
+ if (effect === 'deny') {
201
+ log(`MD-G: operator DENIED '${event.toolName}' (req ${requestId})`);
202
+ return { block: true, reason: `operator denied ${event.toolName}` };
203
+ }
204
+ log(`MD-G: '${event.toolName}' permitted (req ${requestId})`);
205
+ return {};
206
+ }
207
+ // 3) not gated → permit.
208
+ return {};
209
+ });
210
+ log(`MD-C+MD-G tool gate active (mode=headless, toolAccess=${toolAccess})`);
211
+ }
212
+ /** Build session metadata from the (current) identity + host. */
213
+ function buildMetadata() {
214
+ return {
215
+ playerId: currentPlayerId,
216
+ ensemble: config.ensemble,
217
+ hostname: os.hostname(),
218
+ workDir: process.cwd(),
219
+ isConductor,
220
+ agentType: PI_AGENT_TYPE,
221
+ adapterId: 'pi',
222
+ };
223
+ }
224
+ /** Persist the active Pi conversation id to metadata.sessionId IF it changed (P2-5). */
225
+ async function refreshSessionId(rt, sessionId) {
226
+ if (!sessionId || sessionId === rt.lastSessionId)
227
+ return;
228
+ await rt.wf.updateSessionId(sessionId);
229
+ rt.lastSessionId = sessionId;
230
+ }
231
+ /**
232
+ * Get-or-create the runtime for this player. FIRST attach claims the lease +
233
+ * starts the heartbeat + cue pump. A subsequent `session_start` (instance
234
+ * rebuild) RE-BINDS the surviving runtime — session pointer only, no re-claim.
235
+ */
236
+ async function attachOrRebind(payload) {
237
+ const existing = runtimes.get(workflowId);
238
+ if (existing) {
239
+ existing.session = payload.session ?? existing.session;
240
+ log(`re-bound ${currentPlayerId} (Pi instance rebuilt; lease intact)`);
241
+ return existing;
242
+ }
243
+ const client = await clientFactory(config);
244
+ const wf = new workflow_client_1.PiWorkflowClient({
245
+ client,
246
+ config,
247
+ metadata: buildMetadata(),
248
+ expectedAttachmentId: process.env[config_1.ENV.ATTACHMENT_ID] || undefined,
249
+ });
250
+ const driver = new phase_driver_1.PhaseDriver();
251
+ const pump = new cue_pump_1.CuePump({
252
+ source: wf,
253
+ resolveSession: () => runtimes.get(workflowId)?.session ?? null,
254
+ });
255
+ // 3c — inner-loop publisher + its loopback-HTTP sink. The client no-ops
256
+ // unless AGENT_TEMPO_INGEST_TOKEN is present (daemon-spawned headless
257
+ // players only), so interactive Pi gets Tier-1 coarse for free and zero
258
+ // Tier-2 forwarding. URL keyed to the FIXED playerId (matches workflowId).
259
+ const registry = new inner_loop_client_1.InnerLoopHttpClient({ ensemble: config.ensemble, playerId: fixedPlayerId });
260
+ const pub = new inner_loop_publisher_1.InnerLoopPublisher({ workflowId, registry });
261
+ // 3d D14 — reset poll-tick (sibling to the cue pump): polls pendingReset →
262
+ // session.newSession() clean-wipe + ack. resolveSession re-acquired each
263
+ // tick so a session switch never wipes a stale session.
264
+ const reset = new reset_pump_1.ResetPump({
265
+ source: wf,
266
+ resolveSession: () => runtimes.get(workflowId)?.session ?? null,
267
+ });
268
+ const rt = { workflowId, wf, driver, pump, pub, reset, session: payload.session ?? null };
269
+ runtimes.set(workflowId, rt);
270
+ await wf.ensureSessionWorkflow();
271
+ const result = driver.handle('session_start', payload, nowIso());
272
+ await wf.performAction(result.action); // claim → attached, starts heartbeat
273
+ pump.start();
274
+ // Start the publisher AFTER the claim (heartbeat is live → coarse samples
275
+ // have a delivery path) and wire its coarse state into the heartbeat. The
276
+ // bound method is wrapped so `this` survives the provider call.
277
+ pub.start(pi);
278
+ wf.setCoarseProvider(() => pub.getCoarseState());
279
+ reset.start(); // 3d D14 — begin polling for pending resets
280
+ log(`attached ${currentPlayerId} (wf ${workflowId})`);
281
+ return rt;
282
+ }
283
+ // ── Lifecycle: session_start → first attach OR re-bind ──
284
+ pi.on('session_start', async (payload) => {
285
+ try {
286
+ const rt = await attachOrRebind(payload);
287
+ await refreshSessionId(rt, rt.session?.id);
288
+ }
289
+ catch (err) {
290
+ log('session_start wiring failed:', err);
291
+ }
292
+ });
293
+ // ── Lifecycle: phase-affecting events ──
294
+ for (const event of ['agent_start', 'agent_end']) {
295
+ pi.on(event, async (payload) => {
296
+ const rt = runtimes.get(workflowId);
297
+ if (!rt)
298
+ return;
299
+ if (payload.session)
300
+ rt.session = payload.session;
301
+ const result = rt.driver.handle(event, payload, nowIso());
302
+ try {
303
+ await rt.wf.performAction(result.action);
304
+ if (event === 'agent_start')
305
+ await refreshSessionId(rt, rt.session?.id);
306
+ }
307
+ catch (err) {
308
+ log(`${event} → ${result.action.kind} failed:`, err);
309
+ }
310
+ });
311
+ }
312
+ // ── Lifecycle: activity-only events (NEVER drive phase) ──
313
+ for (const event of [
314
+ 'turn_start', 'turn_end', 'tool_execution_start', 'tool_execution_end',
315
+ ]) {
316
+ pi.on(event, (payload) => {
317
+ const rt = runtimes.get(workflowId);
318
+ if (!rt)
319
+ return;
320
+ if (payload.session)
321
+ rt.session = payload.session;
322
+ rt.driver.handle(event, payload, nowIso());
323
+ });
324
+ }
325
+ // ── Lifecycle: session_shutdown → Option C (reason-discriminated teardown) ──
326
+ pi.on('session_shutdown', async (payload) => {
327
+ const rt = runtimes.get(workflowId);
328
+ if (!rt)
329
+ return;
330
+ rt.session = null; // switch gap: cue pump stops injecting (dodges Pi #2860)
331
+ if (payload.reason === 'quit') {
332
+ rt.pub.stop(); // 3c — stop observing + flush the trailing coalesce buffer
333
+ rt.reset.stop(); // 3d — stop the reset poll
334
+ try {
335
+ await rt.wf.detach('agent-exited'); // requestDetach + adapterExited + stopHeartbeat
336
+ runtimes.delete(workflowId);
337
+ }
338
+ catch (err) {
339
+ log('quit detach (best-effort) failed — reaper will backstop:', err);
340
+ }
341
+ }
342
+ });
343
+ };
344
+ }
345
+ /**
346
+ * RELIABLE detach for the headless exit sequence (Phase 3a). Headless owns its
347
+ * exit loop, so — unlike interactive's best-effort `quit` path — it can AWAIT a
348
+ * clean detach before disposing the SDK session. Ordering (architect ruling):
349
+ * stopHeartbeat → requestDetach → adapterExited (all inside `wf.detach`) → unmap.
350
+ * The caller then calls `session.dispose()`; the dispose-fired `session_shutdown`
351
+ * finds no mapped runtime → no-op (avoids double-detach). Detaches every runtime
352
+ * in the process (headless = one player per process).
353
+ */
354
+ async function detachAllPiRuntimesForExit() {
355
+ for (const rt of runtimes.values()) {
356
+ rt.pub.stop(); // 3c — stop the inner-loop publisher before detaching
357
+ rt.reset.stop(); // 3d — stop the reset poll before detaching
358
+ try {
359
+ await rt.wf.detach('agent-exited');
360
+ }
361
+ catch (err) {
362
+ log('headless detach failed (reaper will backstop):', err);
363
+ }
364
+ }
365
+ runtimes.clear();
366
+ }
367
+ /**
368
+ * Headless-only: wire the live Pi SDK session onto a runtime so the cue pump can
369
+ * inject into it. The interactive CLI's `session_start` payload carries
370
+ * `session`, but the headless SDK's DEFAULT session_start payload does NOT (it's
371
+ * `{ type, reason }`) — so `attachOrRebind` sets `rt.session = null` and the cue
372
+ * pump's `resolveSession` returns null (every cue is dropped). The headless entry
373
+ * HOLDS the session from `createAgentSession`, so it calls this after
374
+ * `bindExtensions` (by which point the runtime exists + has claimed) to set it.
375
+ * (3a live smoke — devops.)
376
+ */
377
+ function setRuntimeSession(workflowId, session) {
378
+ const rt = runtimes.get(workflowId);
379
+ if (rt) {
380
+ rt.session = session;
381
+ log(`headless session wired to runtime (wf ${workflowId})`);
382
+ }
383
+ else {
384
+ log(`setRuntimeSession: no runtime for ${workflowId} yet (session_start may not have fired)`);
385
+ }
386
+ }
387
+ // ── Test-only hooks (ADR 0006 `__<verb><Noun>ForTests` convention) ──
388
+ /** Override the Temporal connection factory (inject a fake Client). */
389
+ function __setPiClientFactoryForTests(factory) {
390
+ clientFactory = factory;
391
+ }
392
+ /** Stop timers, clear the per-player runtime map + shared-client singletons + factory. */
393
+ function __resetPiRuntimesForTests() {
394
+ for (const rt of runtimes.values()) {
395
+ rt.pub.stop();
396
+ rt.reset.stop();
397
+ rt.pump.stop();
398
+ rt.wf.stopHeartbeat();
399
+ }
400
+ runtimes.clear();
401
+ sharedClientPromise = null;
402
+ connectedClient = null;
403
+ clientFactory = getSharedClient;
404
+ }
405
+ /** Default export — interactive-mode extension (the human `pi` CLI entry). */
406
+ const piExtension = createPiExtension();
407
+ exports.default = piExtension;
@@ -0,0 +1,54 @@
1
+ /** Env var carrying the per-player ingest token (threaded in at spawn, shared w/ inner-loop). */
2
+ export declare const INGEST_TOKEN_ENV = "AGENT_TEMPO_INGEST_TOKEN";
3
+ /** What the handler does with the result. */
4
+ export type GateEffect = 'allow' | 'deny';
5
+ /** Minimal `fetch` shape (injectable for tests) — same contract as the inner-loop client. */
6
+ export type GateFetch = (url: string, init: {
7
+ method: string;
8
+ headers: Record<string, string>;
9
+ }) => Promise<{
10
+ status: number;
11
+ json(): Promise<unknown>;
12
+ }>;
13
+ export interface GateClientOptions {
14
+ ensemble: string;
15
+ playerId: string;
16
+ ingestToken?: string;
17
+ readPort?: () => number | null;
18
+ fetchFn?: GateFetch;
19
+ pollIntervalMs?: number;
20
+ timeoutMs?: number;
21
+ now?: () => number;
22
+ /** Cancellable wait (tests inject a synchronous/controllable one). */
23
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
24
+ }
25
+ /**
26
+ * Loopback poll-bridge to the daemon gate. Construct one per headless player.
27
+ * The `awaitDecision` poll is the only public surface used by the engagement path.
28
+ */
29
+ export declare class GateClient {
30
+ private readonly ensemble;
31
+ private readonly playerId;
32
+ private readonly ingestToken;
33
+ private readonly readPort;
34
+ private readonly fetchFn;
35
+ private readonly pollIntervalMs;
36
+ private readonly timeoutMs;
37
+ private readonly now;
38
+ private readonly sleep;
39
+ constructor(opts: GateClientOptions);
40
+ private get enabled();
41
+ private resolutionUrl;
42
+ /** One poll. Returns the effect on a resolved answer, or null to keep polling. */
43
+ private pollOnce;
44
+ /**
45
+ * Poll the daemon for the operator's decision on `requestId`, blocking until
46
+ * resolved / timeout / abort. FAIL-OPEN: `allow` unless the operator explicitly
47
+ * denied. Without a token/transport (e.g. interactive Pi, daemon HTTP off) →
48
+ * immediate `allow` (the gate is a daemon-mediated feature).
49
+ */
50
+ awaitDecision(requestId: string, opts?: {
51
+ signal?: AbortSignal;
52
+ timeoutMs?: number;
53
+ }): Promise<GateEffect>;
54
+ }