agent-tempo 1.3.1 → 1.4.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 (199) hide show
  1. package/CLAUDE.md +39 -5
  2. package/README.md +6 -2
  3. package/dashboard/dist/assets/{index-D6Xyje_n.js → index-jmYe6rmS.js} +2 -2
  4. package/dashboard/dist/assets/index-jmYe6rmS.js.map +1 -0
  5. package/dashboard/dist/index.html +1 -1
  6. package/dashboard/package.json +1 -1
  7. package/dist/activities/outbox.d.ts +30 -1
  8. package/dist/activities/outbox.js +96 -3
  9. package/dist/adapters/base.js +5 -0
  10. package/dist/adapters/index.d.ts +1 -1
  11. package/dist/adapters/index.js +7 -0
  12. package/dist/adapters/pi/adapter.d.ts +2 -0
  13. package/dist/adapters/pi/adapter.js +43 -0
  14. package/dist/adapters/pi/index.d.ts +16 -0
  15. package/dist/adapters/pi/index.js +10 -0
  16. package/dist/client/core.js +9 -2
  17. package/dist/client/interface.d.ts +6 -0
  18. package/dist/config.d.ts +79 -0
  19. package/dist/config.js +74 -0
  20. package/dist/daemon.js +32 -1
  21. package/dist/http/aggregate.d.ts +22 -1
  22. package/dist/http/aggregate.js +41 -0
  23. package/dist/http/auth.d.ts +94 -8
  24. package/dist/http/auth.js +93 -9
  25. package/dist/http/body.d.ts +4 -1
  26. package/dist/http/body.js +6 -3
  27. package/dist/http/event-bus.js +1 -0
  28. package/dist/http/event-types.d.ts +34 -2
  29. package/dist/http/event-types.js +1 -0
  30. package/dist/http/gate-audit.d.ts +12 -0
  31. package/dist/http/gate-audit.js +95 -0
  32. package/dist/http/gate-registry.d.ts +167 -0
  33. package/dist/http/gate-registry.js +163 -0
  34. package/dist/http/gate-routes.d.ts +48 -0
  35. package/dist/http/gate-routes.js +102 -0
  36. package/dist/http/ingest-registry.d.ts +30 -0
  37. package/dist/http/ingest-registry.js +108 -0
  38. package/dist/http/inner-loop-routes.d.ts +66 -0
  39. package/dist/http/inner-loop-routes.js +182 -0
  40. package/dist/http/inner-loop.d.ts +92 -0
  41. package/dist/http/inner-loop.js +155 -0
  42. package/dist/http/server.d.ts +38 -3
  43. package/dist/http/server.js +211 -6
  44. package/dist/http/snapshot.d.ts +6 -0
  45. package/dist/http/snapshot.js +6 -0
  46. package/dist/pi/cue-pump.d.ts +61 -0
  47. package/dist/pi/cue-pump.js +95 -0
  48. package/dist/pi/extension.d.ts +45 -0
  49. package/dist/pi/extension.js +407 -0
  50. package/dist/pi/gate-client.d.ts +54 -0
  51. package/dist/pi/gate-client.js +136 -0
  52. package/dist/pi/headless.d.ts +85 -0
  53. package/dist/pi/headless.js +250 -0
  54. package/dist/pi/index.d.ts +28 -0
  55. package/dist/pi/index.js +43 -0
  56. package/dist/pi/inner-loop-client.d.ts +67 -0
  57. package/dist/pi/inner-loop-client.js +164 -0
  58. package/dist/pi/inner-loop-publisher.d.ts +187 -0
  59. package/dist/pi/inner-loop-publisher.js +236 -0
  60. package/dist/pi/lazy-proxy.d.ts +37 -0
  61. package/dist/pi/lazy-proxy.js +55 -0
  62. package/dist/pi/mission-control/actions.d.ts +48 -0
  63. package/dist/pi/mission-control/actions.js +98 -0
  64. package/dist/pi/mission-control/board.d.ts +88 -0
  65. package/dist/pi/mission-control/board.js +141 -0
  66. package/dist/pi/mission-control/extension.d.ts +51 -0
  67. package/dist/pi/mission-control/extension.js +330 -0
  68. package/dist/pi/mission-control/index.d.ts +15 -0
  69. package/dist/pi/mission-control/index.js +32 -0
  70. package/dist/pi/mission-control/inner-tail.d.ts +48 -0
  71. package/dist/pi/mission-control/inner-tail.js +76 -0
  72. package/dist/pi/mission-control/pi-ui.d.ts +43 -0
  73. package/dist/pi/mission-control/pi-ui.js +10 -0
  74. package/dist/pi/mission-control/render.d.ts +6 -0
  75. package/dist/pi/mission-control/render.js +98 -0
  76. package/dist/pi/phase-driver.d.ts +74 -0
  77. package/dist/pi/phase-driver.js +122 -0
  78. package/dist/pi/pi-types.d.ts +222 -0
  79. package/dist/pi/pi-types.js +21 -0
  80. package/dist/pi/probe.d.ts +99 -0
  81. package/dist/pi/probe.js +179 -0
  82. package/dist/pi/render-tools.d.ts +17 -0
  83. package/dist/pi/render-tools.js +56 -0
  84. package/dist/pi/reset-pump.d.ts +47 -0
  85. package/dist/pi/reset-pump.js +85 -0
  86. package/dist/pi/session-seed.d.ts +74 -0
  87. package/dist/pi/session-seed.js +103 -0
  88. package/dist/pi/tool-capability.d.ts +60 -0
  89. package/dist/pi/tool-capability.js +156 -0
  90. package/dist/pi/workflow-client.d.ts +158 -0
  91. package/dist/pi/workflow-client.js +289 -0
  92. package/dist/pi/zod-to-typebox.d.ts +74 -0
  93. package/dist/pi/zod-to-typebox.js +191 -0
  94. package/dist/server-tools.d.ts +2 -0
  95. package/dist/server-tools.js +50 -46
  96. package/dist/spawn.d.ts +55 -0
  97. package/dist/spawn.js +72 -0
  98. package/dist/tools/agent-types.d.ts +2 -2
  99. package/dist/tools/agent-types.js +22 -17
  100. package/dist/tools/attachment-info.d.ts +2 -2
  101. package/dist/tools/attachment-info.js +38 -33
  102. package/dist/tools/broadcast.d.ts +2 -2
  103. package/dist/tools/broadcast.js +69 -64
  104. package/dist/tools/cancel-stage.d.ts +2 -2
  105. package/dist/tools/cancel-stage.js +20 -15
  106. package/dist/tools/clear-state.d.ts +2 -2
  107. package/dist/tools/clear-state.js +25 -20
  108. package/dist/tools/coat-check-evict.d.ts +2 -2
  109. package/dist/tools/coat-check-evict.js +29 -24
  110. package/dist/tools/coat-check-get.d.ts +2 -2
  111. package/dist/tools/coat-check-get.js +38 -33
  112. package/dist/tools/coat-check-list.d.ts +2 -2
  113. package/dist/tools/coat-check-list.js +48 -43
  114. package/dist/tools/coat-check-put.d.ts +2 -2
  115. package/dist/tools/coat-check-put.js +38 -33
  116. package/dist/tools/cue.d.ts +2 -2
  117. package/dist/tools/cue.js +57 -52
  118. package/dist/tools/descriptor.d.ts +72 -0
  119. package/dist/tools/descriptor.js +39 -0
  120. package/dist/tools/destroy.d.ts +2 -2
  121. package/dist/tools/destroy.js +153 -148
  122. package/dist/tools/ensemble.d.ts +2 -2
  123. package/dist/tools/ensemble.js +71 -66
  124. package/dist/tools/evaluate-gate.d.ts +2 -2
  125. package/dist/tools/evaluate-gate.js +33 -27
  126. package/dist/tools/fetch-state.d.ts +2 -2
  127. package/dist/tools/fetch-state.js +42 -37
  128. package/dist/tools/gates.d.ts +2 -2
  129. package/dist/tools/gates.js +39 -34
  130. package/dist/tools/hosts.d.ts +2 -2
  131. package/dist/tools/hosts.js +25 -20
  132. package/dist/tools/listen.d.ts +2 -2
  133. package/dist/tools/listen.js +23 -18
  134. package/dist/tools/load-lineup.d.ts +2 -2
  135. package/dist/tools/load-lineup.js +324 -319
  136. package/dist/tools/migrate.d.ts +2 -2
  137. package/dist/tools/migrate.js +45 -40
  138. package/dist/tools/pause.d.ts +2 -2
  139. package/dist/tools/pause.js +34 -29
  140. package/dist/tools/play.d.ts +2 -2
  141. package/dist/tools/play.js +53 -48
  142. package/dist/tools/quality-gate.d.ts +2 -2
  143. package/dist/tools/quality-gate.js +26 -21
  144. package/dist/tools/recall.d.ts +2 -2
  145. package/dist/tools/recall.js +32 -27
  146. package/dist/tools/recruit.d.ts +2 -2
  147. package/dist/tools/recruit.js +340 -256
  148. package/dist/tools/release.d.ts +2 -2
  149. package/dist/tools/release.js +85 -80
  150. package/dist/tools/report.d.ts +2 -2
  151. package/dist/tools/report.js +28 -23
  152. package/dist/tools/reset.d.ts +3 -0
  153. package/dist/tools/reset.js +51 -0
  154. package/dist/tools/restart.d.ts +2 -2
  155. package/dist/tools/restart.js +51 -46
  156. package/dist/tools/restore.d.ts +2 -2
  157. package/dist/tools/restore.js +76 -71
  158. package/dist/tools/save-lineup.d.ts +2 -2
  159. package/dist/tools/save-lineup.js +32 -27
  160. package/dist/tools/save-state.d.ts +2 -2
  161. package/dist/tools/save-state.js +31 -26
  162. package/dist/tools/schedule.d.ts +2 -2
  163. package/dist/tools/schedule.js +133 -128
  164. package/dist/tools/schedules.d.ts +2 -2
  165. package/dist/tools/schedules.js +41 -36
  166. package/dist/tools/set-ensemble-description.d.ts +2 -2
  167. package/dist/tools/set-ensemble-description.js +26 -21
  168. package/dist/tools/set-name.d.ts +2 -2
  169. package/dist/tools/set-name.js +38 -33
  170. package/dist/tools/set-part.d.ts +2 -2
  171. package/dist/tools/set-part.js +20 -15
  172. package/dist/tools/shutdown.d.ts +2 -2
  173. package/dist/tools/shutdown.js +39 -34
  174. package/dist/tools/stage.d.ts +2 -2
  175. package/dist/tools/stage.js +28 -23
  176. package/dist/tools/stages.d.ts +2 -2
  177. package/dist/tools/stages.js +36 -31
  178. package/dist/tools/unschedule.d.ts +2 -2
  179. package/dist/tools/unschedule.js +30 -25
  180. package/dist/tools/who-am-i.d.ts +2 -2
  181. package/dist/tools/who-am-i.js +36 -31
  182. package/dist/tools/worktree.d.ts +2 -2
  183. package/dist/tools/worktree.js +134 -129
  184. package/dist/tui/index.js +6 -6
  185. package/dist/types.d.ts +47 -2
  186. package/dist/types.js +1 -1
  187. package/dist/utils/default-part.js +1 -0
  188. package/dist/utils/sdk-probe.d.ts +23 -0
  189. package/dist/utils/sdk-probe.js +46 -7
  190. package/dist/worker.d.ts +3 -1
  191. package/dist/worker.js +6 -2
  192. package/dist/workflows/session.js +70 -2
  193. package/dist/workflows/signals.d.ts +32 -2
  194. package/dist/workflows/signals.js +25 -2
  195. package/package.json +4 -1
  196. package/workflow-bundle.js +97 -6
  197. package/dashboard/dist/assets/index-D6Xyje_n.js.map +0 -1
  198. package/dist/tools/helpers.d.ts +0 -21
  199. package/dist/tools/helpers.js +0 -25
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Mission-control board model + reducers (3f) — PURE, no Pi/daemon/IO.
3
+ *
4
+ * Builds an in-memory view of the ensemble from the daemon's coarse SSE stream
5
+ * (`TempoEvent` over `/v1/events/:ensemble`) and a fine inner-loop tail
6
+ * (`InnerFrame` over `/v1/players/:e/:p/inner`) for the SELECTED player. The
7
+ * extension applies events here, then renders the model on a throttled tick —
8
+ * decoupling event-rate from render-rate (decision 3). Pure so it unit-tests
9
+ * without Pi or the daemon.
10
+ */
11
+ import type { TempoEvent, AttachmentPhase } from '../../http/event-types';
12
+ import type { InnerFrame } from '../inner-loop-publisher';
13
+ /** One row on the board — the coarse, always-on view of a player. */
14
+ export interface PlayerRow {
15
+ playerId: string;
16
+ isConductor: boolean;
17
+ /**
18
+ * Daemon host the player runs on (carried from `PlayerSummaryV1.hostname`,
19
+ * 3f/H3a). Undefined on older pre-hostname snapshots → treated as tailable
20
+ * (never block on absent data). Drives {@link tailability}.
21
+ */
22
+ hostname?: string;
23
+ phase?: AttachmentPhase;
24
+ part: string;
25
+ /** Tool currently executing (3c coarse), `null`/undefined = idle. */
26
+ currentTool?: string | null;
27
+ /** Context-window usage fraction/percent (3c coarse). */
28
+ contextPercent?: number;
29
+ /** ISO timestamp of the last coarse activity. */
30
+ lastActivityAt?: string;
31
+ }
32
+ /** Default cap on the retained fine-tail frames for the selected player. */
33
+ export declare const DEFAULT_TAIL_LIMIT = 200;
34
+ export interface BoardModel {
35
+ ensemble: string;
36
+ /** playerId → row, insertion-ordered by the Map. */
37
+ players: Map<string, PlayerRow>;
38
+ /** The player whose fine inner-loop tail is shown, or null. */
39
+ selected: string | null;
40
+ /** Bounded ring of the selected player's recent inner frames (oldest→newest). */
41
+ innerTail: InnerFrame[];
42
+ /** Max retained tail frames. */
43
+ tailLimit: number;
44
+ /** Monotonic counter — bumped on every mutation so the render tick can skip no-op ticks. */
45
+ revision: number;
46
+ }
47
+ export declare function initBoard(ensemble: string, tailLimit?: number): BoardModel;
48
+ /**
49
+ * Fold one coarse `TempoEvent` into the board (mutates + bumps revision). Unknown
50
+ * event kinds are ignored — the board only tracks the player set + phase + the
51
+ * 3c coarse activity fields.
52
+ */
53
+ export declare function applyTempoEvent(model: BoardModel, ev: TempoEvent): void;
54
+ /** Append a fine inner-loop frame for the selected player (bounded ring). */
55
+ export declare function applyInnerFrame(model: BoardModel, frame: InnerFrame): void;
56
+ /** Select a player for the fine tail (clears the prior tail). No-op if absent. */
57
+ export declare function selectPlayer(model: BoardModel, playerId: string | null): boolean;
58
+ /** Result of a {@link tailability} check — whether the operator can open a fine /inner tail. */
59
+ export type Tailability = {
60
+ ok: true;
61
+ } | {
62
+ ok: false;
63
+ reason: 'no-such-player';
64
+ } | {
65
+ ok: false;
66
+ reason: 'ui-player';
67
+ } | {
68
+ ok: false;
69
+ reason: 'cross-host';
70
+ playerHost: string;
71
+ };
72
+ /**
73
+ * Pure: can the local operator open `playerId`'s fine inner-loop tail? The 3f
74
+ * inner-loop tail is DAEMON-LOCAL — only players on this daemon's host are
75
+ * tailable.
76
+ *
77
+ * - missing player → `no-such-player`
78
+ * - the maestro/dashboard UI player (no /inner stream) → `ui-player` (H5; checked
79
+ * BEFORE the host comparison so its `dashboard` sentinel isn't mis-framed as
80
+ * cross-host)
81
+ * - a real player on another host → `cross-host` (carrying `playerHost`); actual
82
+ * cross-host routing is the deferred H3(b) (#645)
83
+ * - a missing/older-snapshot `hostname` (undefined) → tailable; never block on
84
+ * absent data
85
+ */
86
+ export declare function tailability(model: BoardModel, playerId: string, localHost: string): Tailability;
87
+ /** Sorted player ids — conductor first, then alphabetical (stable board ordering). */
88
+ export declare function sortedPlayerIds(model: BoardModel): string[];
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_TAIL_LIMIT = void 0;
4
+ exports.initBoard = initBoard;
5
+ exports.applyTempoEvent = applyTempoEvent;
6
+ exports.applyInnerFrame = applyInnerFrame;
7
+ exports.selectPlayer = selectPlayer;
8
+ exports.tailability = tailability;
9
+ exports.sortedPlayerIds = sortedPlayerIds;
10
+ /** Default cap on the retained fine-tail frames for the selected player. */
11
+ exports.DEFAULT_TAIL_LIMIT = 200;
12
+ function initBoard(ensemble, tailLimit = exports.DEFAULT_TAIL_LIMIT) {
13
+ return { ensemble, players: new Map(), selected: null, innerTail: [], tailLimit, revision: 0 };
14
+ }
15
+ /** Project a PlayerSummaryV1 (snapshot / player.added) into a row. */
16
+ function rowFromSummary(p) {
17
+ return {
18
+ playerId: p.playerId,
19
+ isConductor: p.isConductor,
20
+ // H3a: carry the host through — the board previously DROPPED it. Drives
21
+ // cross-host tail refusal in `tailability`.
22
+ ...(p.hostname !== undefined ? { hostname: p.hostname } : {}),
23
+ ...(p.phase !== undefined ? { phase: p.phase } : {}),
24
+ part: p.part ?? '',
25
+ ...(p.currentTool !== undefined ? { currentTool: p.currentTool } : {}),
26
+ ...(p.contextPercent !== undefined ? { contextPercent: p.contextPercent } : {}),
27
+ ...(p.lastActivityAt !== undefined ? { lastActivityAt: p.lastActivityAt } : {}),
28
+ };
29
+ }
30
+ /**
31
+ * Fold one coarse `TempoEvent` into the board (mutates + bumps revision). Unknown
32
+ * event kinds are ignored — the board only tracks the player set + phase + the
33
+ * 3c coarse activity fields.
34
+ */
35
+ function applyTempoEvent(model, ev) {
36
+ switch (ev.type) {
37
+ case 'snapshot': {
38
+ // Authoritative rebuild from the snapshot's player list.
39
+ model.players = new Map(ev.payload.players.map((p) => [p.playerId, rowFromSummary(p)]));
40
+ // Drop a selection that no longer exists.
41
+ if (model.selected && !model.players.has(model.selected)) {
42
+ model.selected = null;
43
+ model.innerTail = [];
44
+ }
45
+ break;
46
+ }
47
+ case 'player.added': {
48
+ model.players.set(ev.payload.playerId, rowFromSummary(ev.payload));
49
+ break;
50
+ }
51
+ case 'player.removed': {
52
+ model.players.delete(ev.payload.playerId);
53
+ if (model.selected === ev.payload.playerId) {
54
+ model.selected = null;
55
+ model.innerTail = [];
56
+ }
57
+ break;
58
+ }
59
+ case 'player.phase_changed': {
60
+ const row = model.players.get(ev.payload.playerId);
61
+ if (row) {
62
+ row.phase = ev.payload.phase;
63
+ row.lastActivityAt = ev.payload.at;
64
+ }
65
+ break;
66
+ }
67
+ case 'player.activity': {
68
+ const row = model.players.get(ev.payload.playerId);
69
+ if (row) {
70
+ row.currentTool = ev.payload.currentTool;
71
+ if (ev.payload.contextPercent !== undefined)
72
+ row.contextPercent = ev.payload.contextPercent;
73
+ row.lastActivityAt = ev.payload.at;
74
+ }
75
+ break;
76
+ }
77
+ default:
78
+ return; // not board-relevant — no revision bump
79
+ }
80
+ model.revision++;
81
+ }
82
+ /** Append a fine inner-loop frame for the selected player (bounded ring). */
83
+ function applyInnerFrame(model, frame) {
84
+ model.innerTail.push(frame);
85
+ if (model.innerTail.length > model.tailLimit) {
86
+ model.innerTail.splice(0, model.innerTail.length - model.tailLimit);
87
+ }
88
+ model.revision++;
89
+ }
90
+ /** Select a player for the fine tail (clears the prior tail). No-op if absent. */
91
+ function selectPlayer(model, playerId) {
92
+ if (playerId !== null && !model.players.has(playerId))
93
+ return false;
94
+ model.selected = playerId;
95
+ model.innerTail = [];
96
+ model.revision++;
97
+ return true;
98
+ }
99
+ /**
100
+ * Sentinel hostname the TUI's own maestro/dashboard session stamps on its
101
+ * metadata (see `ensureMaestroSession` in `client/core.ts`). It's a UI player
102
+ * with NO /inner stream — non-tailable, but NOT a cross-host case (the sentinel
103
+ * is not a real daemon host). Mirrored locally to avoid a client→mission-control
104
+ * import; the maestro-tail test trips if it ever drifts. (H5)
105
+ */
106
+ const UI_PLAYER_HOSTNAME = 'dashboard';
107
+ /**
108
+ * Pure: can the local operator open `playerId`'s fine inner-loop tail? The 3f
109
+ * inner-loop tail is DAEMON-LOCAL — only players on this daemon's host are
110
+ * tailable.
111
+ *
112
+ * - missing player → `no-such-player`
113
+ * - the maestro/dashboard UI player (no /inner stream) → `ui-player` (H5; checked
114
+ * BEFORE the host comparison so its `dashboard` sentinel isn't mis-framed as
115
+ * cross-host)
116
+ * - a real player on another host → `cross-host` (carrying `playerHost`); actual
117
+ * cross-host routing is the deferred H3(b) (#645)
118
+ * - a missing/older-snapshot `hostname` (undefined) → tailable; never block on
119
+ * absent data
120
+ */
121
+ function tailability(model, playerId, localHost) {
122
+ const row = model.players.get(playerId);
123
+ if (!row)
124
+ return { ok: false, reason: 'no-such-player' };
125
+ if (row.hostname === UI_PLAYER_HOSTNAME)
126
+ return { ok: false, reason: 'ui-player' };
127
+ if (row.hostname && row.hostname !== localHost) {
128
+ return { ok: false, reason: 'cross-host', playerHost: row.hostname };
129
+ }
130
+ return { ok: true };
131
+ }
132
+ /** Sorted player ids — conductor first, then alphabetical (stable board ordering). */
133
+ function sortedPlayerIds(model) {
134
+ return [...model.players.values()]
135
+ .sort((a, b) => {
136
+ if (a.isConductor !== b.isConductor)
137
+ return a.isConductor ? -1 : 1;
138
+ return a.playerId.localeCompare(b.playerId);
139
+ })
140
+ .map((r) => r.playerId);
141
+ }
@@ -0,0 +1,51 @@
1
+ import { type BoardModel } from './board';
2
+ import { MissionControlActions } from './actions';
3
+ import type { McExtensionAPI, McExtensionContext } from './pi-ui';
4
+ /** Injectable seams (production defaults; tests override). */
5
+ export interface MissionControlDeps {
6
+ ensemble?: string;
7
+ adminToken?: string;
8
+ baseUrl?: string;
9
+ renderThrottleMs?: number;
10
+ /** Local daemon host for tailability (test override; defaults to `os.hostname()`). */
11
+ localHost?: string;
12
+ }
13
+ /**
14
+ * The operator-command + board controller. Holds the model + the action client;
15
+ * command methods are independently unit-testable with a fake actions + ctx.
16
+ * The lifecycle (SSE/render/teardown) lives in {@link createMissionControlExtension}.
17
+ */
18
+ export declare class Controller {
19
+ readonly model: BoardModel;
20
+ readonly actions: MissionControlActions;
21
+ /**
22
+ * This daemon's host (`os.hostname()`). The fine /inner tail is daemon-local,
23
+ * so only same-host players are tailable — see {@link tailability}.
24
+ */
25
+ readonly localHost: string;
26
+ /** Set by the extension so /tail can (re)open the fine SSE; null in unit tests. */
27
+ onTailRequest: ((playerId: string | null) => void) | null;
28
+ constructor(ensemble: string, actions: MissionControlActions, localHost?: string);
29
+ private notify;
30
+ private report;
31
+ /** First whitespace-delimited token + the remainder. */
32
+ private static splitFirst;
33
+ cmdPlayers(ctx: McExtensionContext): Promise<void>;
34
+ cmdTail(args: string, ctx: McExtensionContext): Promise<void>;
35
+ cmdCue(args: string, ctx: McExtensionContext): Promise<void>;
36
+ cmdPause(_args: string, ctx: McExtensionContext): Promise<void>;
37
+ cmdPlay(args: string, ctx: McExtensionContext): Promise<void>;
38
+ cmdRestart(args: string, ctx: McExtensionContext): Promise<void>;
39
+ cmdDestroy(args: string, ctx: McExtensionContext): Promise<void>;
40
+ cmdReset(args: string, ctx: McExtensionContext): Promise<void>;
41
+ cmdArm(args: string, ctx: McExtensionContext): Promise<void>;
42
+ cmdGate(args: string, ctx: McExtensionContext): Promise<void>;
43
+ }
44
+ /**
45
+ * Build the mission-control extension (default-export shape). The operator's Pi
46
+ * loads it in OBSERVER mode. `deps` overrides config/token/baseUrl for tests.
47
+ */
48
+ export declare function createMissionControlExtension(deps?: MissionControlDeps): (pi: McExtensionAPI) => void;
49
+ /** Default export — the loadable Pi extension. */
50
+ declare const missionControlExtension: (pi: McExtensionAPI) => void;
51
+ export default missionControlExtension;
@@ -0,0 +1,330 @@
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.Controller = void 0;
37
+ exports.createMissionControlExtension = createMissionControlExtension;
38
+ /**
39
+ * Mission-control Pi extension (3f) — turns ONE interactive Pi TUI into an
40
+ * ensemble mission-control board + operator controller.
41
+ *
42
+ * Three ruled decisions:
43
+ * 1. DRIVE = HTTP — controls POST to the daemon write/gate surface ({@link MissionControlActions}).
44
+ * 2. OBSERVER-ONLY — this extension NEVER claimAttachment / registers as a player.
45
+ * 3. RENDER THROTTLE ~200ms — events fold into the in-memory {@link BoardModel};
46
+ * a tick re-renders only when the model changed (revision bump), so the
47
+ * /inner tail can't thrash the TUI.
48
+ *
49
+ * Lifecycle: `session_start` opens the coarse SSE (`/v1/events/:ensemble` via the
50
+ * Node `subscribe` path), starts the render tick, and registers operator
51
+ * commands; `session_shutdown` tears all of it down + clears the widget.
52
+ */
53
+ const os = __importStar(require("os"));
54
+ const config_1 = require("../../config");
55
+ const port_file_1 = require("../../http/port-file");
56
+ const subscribe_1 = require("../../client/subscribe");
57
+ const board_1 = require("./board");
58
+ const render_1 = require("./render");
59
+ const actions_1 = require("./actions");
60
+ const inner_tail_1 = require("./inner-tail");
61
+ const WIDGET_KEY = 'mission-control';
62
+ const DEFAULT_RENDER_THROTTLE_MS = 200;
63
+ const DEFAULT_PORT = 8473;
64
+ /**
65
+ * The operator-command + board controller. Holds the model + the action client;
66
+ * command methods are independently unit-testable with a fake actions + ctx.
67
+ * The lifecycle (SSE/render/teardown) lives in {@link createMissionControlExtension}.
68
+ */
69
+ class Controller {
70
+ model;
71
+ actions;
72
+ /**
73
+ * This daemon's host (`os.hostname()`). The fine /inner tail is daemon-local,
74
+ * so only same-host players are tailable — see {@link tailability}.
75
+ */
76
+ localHost;
77
+ /** Set by the extension so /tail can (re)open the fine SSE; null in unit tests. */
78
+ onTailRequest = null;
79
+ constructor(ensemble, actions, localHost = os.hostname()) {
80
+ this.model = (0, board_1.initBoard)(ensemble);
81
+ this.actions = actions;
82
+ this.localHost = localHost;
83
+ }
84
+ notify(ctx, msg) {
85
+ if (ctx.hasUI)
86
+ ctx.ui.notify(msg);
87
+ }
88
+ report(ctx, label, r) {
89
+ this.notify(ctx, r.ok ? `${label} ✓` : `${label} failed: ${r.error}`);
90
+ }
91
+ /** First whitespace-delimited token + the remainder. */
92
+ static splitFirst(args) {
93
+ const t = args.trim();
94
+ const i = t.indexOf(' ');
95
+ return i < 0 ? [t, ''] : [t.slice(0, i), t.slice(i + 1).trim()];
96
+ }
97
+ async cmdPlayers(ctx) {
98
+ const ids = (0, board_1.sortedPlayerIds)(this.model);
99
+ this.notify(ctx, ids.length ? `Players (${ids.length}): ${ids.join(', ')}` : 'No players in the ensemble.');
100
+ }
101
+ async cmdTail(args, ctx) {
102
+ const target = args.trim();
103
+ if (!target || target === 'off') {
104
+ (0, board_1.selectPlayer)(this.model, null);
105
+ this.onTailRequest?.(null);
106
+ this.notify(ctx, 'Inner-loop tail off.');
107
+ return;
108
+ }
109
+ // H3a: the /inner tail is daemon-local — refuse a cross-host tail with an
110
+ // actionable message rather than silently selecting a player that would only
111
+ // ever show "(no inner-loop activity yet)". Decided BEFORE select/open so a
112
+ // cross-host player is never selected and no tail SSE is opened.
113
+ const t = (0, board_1.tailability)(this.model, target, this.localHost);
114
+ if (!t.ok) {
115
+ let msg;
116
+ if (t.reason === 'cross-host') {
117
+ msg = `Inner tail unavailable: ${target} runs on host ${t.playerHost}, not this daemon's host (${this.localHost}). Cross-host tail is a tracked follow-up (#645 / H3b).`;
118
+ }
119
+ else if (t.reason === 'ui-player') {
120
+ msg = `${target} is a UI player — nothing to tail.`;
121
+ }
122
+ else {
123
+ msg = `No such player: ${target}`;
124
+ }
125
+ this.notify(ctx, msg);
126
+ return;
127
+ }
128
+ (0, board_1.selectPlayer)(this.model, target);
129
+ this.onTailRequest?.(target);
130
+ this.notify(ctx, `Tailing ${target}.`);
131
+ }
132
+ async cmdCue(args, ctx) {
133
+ const [to, message] = Controller.splitFirst(args);
134
+ if (!to || !message) {
135
+ this.notify(ctx, 'Usage: /cue <player> <message>');
136
+ return;
137
+ }
138
+ this.report(ctx, `cue → ${to}`, await this.actions.cue(to, message));
139
+ }
140
+ async cmdPause(_args, ctx) {
141
+ this.report(ctx, 'pause', await this.actions.pause());
142
+ }
143
+ async cmdPlay(args, ctx) {
144
+ const release = args.trim() === 'release';
145
+ this.report(ctx, 'play', await this.actions.play(release));
146
+ }
147
+ async cmdRestart(args, ctx) {
148
+ const [p, reason] = Controller.splitFirst(args);
149
+ if (!p) {
150
+ this.notify(ctx, 'Usage: /restart <player> [reason]');
151
+ return;
152
+ }
153
+ this.report(ctx, `restart ${p}`, await this.actions.restart(p, reason || undefined));
154
+ }
155
+ async cmdDestroy(args, ctx) {
156
+ const [p, reason] = Controller.splitFirst(args);
157
+ if (!p) {
158
+ this.notify(ctx, 'Usage: /destroy <player> [reason]');
159
+ return;
160
+ }
161
+ this.report(ctx, `destroy ${p}`, await this.actions.destroy(p, reason || undefined));
162
+ }
163
+ async cmdReset(args, ctx) {
164
+ const p = args.trim();
165
+ if (!p) {
166
+ this.notify(ctx, 'Usage: /reset <player>');
167
+ return;
168
+ }
169
+ // D14 reset has NO daemon HTTP route yet (MCP/outbox only). Surface clearly
170
+ // rather than silently fail. Wiring a POST /v1/ensembles/:e/reset is a daemon
171
+ // follow-up (flagged to the conductor).
172
+ this.notify(ctx, `reset ${p}: not available over the daemon HTTP surface yet (MCP/outbox only). Flagged for a daemon route.`);
173
+ }
174
+ async cmdArm(args, ctx) {
175
+ const [p, mode] = Controller.splitFirst(args);
176
+ if (!p) {
177
+ this.notify(ctx, 'Usage: /arm <player> [off]');
178
+ return;
179
+ }
180
+ const off = mode.trim() === 'off';
181
+ this.report(ctx, `${off ? 'disarm' : 'arm'} ${p}`, off ? await this.actions.gateDisarm(p) : await this.actions.gateArm(p));
182
+ }
183
+ async cmdGate(args, ctx) {
184
+ const [reqId, decisionRaw] = Controller.splitFirst(args);
185
+ const decision = decisionRaw.trim();
186
+ if (!reqId || (decision !== 'allow' && decision !== 'deny')) {
187
+ this.notify(ctx, 'Usage: /gate <requestId> allow|deny (decides for the tailed player)');
188
+ return;
189
+ }
190
+ if (!this.model.selected) {
191
+ this.notify(ctx, 'Select a player first with /tail <player> — gate decisions are per-player.');
192
+ return;
193
+ }
194
+ this.report(ctx, `gate ${reqId} ${decision}`, await this.actions.gateDecide(this.model.selected, reqId, decision));
195
+ }
196
+ }
197
+ exports.Controller = Controller;
198
+ function resolveBaseUrl(override) {
199
+ if (override)
200
+ return override.replace(/\/$/, '');
201
+ return `http://127.0.0.1:${(0, port_file_1.readPortFile)() ?? DEFAULT_PORT}`;
202
+ }
203
+ const log = (...args) => {
204
+ // eslint-disable-next-line no-console
205
+ console.error('[agent-tempo:pi:mission-control]', ...args);
206
+ };
207
+ /**
208
+ * Build the mission-control extension (default-export shape). The operator's Pi
209
+ * loads it in OBSERVER mode. `deps` overrides config/token/baseUrl for tests.
210
+ */
211
+ function createMissionControlExtension(deps = {}) {
212
+ return (pi) => {
213
+ const ensemble = deps.ensemble ?? (0, config_1.getConfig)().ensemble;
214
+ const adminToken = deps.adminToken ?? process.env[actions_1.ADMIN_TOKEN_ENV];
215
+ const throttleMs = deps.renderThrottleMs ?? DEFAULT_RENDER_THROTTLE_MS;
216
+ // H3a: mission-control is co-located with its 127.0.0.1 daemon, so this
217
+ // process's hostname IS the daemon's host. (HealthV1.hostname is the noted
218
+ // future upgrade for a baseUrl pointing at a remote daemon — not built here.)
219
+ const localHost = deps.localHost ?? os.hostname();
220
+ // H5: do NOT pre-resolve baseUrl. createSubscribe + MissionControlActions both
221
+ // re-read ~/.agent-tempo/daemon.port per-call when baseUrl is undefined, so a
222
+ // daemon restart on a new port self-heals (a once-resolved URL wedges the
223
+ // board). A `deps.baseUrl` override (tests / future remote daemon) still wins.
224
+ const actions = new actions_1.MissionControlActions({
225
+ ensemble,
226
+ ...(adminToken ? { adminToken } : {}),
227
+ ...(deps.baseUrl ? { baseUrl: deps.baseUrl } : {}),
228
+ });
229
+ const ctrl = new Controller(ensemble, actions, localHost);
230
+ // Per-session lifecycle state (re-created on each session_start).
231
+ let coarseAbort = null;
232
+ let tailAbort = null;
233
+ let renderTimer = null;
234
+ let lastRenderedRevision = -1;
235
+ let activeCtx = null;
236
+ const renderNow = () => {
237
+ if (!activeCtx?.hasUI)
238
+ return;
239
+ if (ctrl.model.revision === lastRenderedRevision)
240
+ return; // throttle: skip no-op ticks
241
+ lastRenderedRevision = ctrl.model.revision;
242
+ activeCtx.ui.setWidget(WIDGET_KEY, (0, render_1.renderBoard)(ctrl.model, ctrl.localHost), { placement: 'aboveEditor' });
243
+ };
244
+ const startCoarse = () => {
245
+ if (!adminToken) {
246
+ log(`no admin token (${actions_1.ADMIN_TOKEN_ENV}) — board limited / disabled`);
247
+ }
248
+ // H5: capture the controller locally. teardown nulls the outer `coarseAbort`,
249
+ // so the catch must check THIS signal — checking the nulled outer ref made an
250
+ // expected teardown abort log a spurious "coarse SSE ended: AbortError".
251
+ const ac = new AbortController();
252
+ coarseAbort = ac;
253
+ // H5: omit baseUrl → createSubscribe re-resolves the daemon port per
254
+ // (re)connect, so a daemon restart on a new port self-heals.
255
+ const subscribe = (0, subscribe_1.createSubscribe)({
256
+ ...(deps.baseUrl ? { baseUrl: deps.baseUrl } : {}),
257
+ ...(adminToken ? { token: adminToken } : {}),
258
+ });
259
+ void (async () => {
260
+ try {
261
+ for await (const ev of subscribe(ensemble, { signal: ac.signal })) {
262
+ (0, board_1.applyTempoEvent)(ctrl.model, ev);
263
+ }
264
+ }
265
+ catch (err) {
266
+ if (!ac.signal.aborted)
267
+ log('coarse SSE ended:', err instanceof Error ? err.message : err);
268
+ }
269
+ })();
270
+ };
271
+ const openTail = (playerId) => {
272
+ tailAbort?.abort();
273
+ tailAbort = null;
274
+ if (playerId === null || !adminToken)
275
+ return;
276
+ tailAbort = new AbortController();
277
+ // H5: resolve the daemon base URL HERE (per /tail) so a port change is
278
+ // picked up on the next tail instead of being pinned at session start.
279
+ const baseUrl = resolveBaseUrl(deps.baseUrl);
280
+ const fetchFn = globalThis.fetch;
281
+ if (!fetchFn)
282
+ return;
283
+ void (0, inner_tail_1.openInnerTail)({
284
+ baseUrl, adminToken, ensemble, playerId,
285
+ signal: tailAbort.signal,
286
+ fetchFn: fetchFn,
287
+ onFrame: (f) => (0, board_1.applyInnerFrame)(ctrl.model, f),
288
+ onError: (m) => log(`inner tail (${playerId}):`, m),
289
+ });
290
+ };
291
+ ctrl.onTailRequest = openTail;
292
+ const teardown = () => {
293
+ coarseAbort?.abort();
294
+ coarseAbort = null;
295
+ tailAbort?.abort();
296
+ tailAbort = null;
297
+ if (renderTimer) {
298
+ clearInterval(renderTimer);
299
+ renderTimer = null;
300
+ }
301
+ if (activeCtx?.hasUI)
302
+ activeCtx.ui.setWidget(WIDGET_KEY, undefined);
303
+ activeCtx = null;
304
+ };
305
+ pi.on('session_start', (_event, ctx) => {
306
+ activeCtx = ctx;
307
+ lastRenderedRevision = -1;
308
+ startCoarse();
309
+ renderTimer = setInterval(renderNow, throttleMs);
310
+ if (typeof renderTimer.unref === 'function')
311
+ renderTimer.unref();
312
+ renderNow();
313
+ });
314
+ pi.on('session_shutdown', () => teardown());
315
+ // Operator commands (display-only widget → slash-commands drive everything).
316
+ pi.registerCommand('players', { description: 'List ensemble players', handler: (_a, ctx) => ctrl.cmdPlayers(ctx) });
317
+ pi.registerCommand('tail', { description: 'Tail a player\'s inner loop (/tail <player> | off)', handler: (a, ctx) => ctrl.cmdTail(a, ctx) });
318
+ pi.registerCommand('cue', { description: 'Send a message to a player (/cue <player> <msg>)', handler: (a, ctx) => ctrl.cmdCue(a, ctx) });
319
+ pi.registerCommand('pause', { description: 'Pause the ensemble', handler: (a, ctx) => ctrl.cmdPause(a, ctx) });
320
+ pi.registerCommand('play', { description: 'Resume the ensemble (/play [release])', handler: (a, ctx) => ctrl.cmdPlay(a, ctx) });
321
+ pi.registerCommand('restart', { description: 'Restart a player (/restart <player> [reason])', handler: (a, ctx) => ctrl.cmdRestart(a, ctx) });
322
+ pi.registerCommand('destroy', { description: 'Destroy a player (/destroy <player> [reason])', handler: (a, ctx) => ctrl.cmdDestroy(a, ctx) });
323
+ pi.registerCommand('reset', { description: 'Clean-wipe a player (/reset <player>)', handler: (a, ctx) => ctrl.cmdReset(a, ctx) });
324
+ pi.registerCommand('arm', { description: 'Arm/disarm the operator gate for a player (/arm <player> [off])', handler: (a, ctx) => ctrl.cmdArm(a, ctx) });
325
+ pi.registerCommand('gate', { description: 'Decide a gate request for the tailed player (/gate <reqId> allow|deny)', handler: (a, ctx) => ctrl.cmdGate(a, ctx) });
326
+ };
327
+ }
328
+ /** Default export — the loadable Pi extension. */
329
+ const missionControlExtension = createMissionControlExtension();
330
+ exports.default = missionControlExtension;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Mission-control (3f) — a Pi extension that turns one interactive Pi TUI into
3
+ * an ensemble mission-control board + operator controller. Observer-only,
4
+ * HTTP-driven, throttled render. See ./extension.ts.
5
+ *
6
+ * The default export is the loadable Pi extension.
7
+ */
8
+ export { default, createMissionControlExtension, Controller } from './extension';
9
+ export type { MissionControlDeps } from './extension';
10
+ export { initBoard, applyTempoEvent, applyInnerFrame, selectPlayer, sortedPlayerIds, DEFAULT_TAIL_LIMIT, } from './board';
11
+ export type { BoardModel, PlayerRow } from './board';
12
+ export { renderBoard } from './render';
13
+ export { MissionControlActions, ADMIN_TOKEN_ENV } from './actions';
14
+ export type { ActionResult } from './actions';
15
+ export { parseInnerSse, openInnerTail } from './inner-tail';
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.openInnerTail = exports.parseInnerSse = exports.ADMIN_TOKEN_ENV = exports.MissionControlActions = exports.renderBoard = exports.DEFAULT_TAIL_LIMIT = exports.sortedPlayerIds = exports.selectPlayer = exports.applyInnerFrame = exports.applyTempoEvent = exports.initBoard = exports.Controller = exports.createMissionControlExtension = exports.default = void 0;
7
+ /**
8
+ * Mission-control (3f) — a Pi extension that turns one interactive Pi TUI into
9
+ * an ensemble mission-control board + operator controller. Observer-only,
10
+ * HTTP-driven, throttled render. See ./extension.ts.
11
+ *
12
+ * The default export is the loadable Pi extension.
13
+ */
14
+ var extension_1 = require("./extension");
15
+ Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(extension_1).default; } });
16
+ Object.defineProperty(exports, "createMissionControlExtension", { enumerable: true, get: function () { return extension_1.createMissionControlExtension; } });
17
+ Object.defineProperty(exports, "Controller", { enumerable: true, get: function () { return extension_1.Controller; } });
18
+ var board_1 = require("./board");
19
+ Object.defineProperty(exports, "initBoard", { enumerable: true, get: function () { return board_1.initBoard; } });
20
+ Object.defineProperty(exports, "applyTempoEvent", { enumerable: true, get: function () { return board_1.applyTempoEvent; } });
21
+ Object.defineProperty(exports, "applyInnerFrame", { enumerable: true, get: function () { return board_1.applyInnerFrame; } });
22
+ Object.defineProperty(exports, "selectPlayer", { enumerable: true, get: function () { return board_1.selectPlayer; } });
23
+ Object.defineProperty(exports, "sortedPlayerIds", { enumerable: true, get: function () { return board_1.sortedPlayerIds; } });
24
+ Object.defineProperty(exports, "DEFAULT_TAIL_LIMIT", { enumerable: true, get: function () { return board_1.DEFAULT_TAIL_LIMIT; } });
25
+ var render_1 = require("./render");
26
+ Object.defineProperty(exports, "renderBoard", { enumerable: true, get: function () { return render_1.renderBoard; } });
27
+ var actions_1 = require("./actions");
28
+ Object.defineProperty(exports, "MissionControlActions", { enumerable: true, get: function () { return actions_1.MissionControlActions; } });
29
+ Object.defineProperty(exports, "ADMIN_TOKEN_ENV", { enumerable: true, get: function () { return actions_1.ADMIN_TOKEN_ENV; } });
30
+ var inner_tail_1 = require("./inner-tail");
31
+ Object.defineProperty(exports, "parseInnerSse", { enumerable: true, get: function () { return inner_tail_1.parseInnerSse; } });
32
+ Object.defineProperty(exports, "openInnerTail", { enumerable: true, get: function () { return inner_tail_1.openInnerTail; } });