agent-tempo 1.7.0 → 2.0.0-beta.2

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 (171) hide show
  1. package/CLAUDE.md +10 -13
  2. package/README.md +21 -15
  3. package/dashboard/dist/assets/index-BbR7zIdK.js.map +1 -1
  4. package/dashboard/package.json +1 -1
  5. package/dist/activities/hard-terminate.d.ts +2 -2
  6. package/dist/activities/hard-terminate.js +3 -9
  7. package/dist/activities/maestro.d.ts +8 -13
  8. package/dist/activities/maestro.js +3 -10
  9. package/dist/activities/outbox.js +16 -2
  10. package/dist/activities/resolve.d.ts +18 -7
  11. package/dist/activities/resolve.js +58 -46
  12. package/dist/adapters/base.js +3 -0
  13. package/dist/adapters/claude-code/adapter.js +7 -0
  14. package/dist/cli/command-center-command.js +15 -1
  15. package/dist/cli/commands.d.ts +8 -0
  16. package/dist/cli/commands.js +79 -11
  17. package/dist/cli/daemon-command.js +19 -33
  18. package/dist/cli/help-text.js +9 -7
  19. package/dist/cli/home-command.d.ts +26 -0
  20. package/dist/cli/home-command.js +156 -0
  21. package/dist/cli/removed-verbs.d.ts +0 -4
  22. package/dist/cli/removed-verbs.js +23 -12
  23. package/dist/cli/sa-preflight.d.ts +0 -14
  24. package/dist/cli/sa-preflight.js +1 -17
  25. package/dist/cli/startup.d.ts +1 -10
  26. package/dist/cli/startup.js +0 -40
  27. package/dist/cli.js +34 -80
  28. package/dist/client/core.js +13 -15
  29. package/dist/client/index.d.ts +2 -1
  30. package/dist/client/interface.d.ts +5 -14
  31. package/dist/client/with-spawn.d.ts +9 -4
  32. package/dist/client/with-spawn.js +7 -10
  33. package/dist/config.d.ts +14 -13
  34. package/dist/config.js +14 -0
  35. package/dist/constants.d.ts +50 -0
  36. package/dist/constants.js +60 -1
  37. package/dist/daemon.js +81 -0
  38. package/dist/http/auth.d.ts +3 -21
  39. package/dist/http/auth.js +10 -31
  40. package/dist/http/event-types.d.ts +38 -4
  41. package/dist/http/orphans.d.ts +3 -3
  42. package/dist/http/orphans.js +3 -3
  43. package/dist/http/server.d.ts +0 -5
  44. package/dist/http/server.js +79 -29
  45. package/dist/http/snapshot.js +3 -0
  46. package/dist/http/writes.d.ts +1 -1
  47. package/dist/http/writes.js +25 -2
  48. package/dist/observability/nondeterminism-alarm.d.ts +113 -0
  49. package/dist/observability/nondeterminism-alarm.js +162 -0
  50. package/dist/palette/index.d.ts +6 -7
  51. package/dist/palette/index.js +6 -7
  52. package/dist/pi/mission-control/actions.d.ts +67 -1
  53. package/dist/pi/mission-control/actions.js +61 -0
  54. package/dist/pi/mission-control/extension.d.ts +77 -0
  55. package/dist/pi/mission-control/extension.js +171 -0
  56. package/dist/pi/workflow-client.d.ts +14 -16
  57. package/dist/pi/workflow-client.js +18 -44
  58. package/dist/server-tools.js +30 -29
  59. package/dist/server.js +42 -0
  60. package/dist/tools/action-guard.d.ts +23 -0
  61. package/dist/tools/action-guard.js +33 -0
  62. package/dist/tools/coat-check.d.ts +20 -0
  63. package/dist/tools/coat-check.js +129 -0
  64. package/dist/tools/gate.d.ts +13 -0
  65. package/dist/tools/gate.js +88 -0
  66. package/dist/tools/schedule.d.ts +18 -0
  67. package/dist/tools/schedule.js +93 -1
  68. package/dist/tools/stage.d.ts +17 -0
  69. package/dist/tools/stage.js +85 -1
  70. package/dist/tools/state.d.ts +14 -0
  71. package/dist/tools/state.js +95 -0
  72. package/dist/types.d.ts +54 -7
  73. package/dist/upgrade/boot-guard.d.ts +95 -0
  74. package/dist/upgrade/boot-guard.js +88 -0
  75. package/dist/upgrade/from-upgrade.d.ts +95 -0
  76. package/dist/upgrade/from-upgrade.js +330 -0
  77. package/dist/upgrade/phase-engine.js +4 -1
  78. package/dist/utils/duration.d.ts +3 -6
  79. package/dist/utils/duration.js +3 -6
  80. package/dist/utils/format-hosts.d.ts +1 -1
  81. package/dist/utils/format-hosts.js +0 -2
  82. package/dist/utils/grpc-shutdown-guard.js +0 -4
  83. package/dist/utils/hosts.d.ts +4 -10
  84. package/dist/utils/hosts.js +12 -28
  85. package/dist/utils/orphan-guard.d.ts +37 -0
  86. package/dist/utils/orphan-guard.js +26 -0
  87. package/dist/utils/search-attributes.d.ts +28 -18
  88. package/dist/utils/search-attributes.js +48 -18
  89. package/dist/utils/visibility-deadline.d.ts +16 -0
  90. package/dist/utils/visibility-deadline.js +16 -0
  91. package/dist/workflows/maestro-signals.d.ts +0 -13
  92. package/dist/workflows/maestro-signals.js +6 -14
  93. package/dist/workflows/maestro.js +27 -32
  94. package/dist/workflows/scheduler.js +7 -1
  95. package/dist/workflows/session.js +237 -153
  96. package/dist/workflows/signals.d.ts +9 -1
  97. package/dist/workflows/signals.js +8 -6
  98. package/package.json +1 -6
  99. package/workflow-bundle.js +478 -225
  100. package/dist/cli/legacy-migration.d.ts +0 -35
  101. package/dist/cli/legacy-migration.js +0 -341
  102. package/dist/client/ensure-conductor-spawned.d.ts +0 -35
  103. package/dist/client/ensure-conductor-spawned.js +0 -48
  104. package/dist/tui/App.d.ts +0 -85
  105. package/dist/tui/App.js +0 -1791
  106. package/dist/tui/bootstrap-types.d.ts +0 -46
  107. package/dist/tui/bootstrap-types.js +0 -7
  108. package/dist/tui/commands.d.ts +0 -71
  109. package/dist/tui/commands.js +0 -1375
  110. package/dist/tui/components/ChatView.d.ts +0 -35
  111. package/dist/tui/components/ChatView.js +0 -54
  112. package/dist/tui/components/CommandPalette.d.ts +0 -21
  113. package/dist/tui/components/CommandPalette.js +0 -67
  114. package/dist/tui/components/ConversationStream.d.ts +0 -114
  115. package/dist/tui/components/ConversationStream.js +0 -307
  116. package/dist/tui/components/CreateEnsembleWizard.d.ts +0 -19
  117. package/dist/tui/components/CreateEnsembleWizard.js +0 -223
  118. package/dist/tui/components/DestroyConfirmModal.d.ts +0 -17
  119. package/dist/tui/components/DestroyConfirmModal.js +0 -62
  120. package/dist/tui/components/ErrorView.d.ts +0 -31
  121. package/dist/tui/components/ErrorView.js +0 -129
  122. package/dist/tui/components/HomeView.d.ts +0 -54
  123. package/dist/tui/components/HomeView.js +0 -306
  124. package/dist/tui/components/LoadLineupModal.d.ts +0 -18
  125. package/dist/tui/components/LoadLineupModal.js +0 -79
  126. package/dist/tui/components/MainView.d.ts +0 -21
  127. package/dist/tui/components/MainView.js +0 -107
  128. package/dist/tui/components/NewEnsembleModal.d.ts +0 -9
  129. package/dist/tui/components/NewEnsembleModal.js +0 -73
  130. package/dist/tui/components/Picker.d.ts +0 -23
  131. package/dist/tui/components/Picker.js +0 -70
  132. package/dist/tui/components/PlayerDetailView.d.ts +0 -26
  133. package/dist/tui/components/PlayerDetailView.js +0 -118
  134. package/dist/tui/components/PromptArea.d.ts +0 -50
  135. package/dist/tui/components/PromptArea.js +0 -303
  136. package/dist/tui/components/RecruitWizard.d.ts +0 -17
  137. package/dist/tui/components/RecruitWizard.js +0 -221
  138. package/dist/tui/components/RestoreConfirmModal.d.ts +0 -18
  139. package/dist/tui/components/RestoreConfirmModal.js +0 -71
  140. package/dist/tui/components/ScheduleWizard.d.ts +0 -19
  141. package/dist/tui/components/ScheduleWizard.js +0 -259
  142. package/dist/tui/components/Splash.d.ts +0 -23
  143. package/dist/tui/components/Splash.js +0 -221
  144. package/dist/tui/components/StatusBar.d.ts +0 -48
  145. package/dist/tui/components/StatusBar.js +0 -128
  146. package/dist/tui/components/StatusOverlay.d.ts +0 -15
  147. package/dist/tui/components/StatusOverlay.js +0 -76
  148. package/dist/tui/components/TitleBar.d.ts +0 -10
  149. package/dist/tui/components/TitleBar.js +0 -21
  150. package/dist/tui/index.d.ts +0 -15
  151. package/dist/tui/index.js +0 -162
  152. package/dist/tui/ink-context.d.ts +0 -18
  153. package/dist/tui/ink-context.js +0 -59
  154. package/dist/tui/ink-loader.d.ts +0 -26
  155. package/dist/tui/ink-loader.js +0 -42
  156. package/dist/tui/removed-commands.d.ts +0 -9
  157. package/dist/tui/removed-commands.js +0 -22
  158. package/dist/tui/sse-handler.d.ts +0 -52
  159. package/dist/tui/sse-handler.js +0 -157
  160. package/dist/tui/store.d.ts +0 -598
  161. package/dist/tui/store.js +0 -753
  162. package/dist/tui/utils/format.d.ts +0 -56
  163. package/dist/tui/utils/format.js +0 -155
  164. package/dist/tui/utils/fullscreen.d.ts +0 -23
  165. package/dist/tui/utils/fullscreen.js +0 -71
  166. package/dist/tui/utils/history.d.ts +0 -10
  167. package/dist/tui/utils/history.js +0 -85
  168. package/dist/tui/utils/platform.d.ts +0 -45
  169. package/dist/tui/utils/platform.js +0 -258
  170. package/dist/tui/utils/theme.d.ts +0 -21
  171. package/dist/tui/utils/theme.js +0 -24
@@ -1,4 +1,4 @@
1
- import type { AnswerEntry } from '../../types';
1
+ import type { AnswerEntry, HostInfo, AttachmentInfo, ScheduleEntry, Message, SentMessage } from '../../types';
2
2
  import type { EnsembleSummary } from '../../client/interface';
3
3
  /** Env var holding the daemon admin (T3) token (writes + inner tail). */
4
4
  export declare const ADMIN_TOKEN_ENV = "AGENT_TEMPO_HTTP_ADMIN_TOKEN";
@@ -104,9 +104,75 @@ export declare class MissionControlActions {
104
104
  ok: false;
105
105
  error: string;
106
106
  }>;
107
+ /**
108
+ * List connected hosts (`GET /v1/hosts`, Tier-1 read) — the cluster host
109
+ * registry + freshness + capability profile, the same payload the dashboard
110
+ * `/hosts` view and the CLI `agent-tempo hosts` consume. NOT ensemble-scoped
111
+ * (the host registry is cluster-global), so it ignores the bound ensemble.
112
+ */
113
+ listHosts(): Promise<{
114
+ ok: true;
115
+ hosts: HostInfo[];
116
+ } | {
117
+ ok: false;
118
+ error: string;
119
+ }>;
120
+ /**
121
+ * A player's V2 attachment-lifecycle snapshot (`GET /v1/ensembles/:e/attachment-info`,
122
+ * Tier-1 read). Thin shim over `client.attachmentInfo` — the same payload the
123
+ * MCP `attachment_info` tool and CLI `attachment-info` render via the shared
124
+ * `formatAttachmentInfoForDisplay`. The player is a `?playerId=` query param.
125
+ */
126
+ attachmentInfo(playerId: string): Promise<{
127
+ ok: true;
128
+ info: AttachmentInfo;
129
+ } | {
130
+ ok: false;
131
+ error: string;
132
+ }>;
133
+ /**
134
+ * A player's full message timeline (received + sent). Hits the existing
135
+ * `POST /v1/ensembles/:e/recall` route with `{ full: true }` so the daemon
136
+ * returns the raw `{ received, sent }` arrays (the default count-only shape
137
+ * stays for the dashboard). Thin shim over `client.recall`; the caller feeds
138
+ * the result through the shared `buildTimeline`/`formatRecall` helpers. POST
139
+ * (not GET) because recall groups with the per-player action surface (T2).
140
+ */
141
+ recall(playerId: string): Promise<{
142
+ ok: true;
143
+ received: Message[];
144
+ sent: SentMessage[];
145
+ } | {
146
+ ok: false;
147
+ error: string;
148
+ }>;
149
+ /**
150
+ * Active schedules for the bound ensemble (`GET /v1/ensembles/:e/schedules`,
151
+ * Tier-1 read). Thin shim over `client.getSchedules` — the same `ScheduleEntry[]`
152
+ * the MCP `schedules` tool and CLI render. Empty when no scheduler is running.
153
+ */
154
+ listSchedules(): Promise<{
155
+ ok: true;
156
+ schedules: ScheduleEntry[];
157
+ } | {
158
+ ok: false;
159
+ error: string;
160
+ }>;
107
161
  cue(to: string, message: string): Promise<ActionResult>;
108
162
  pause(): Promise<ActionResult>;
109
163
  play(release?: boolean): Promise<ActionResult>;
164
+ /**
165
+ * Free HELD players via the dedicated `POST /v1/ensembles/:e/release` route
166
+ * (#742 gap 8 — `/go`). Optional `playerId` releases ONE held player; omitted
167
+ * releases all held in the ensemble. Distinct from `play(release:true)`, which
168
+ * ALSO clears the PAUSE axis — `/go` frees holds without touching pause.
169
+ */
170
+ release(playerId?: string): Promise<ActionResult>;
171
+ /**
172
+ * Cancel a named schedule (`POST /v1/ensembles/:e/unschedule`, #742 gap —
173
+ * `/unschedule` & `/schedule delete`). Thin shim over `client.cancelSchedule`.
174
+ */
175
+ unschedule(name: string): Promise<ActionResult>;
110
176
  restart(playerId: string, reason?: string): Promise<ActionResult>;
111
177
  destroy(playerId: string, reason?: string): Promise<ActionResult>;
112
178
  reset(playerId: string, reason?: string): Promise<ActionResult>;
@@ -211,6 +211,51 @@ class MissionControlActions {
211
211
  const res = await this.getJson('/v1/ensembles');
212
212
  return res.ok ? { ok: true, ensembles: res.data } : res;
213
213
  }
214
+ // ── Read surface: hosts (#742 gap 5) ──
215
+ /**
216
+ * List connected hosts (`GET /v1/hosts`, Tier-1 read) — the cluster host
217
+ * registry + freshness + capability profile, the same payload the dashboard
218
+ * `/hosts` view and the CLI `agent-tempo hosts` consume. NOT ensemble-scoped
219
+ * (the host registry is cluster-global), so it ignores the bound ensemble.
220
+ */
221
+ async listHosts() {
222
+ const res = await this.getJson('/v1/hosts');
223
+ return res.ok ? { ok: true, hosts: res.data } : res;
224
+ }
225
+ // ── Read surface: attachment-info (#742 gap — /attachment-info) ──
226
+ /**
227
+ * A player's V2 attachment-lifecycle snapshot (`GET /v1/ensembles/:e/attachment-info`,
228
+ * Tier-1 read). Thin shim over `client.attachmentInfo` — the same payload the
229
+ * MCP `attachment_info` tool and CLI `attachment-info` render via the shared
230
+ * `formatAttachmentInfoForDisplay`. The player is a `?playerId=` query param.
231
+ */
232
+ async attachmentInfo(playerId) {
233
+ const res = await this.getJson(`/v1/ensembles/${this.ens()}/attachment-info?playerId=${encodeURIComponent(playerId)}`);
234
+ return res.ok ? { ok: true, info: res.data } : res;
235
+ }
236
+ // ── Read surface: recall (#742 gap — /recall) ──
237
+ /**
238
+ * A player's full message timeline (received + sent). Hits the existing
239
+ * `POST /v1/ensembles/:e/recall` route with `{ full: true }` so the daemon
240
+ * returns the raw `{ received, sent }` arrays (the default count-only shape
241
+ * stays for the dashboard). Thin shim over `client.recall`; the caller feeds
242
+ * the result through the shared `buildTimeline`/`formatRecall` helpers. POST
243
+ * (not GET) because recall groups with the per-player action surface (T2).
244
+ */
245
+ async recall(playerId) {
246
+ const res = await this.postJson(`/v1/ensembles/${this.ens()}/recall`, { playerId, full: true });
247
+ return res.ok ? { ok: true, received: res.data.received, sent: res.data.sent } : res;
248
+ }
249
+ // ── Read surface: schedules (#742 gap — /schedule list) ──
250
+ /**
251
+ * Active schedules for the bound ensemble (`GET /v1/ensembles/:e/schedules`,
252
+ * Tier-1 read). Thin shim over `client.getSchedules` — the same `ScheduleEntry[]`
253
+ * the MCP `schedules` tool and CLI render. Empty when no scheduler is running.
254
+ */
255
+ async listSchedules() {
256
+ const res = await this.getJson(`/v1/ensembles/${this.ens()}/schedules`);
257
+ return res.ok ? { ok: true, schedules: res.data } : res;
258
+ }
214
259
  // ── Ensemble write surface (T2) ──
215
260
  cue(to, message) {
216
261
  // #822 — parse the deliverability hint so the board warns on a detached/gone
@@ -223,6 +268,22 @@ class MissionControlActions {
223
268
  play(release) {
224
269
  return this.post(`/v1/ensembles/${this.ens()}/play`, release ? { release } : {});
225
270
  }
271
+ /**
272
+ * Free HELD players via the dedicated `POST /v1/ensembles/:e/release` route
273
+ * (#742 gap 8 — `/go`). Optional `playerId` releases ONE held player; omitted
274
+ * releases all held in the ensemble. Distinct from `play(release:true)`, which
275
+ * ALSO clears the PAUSE axis — `/go` frees holds without touching pause.
276
+ */
277
+ release(playerId) {
278
+ return this.post(`/v1/ensembles/${this.ens()}/release`, playerId ? { playerId } : {});
279
+ }
280
+ /**
281
+ * Cancel a named schedule (`POST /v1/ensembles/:e/unschedule`, #742 gap —
282
+ * `/unschedule` & `/schedule delete`). Thin shim over `client.cancelSchedule`.
283
+ */
284
+ unschedule(name) {
285
+ return this.post(`/v1/ensembles/${this.ens()}/unschedule`, { name });
286
+ }
226
287
  restart(playerId, reason) {
227
288
  return this.post(`/v1/ensembles/${this.ens()}/restart`, { playerId, ...(reason ? { reason } : {}) });
228
289
  }
@@ -194,6 +194,83 @@ export declare class Controller {
194
194
  /** First whitespace-delimited token + the remainder. */
195
195
  private static splitFirst;
196
196
  cmdPlayers(ctx: McExtensionContext): Promise<void>;
197
+ /**
198
+ * Connected-host listing — reuses the shared `formatHostList` formatter (the
199
+ * same text the CLI `hosts` and MCP `hosts` tool render). `includeStale`
200
+ * surfaces hosts past their liveness window (`/hosts --all`). Returns the
201
+ * text so a future planner tool can reuse it (same shape as
202
+ * {@link ensemblesText}). NOT ensemble-scoped — the host registry is global.
203
+ */
204
+ hostsText(includeStale: boolean): Promise<{
205
+ ok: true;
206
+ text: string;
207
+ } | {
208
+ ok: false;
209
+ error: string;
210
+ }>;
211
+ cmdHosts(args: string, ctx: McExtensionContext): Promise<void>;
212
+ /**
213
+ * Full board detail — the human counterpart to the `observe_board` planner
214
+ * tool. Renders the live `BoardModel` (already kept current by the coarse SSE
215
+ * stream), so there's no extra fetch: the board IS the snapshot. Richer than
216
+ * the TUI `/status` overlay (phase glyphs + currentTool + context% + tail).
217
+ */
218
+ cmdStatus(ctx: McExtensionContext): void;
219
+ /**
220
+ * `/go [player]` — free HELD players. Uses the dedicated `release` route so
221
+ * it frees holds WITHOUT clearing the PAUSE axis (that's `/play` / `/unpause`).
222
+ * No arg → release all held in the ensemble; `/go <player>` releases one.
223
+ */
224
+ cmdGo(args: string, ctx: McExtensionContext): Promise<void>;
225
+ /**
226
+ * A player's attachment-lifecycle snapshot — reuses the shared
227
+ * `formatAttachmentInfoForDisplay` (the same text the CLI `attachment-info`
228
+ * and MCP `attachment_info` tool render). Returns the text so a future planner
229
+ * tool can reuse it (same shape as {@link hostsText}).
230
+ */
231
+ attachmentInfoText(playerId: string): Promise<{
232
+ ok: true;
233
+ text: string;
234
+ } | {
235
+ ok: false;
236
+ error: string;
237
+ }>;
238
+ cmdAttachmentInfo(args: string, ctx: McExtensionContext): Promise<void>;
239
+ /**
240
+ * A player's message timeline — reuses the shared `buildTimeline`/`formatRecall`
241
+ * helpers (the same render the CLI `recall` and MCP `recall` tool produce).
242
+ * Includes sent messages (operator wants the full picture). Pulls the full
243
+ * timeline via the `{ full: true }` recall route, NOT the count-only default.
244
+ */
245
+ recallText(playerId: string): Promise<{
246
+ ok: true;
247
+ text: string;
248
+ } | {
249
+ ok: false;
250
+ error: string;
251
+ }>;
252
+ cmdRecall(args: string, ctx: McExtensionContext): Promise<void>;
253
+ /**
254
+ * Active-schedule listing — renders each {@link ScheduleEntry} in the same
255
+ * shape the MCP `schedules` tool produces (name → target | recurrence | next).
256
+ */
257
+ schedulesText(): Promise<{
258
+ ok: true;
259
+ text: string;
260
+ } | {
261
+ ok: false;
262
+ error: string;
263
+ }>;
264
+ /**
265
+ * `/schedule [list | delete <name>]` — list active schedules (default) or
266
+ * cancel one. CREATE is intentionally NOT here: there is no thin-shim create
267
+ * method on the client (the MCP `schedule` tool signals the scheduler workflow
268
+ * directly), so create stays on the MCP tool / CLI per the architect's
269
+ * thin-shim ruling.
270
+ */
271
+ cmdSchedule(args: string, ctx: McExtensionContext): Promise<void>;
272
+ /** `/unschedule <name>` — alias for `/schedule delete <name>`. */
273
+ cmdUnschedule(args: string, ctx: McExtensionContext): Promise<void>;
197
274
  /**
198
275
  * One-line ensembles listing — `▶` marks the board's current binding. Shared
199
276
  * by the `/ensembles` command and the `list_ensembles` planner tool. Returns
@@ -74,6 +74,9 @@ const inner_tail_1 = require("./inner-tail");
74
74
  const ensure_infra_1 = require("../../cli/ensure-infra");
75
75
  const zod_to_typebox_1 = require("../zod-to-typebox");
76
76
  const validation_1 = require("../../utils/validation");
77
+ const format_hosts_1 = require("../../utils/format-hosts");
78
+ const attachment_format_1 = require("../../utils/attachment-format");
79
+ const recall_format_1 = require("../../utils/recall-format");
77
80
  /** The durable conductor a `/handoff` targets by default (matches catalog's conductorName default). */
78
81
  const DEFAULT_CONDUCTOR = 'conductor';
79
82
  /**
@@ -96,6 +99,34 @@ const ASK_POLL_INTERVAL_MS = 1_000;
96
99
  function mintQuestionId() {
97
100
  return `q-${crypto.randomUUID()}`;
98
101
  }
102
+ /** Compact recurrence label for a schedule (ms interval → "5m"/"1h"/…). */
103
+ function formatScheduleInterval(ms) {
104
+ if (ms >= 86_400_000)
105
+ return `${ms / 86_400_000}d`;
106
+ if (ms >= 3_600_000)
107
+ return `${ms / 3_600_000}h`;
108
+ if (ms >= 60_000)
109
+ return `${ms / 60_000}m`;
110
+ return `${ms / 1000}s`;
111
+ }
112
+ /**
113
+ * #742 — render one {@link ScheduleEntry} for the board, mirroring the MCP
114
+ * `schedules` tool line shape (name → target | recurrence (bounds) | next + msg
115
+ * preview). Kept module-local + pure so the controller test can assert it.
116
+ */
117
+ function formatScheduleLine(s) {
118
+ const recur = s.cronExpression
119
+ ? `cron: ${s.cronExpression} (${s.timezone || 'UTC'})`
120
+ : s.interval ? `every ${formatScheduleInterval(s.interval)}` : 'one-shot';
121
+ const bounds = [];
122
+ if (s.until)
123
+ bounds.push(`until ${s.until}`);
124
+ if (s.remainingCount != null)
125
+ bounds.push(`${s.firedCount}/${s.firedCount + s.remainingCount} fired`);
126
+ const boundsStr = bounds.length ? ` (${bounds.join(', ')})` : '';
127
+ const msgPreview = s.message.length > 60 ? s.message.slice(0, 57) + '...' : s.message;
128
+ return `• ${s.name} → ${s.target} | ${recur}${boundsStr} | next: ${s.nextFireAt}\n msg: ${msgPreview}`;
129
+ }
99
130
  /**
100
131
  * #700 P2 — build the planner-wake injection from a resolved `answer` SSE event.
101
132
  * Pure (testable). The planner has no inbox; the SSE `answer` event is turned
@@ -344,6 +375,139 @@ class Controller {
344
375
  const ids = (0, board_1.sortedPlayerIds)(this.model);
345
376
  this.notify(ctx, ids.length ? `Players (${ids.length}): ${ids.join(', ')}` : 'No players in the ensemble.');
346
377
  }
378
+ // ── Hosts (#742 gap 5) ──
379
+ /**
380
+ * Connected-host listing — reuses the shared `formatHostList` formatter (the
381
+ * same text the CLI `hosts` and MCP `hosts` tool render). `includeStale`
382
+ * surfaces hosts past their liveness window (`/hosts --all`). Returns the
383
+ * text so a future planner tool can reuse it (same shape as
384
+ * {@link ensemblesText}). NOT ensemble-scoped — the host registry is global.
385
+ */
386
+ async hostsText(includeStale) {
387
+ const r = await this.actions.listHosts();
388
+ if (!r.ok)
389
+ return r;
390
+ return { ok: true, text: (0, format_hosts_1.formatHostList)(r.hosts, { includeStale }) };
391
+ }
392
+ async cmdHosts(args, ctx) {
393
+ const includeStale = /(?:^|\s)(?:--all|-a)(?:\s|$)/.test(args);
394
+ const r = await this.hostsText(includeStale);
395
+ this.notify(ctx, r.ok ? r.text : `hosts failed: ${r.error}`);
396
+ }
397
+ // ── Status (#742 gap 9) ──
398
+ /**
399
+ * Full board detail — the human counterpart to the `observe_board` planner
400
+ * tool. Renders the live `BoardModel` (already kept current by the coarse SSE
401
+ * stream), so there's no extra fetch: the board IS the snapshot. Richer than
402
+ * the TUI `/status` overlay (phase glyphs + currentTool + context% + tail).
403
+ */
404
+ cmdStatus(ctx) {
405
+ this.notify(ctx, (0, render_1.renderBoard)(this.model, this.localHost).join('\n'));
406
+ }
407
+ // ── Release / go (#742 gap 8) ──
408
+ /**
409
+ * `/go [player]` — free HELD players. Uses the dedicated `release` route so
410
+ * it frees holds WITHOUT clearing the PAUSE axis (that's `/play` / `/unpause`).
411
+ * No arg → release all held in the ensemble; `/go <player>` releases one.
412
+ */
413
+ async cmdGo(args, ctx) {
414
+ const player = args.trim().split(/\s+/).filter(Boolean)[0];
415
+ const label = player ? `go (release ${player})` : 'go (release held)';
416
+ this.report(ctx, label, await this.actions.release(player || undefined));
417
+ }
418
+ // ── Attachment-info (#742 — /attachment-info) ──
419
+ /**
420
+ * A player's attachment-lifecycle snapshot — reuses the shared
421
+ * `formatAttachmentInfoForDisplay` (the same text the CLI `attachment-info`
422
+ * and MCP `attachment_info` tool render). Returns the text so a future planner
423
+ * tool can reuse it (same shape as {@link hostsText}).
424
+ */
425
+ async attachmentInfoText(playerId) {
426
+ const r = await this.actions.attachmentInfo(playerId);
427
+ if (!r.ok)
428
+ return r;
429
+ return { ok: true, text: (0, attachment_format_1.formatAttachmentInfoForDisplay)(playerId, r.info).join('\n') };
430
+ }
431
+ async cmdAttachmentInfo(args, ctx) {
432
+ const player = args.trim().split(/\s+/).filter(Boolean)[0];
433
+ if (!player) {
434
+ this.notify(ctx, 'Usage: /attachment-info <player>');
435
+ return;
436
+ }
437
+ const r = await this.attachmentInfoText(player);
438
+ this.notify(ctx, r.ok ? r.text : `attachment-info failed: ${r.error}`);
439
+ }
440
+ // ── Recall (#742 — /recall) ──
441
+ /**
442
+ * A player's message timeline — reuses the shared `buildTimeline`/`formatRecall`
443
+ * helpers (the same render the CLI `recall` and MCP `recall` tool produce).
444
+ * Includes sent messages (operator wants the full picture). Pulls the full
445
+ * timeline via the `{ full: true }` recall route, NOT the count-only default.
446
+ */
447
+ async recallText(playerId) {
448
+ const r = await this.actions.recall(playerId);
449
+ if (!r.ok)
450
+ return r;
451
+ const timeline = (0, recall_format_1.buildTimeline)(r.received, r.sent, true);
452
+ return { ok: true, text: (0, recall_format_1.formatRecall)(timeline).text };
453
+ }
454
+ async cmdRecall(args, ctx) {
455
+ const player = args.trim().split(/\s+/).filter(Boolean)[0];
456
+ if (!player) {
457
+ this.notify(ctx, 'Usage: /recall <player>');
458
+ return;
459
+ }
460
+ const r = await this.recallText(player);
461
+ this.notify(ctx, r.ok ? r.text : `recall failed: ${r.error}`);
462
+ }
463
+ // ── Schedules (#742 — /schedule, /unschedule) ──
464
+ /**
465
+ * Active-schedule listing — renders each {@link ScheduleEntry} in the same
466
+ * shape the MCP `schedules` tool produces (name → target | recurrence | next).
467
+ */
468
+ async schedulesText() {
469
+ const r = await this.actions.listSchedules();
470
+ if (!r.ok)
471
+ return r;
472
+ if (r.schedules.length === 0)
473
+ return { ok: true, text: 'No active schedules.' };
474
+ return { ok: true, text: r.schedules.map(formatScheduleLine).join('\n') };
475
+ }
476
+ /**
477
+ * `/schedule [list | delete <name>]` — list active schedules (default) or
478
+ * cancel one. CREATE is intentionally NOT here: there is no thin-shim create
479
+ * method on the client (the MCP `schedule` tool signals the scheduler workflow
480
+ * directly), so create stays on the MCP tool / CLI per the architect's
481
+ * thin-shim ruling.
482
+ */
483
+ async cmdSchedule(args, ctx) {
484
+ const [sub, rest] = Controller.splitFirst(args);
485
+ if (sub === 'delete' || sub === 'rm') {
486
+ const name = rest.trim().split(/\s+/).filter(Boolean)[0];
487
+ if (!name) {
488
+ this.notify(ctx, 'Usage: /schedule delete <name>');
489
+ return;
490
+ }
491
+ this.report(ctx, `unschedule ${name}`, await this.actions.unschedule(name));
492
+ return;
493
+ }
494
+ if (sub === 'create' || sub === 'new' || sub === 'add') {
495
+ this.notify(ctx, 'Schedule creation runs through the MCP `schedule` tool / `agent-tempo schedule` CLI — the board lists and deletes only.');
496
+ return;
497
+ }
498
+ // Default (no arg or `list`) → list.
499
+ const r = await this.schedulesText();
500
+ this.notify(ctx, r.ok ? r.text : `schedules failed: ${r.error}`);
501
+ }
502
+ /** `/unschedule <name>` — alias for `/schedule delete <name>`. */
503
+ async cmdUnschedule(args, ctx) {
504
+ const name = args.trim().split(/\s+/).filter(Boolean)[0];
505
+ if (!name) {
506
+ this.notify(ctx, 'Usage: /unschedule <name>');
507
+ return;
508
+ }
509
+ this.report(ctx, `unschedule ${name}`, await this.actions.unschedule(name));
510
+ }
347
511
  // ── Multi-ensemble home (#790) ──
348
512
  /**
349
513
  * One-line ensembles listing — `▶` marks the board's current binding. Shared
@@ -1086,6 +1250,13 @@ function createMissionControlExtension(deps = {}) {
1086
1250
  pi.on('session_shutdown', () => teardown());
1087
1251
  // Operator commands (display-only widget → slash-commands drive everything).
1088
1252
  pi.registerCommand('players', { description: 'List ensemble players', handler: (_a, ctx) => ctrl.cmdPlayers(ctx) });
1253
+ pi.registerCommand('hosts', { description: 'List connected hosts (/hosts [--all])', handler: (a, ctx) => ctrl.cmdHosts(a, ctx) });
1254
+ pi.registerCommand('status', { description: 'Show the full board detail', handler: (_a, ctx) => ctrl.cmdStatus(ctx) });
1255
+ pi.registerCommand('go', { description: 'Release HELD players (/go [player])', handler: (a, ctx) => ctrl.cmdGo(a, ctx) });
1256
+ pi.registerCommand('attachment-info', { description: 'Show a player\'s attachment lifecycle (/attachment-info <player>)', handler: (a, ctx) => ctrl.cmdAttachmentInfo(a, ctx) });
1257
+ pi.registerCommand('recall', { description: 'Show a player\'s message timeline (/recall <player>)', handler: (a, ctx) => ctrl.cmdRecall(a, ctx) });
1258
+ pi.registerCommand('schedule', { description: 'List or delete schedules (/schedule [list | delete <name>])', handler: (a, ctx) => ctrl.cmdSchedule(a, ctx) });
1259
+ pi.registerCommand('unschedule', { description: 'Cancel a schedule (/unschedule <name>)', handler: (a, ctx) => ctrl.cmdUnschedule(a, ctx) });
1089
1260
  // #790 — multi-ensemble home: list + re-bind.
1090
1261
  pi.registerCommand('ensembles', { description: 'List all ensembles (▶ marks the bound one)', handler: (_a, ctx) => ctrl.cmdEnsembles(ctx) });
1091
1262
  pi.registerCommand('ensemble', { description: 'Switch the board to another ensemble (/ensemble <name> [--force])', handler: (a, ctx) => ctrl.cmdEnsemble(a, ctx) });
@@ -153,26 +153,24 @@ export declare class PiWorkflowClient {
153
153
  fetchPending(): Promise<Message[]>;
154
154
  /** Acknowledge delivered cue ids so the workflow stops re-serving them. */
155
155
  ackDelivered(messageIds: string[]): Promise<void>;
156
- /**
157
- * `true` until a `pendingIntake` query is rejected as unregistered — i.e.
158
- * the session workflow predates #750. Cached so the pump pays the failed
159
- * probe ONCE, not every tick, then sticks to the legacy two-query path for
160
- * the lifetime of this client. Known cost-only staleness: the Pi client
161
- * handle follows continueAsNew transparently, so a pre-#750 workflow that
162
- * CANs onto a #750+ worker bundle MID-SESSION keeps paying 2 queries/tick
163
- * here until the extension process restarts (fresh client → fresh probe).
164
- * Benign and self-healing — correctness is unaffected either way.
165
- */
166
- private combinedIntakeSupported;
167
156
  /**
168
157
  * One-query intake: undelivered cues + the pending reset together —
169
- * halves the pump's idle Temporal actions (2 → 1 query/tick). Falls back
170
- * to the legacy `pendingMessages` + `pendingReset` pair when the workflow
171
- * lacks the combined handler (pre-#750 build); genuine failures (timeouts,
172
- * connection errors) propagate to the pump's existing per-tick handling.
158
+ * halves the pump's idle Temporal actions (2 → 1 query/tick).
159
+ *
160
+ * 2.0 (#788): the pre-#750 two-query fallback (`combinedIntakeSupported`
161
+ * probe + the legacy `pendingMessages` + `pendingReset` pair) is gone — the
162
+ * A2 clean cutover (guarded by #786) guarantees every 2.0 session workflow
163
+ * serves the `pendingIntake` handler, so the fallback was dead.
173
164
  */
174
165
  fetchIntake(): Promise<PendingIntake>;
175
- /** Poll the workflow's single-slot pending reset (null = none). */
166
+ /**
167
+ * Poll the workflow's single-slot pending reset (null = none).
168
+ *
169
+ * 2.0 (#788): the standalone `pendingReset` query was removed — derive the
170
+ * reset from the combined `pendingIntake` query. Only the cue-pump's
171
+ * no-`intakeSource` fallback path reaches this; the primary pump path reads
172
+ * `pendingReset` straight off `fetchIntake`'s result.
173
+ */
176
174
  fetchPendingReset(): Promise<PendingReset | null>;
177
175
  /**
178
176
  * Ack a performed reset by id — the workflow clears the slot ONLY if the id
@@ -23,6 +23,7 @@ const action_counters_1 = require("../utils/action-counters");
23
23
  const search_attributes_1 = require("../utils/search-attributes");
24
24
  const config_1 = require("../config");
25
25
  const signals_1 = require("../workflows/signals");
26
+ const constants_1 = require("../constants");
26
27
  /** Adapter identity advertised on claim. Pi interactive players are `interactive`-class. */
27
28
  const PI_ADAPTER_ID = 'pi';
28
29
  /**
@@ -160,6 +161,7 @@ class PiWorkflowClient {
160
161
  adapterId: PI_ADAPTER_ID,
161
162
  adapterClass: 'interactive',
162
163
  leaseMs: this.leaseMs,
164
+ protocolVersion: constants_1.PROTOCOL_VERSION, // #786 — 2.0 wire handshake
163
165
  ...(this.expectedAttachmentId ? { expectedAttachmentId: this.expectedAttachmentId } : {}),
164
166
  },
165
167
  ],
@@ -286,57 +288,29 @@ class PiWorkflowClient {
286
288
  await handle.signal(signals_1.markDeliveredSignal, messageIds);
287
289
  }
288
290
  // ── Combined intake (T0.3 of #747, #750) ──
289
- /**
290
- * `true` until a `pendingIntake` query is rejected as unregistered — i.e.
291
- * the session workflow predates #750. Cached so the pump pays the failed
292
- * probe ONCE, not every tick, then sticks to the legacy two-query path for
293
- * the lifetime of this client. Known cost-only staleness: the Pi client
294
- * handle follows continueAsNew transparently, so a pre-#750 workflow that
295
- * CANs onto a #750+ worker bundle MID-SESSION keeps paying 2 queries/tick
296
- * here until the extension process restarts (fresh client → fresh probe).
297
- * Benign and self-healing — correctness is unaffected either way.
298
- */
299
- combinedIntakeSupported = true;
300
291
  /**
301
292
  * One-query intake: undelivered cues + the pending reset together —
302
- * halves the pump's idle Temporal actions (2 → 1 query/tick). Falls back
303
- * to the legacy `pendingMessages` + `pendingReset` pair when the workflow
304
- * lacks the combined handler (pre-#750 build); genuine failures (timeouts,
305
- * connection errors) propagate to the pump's existing per-tick handling.
293
+ * halves the pump's idle Temporal actions (2 → 1 query/tick).
294
+ *
295
+ * 2.0 (#788): the pre-#750 two-query fallback (`combinedIntakeSupported`
296
+ * probe + the legacy `pendingMessages` + `pendingReset` pair) is gone — the
297
+ * A2 clean cutover (guarded by #786) guarantees every 2.0 session workflow
298
+ * serves the `pendingIntake` handler, so the fallback was dead.
306
299
  */
307
300
  async fetchIntake() {
308
- if (this.combinedIntakeSupported) {
309
- const handle = this.requireHandle();
310
- try {
311
- const intake = await handle.query(signals_1.pendingIntakeQuery);
312
- // Shape-guard: a real server either serves the query or rejects it
313
- // as unregistered, but belt-and-braces against converter quirks /
314
- // test doubles — a malformed result downgrades to the legacy pair
315
- // rather than crashing the pump tick.
316
- if (intake && Array.isArray(intake.messages))
317
- return intake;
318
- this.combinedIntakeSupported = false;
319
- log('pendingIntake returned a malformed result — falling back to the legacy pair');
320
- }
321
- catch (err) {
322
- if (!isQueryNotRegisteredError(err))
323
- throw err;
324
- this.combinedIntakeSupported = false;
325
- log('pendingIntake query not registered on this workflow (pre-#750 build) — ' +
326
- 'falling back to the legacy pendingMessages + pendingReset pair');
327
- }
328
- }
329
- const [messages, pendingReset] = await Promise.all([
330
- this.fetchPending(),
331
- this.fetchPendingReset(),
332
- ]);
333
- return { messages, pendingReset };
301
+ return this.requireHandle().query(signals_1.pendingIntakeQuery);
334
302
  }
335
303
  // ── Reset intake (3d D14) ──
336
- /** Poll the workflow's single-slot pending reset (null = none). */
304
+ /**
305
+ * Poll the workflow's single-slot pending reset (null = none).
306
+ *
307
+ * 2.0 (#788): the standalone `pendingReset` query was removed — derive the
308
+ * reset from the combined `pendingIntake` query. Only the cue-pump's
309
+ * no-`intakeSource` fallback path reaches this; the primary pump path reads
310
+ * `pendingReset` straight off `fetchIntake`'s result.
311
+ */
337
312
  async fetchPendingReset() {
338
- const handle = this.requireHandle();
339
- return handle.query(signals_1.pendingResetQuery);
313
+ return (await this.requireHandle().query(signals_1.pendingIntakeQuery)).pendingReset;
340
314
  }
341
315
  /**
342
316
  * Ack a performed reset by id — the workflow clears the slot ONLY if the id