agent-tempo 1.7.0 → 2.0.0-beta.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 (147) 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 +0 -10
  9. package/dist/activities/outbox.js +4 -2
  10. package/dist/adapters/base.js +3 -0
  11. package/dist/cli/commands.d.ts +8 -0
  12. package/dist/cli/commands.js +79 -11
  13. package/dist/cli/daemon-command.js +19 -33
  14. package/dist/cli/help-text.js +7 -7
  15. package/dist/cli/home-command.d.ts +26 -0
  16. package/dist/cli/home-command.js +156 -0
  17. package/dist/cli/removed-verbs.d.ts +0 -4
  18. package/dist/cli/removed-verbs.js +23 -12
  19. package/dist/cli/sa-preflight.d.ts +0 -14
  20. package/dist/cli/sa-preflight.js +1 -17
  21. package/dist/cli/startup.d.ts +1 -10
  22. package/dist/cli/startup.js +0 -40
  23. package/dist/cli.js +34 -80
  24. package/dist/client/core.js +13 -15
  25. package/dist/client/index.d.ts +2 -1
  26. package/dist/client/interface.d.ts +5 -14
  27. package/dist/client/with-spawn.d.ts +9 -4
  28. package/dist/client/with-spawn.js +7 -10
  29. package/dist/config.d.ts +0 -13
  30. package/dist/constants.d.ts +22 -0
  31. package/dist/constants.js +23 -1
  32. package/dist/daemon.js +52 -0
  33. package/dist/http/auth.d.ts +3 -21
  34. package/dist/http/auth.js +10 -31
  35. package/dist/http/event-types.d.ts +5 -4
  36. package/dist/http/orphans.d.ts +3 -3
  37. package/dist/http/orphans.js +3 -3
  38. package/dist/http/server.d.ts +0 -5
  39. package/dist/http/server.js +69 -29
  40. package/dist/http/writes.d.ts +1 -1
  41. package/dist/http/writes.js +25 -2
  42. package/dist/palette/index.d.ts +6 -7
  43. package/dist/palette/index.js +6 -7
  44. package/dist/pi/mission-control/actions.d.ts +67 -1
  45. package/dist/pi/mission-control/actions.js +61 -0
  46. package/dist/pi/mission-control/extension.d.ts +77 -0
  47. package/dist/pi/mission-control/extension.js +171 -0
  48. package/dist/pi/workflow-client.d.ts +14 -16
  49. package/dist/pi/workflow-client.js +18 -44
  50. package/dist/types.d.ts +15 -6
  51. package/dist/upgrade/boot-guard.d.ts +95 -0
  52. package/dist/upgrade/boot-guard.js +88 -0
  53. package/dist/upgrade/from-upgrade.d.ts +95 -0
  54. package/dist/upgrade/from-upgrade.js +330 -0
  55. package/dist/upgrade/phase-engine.js +4 -1
  56. package/dist/utils/duration.d.ts +3 -6
  57. package/dist/utils/duration.js +3 -6
  58. package/dist/utils/format-hosts.d.ts +1 -1
  59. package/dist/utils/format-hosts.js +0 -2
  60. package/dist/utils/grpc-shutdown-guard.js +0 -4
  61. package/dist/utils/hosts.d.ts +4 -10
  62. package/dist/utils/hosts.js +12 -28
  63. package/dist/utils/search-attributes.d.ts +27 -18
  64. package/dist/utils/search-attributes.js +39 -18
  65. package/dist/utils/visibility-deadline.d.ts +16 -0
  66. package/dist/utils/visibility-deadline.js +16 -0
  67. package/dist/workflows/maestro-signals.d.ts +0 -13
  68. package/dist/workflows/maestro-signals.js +6 -14
  69. package/dist/workflows/maestro.js +27 -32
  70. package/dist/workflows/scheduler.js +7 -1
  71. package/dist/workflows/session.js +44 -110
  72. package/dist/workflows/signals.d.ts +9 -1
  73. package/dist/workflows/signals.js +8 -6
  74. package/package.json +1 -6
  75. package/workflow-bundle.js +239 -182
  76. package/dist/cli/legacy-migration.d.ts +0 -35
  77. package/dist/cli/legacy-migration.js +0 -341
  78. package/dist/client/ensure-conductor-spawned.d.ts +0 -35
  79. package/dist/client/ensure-conductor-spawned.js +0 -48
  80. package/dist/tui/App.d.ts +0 -85
  81. package/dist/tui/App.js +0 -1791
  82. package/dist/tui/bootstrap-types.d.ts +0 -46
  83. package/dist/tui/bootstrap-types.js +0 -7
  84. package/dist/tui/commands.d.ts +0 -71
  85. package/dist/tui/commands.js +0 -1375
  86. package/dist/tui/components/ChatView.d.ts +0 -35
  87. package/dist/tui/components/ChatView.js +0 -54
  88. package/dist/tui/components/CommandPalette.d.ts +0 -21
  89. package/dist/tui/components/CommandPalette.js +0 -67
  90. package/dist/tui/components/ConversationStream.d.ts +0 -114
  91. package/dist/tui/components/ConversationStream.js +0 -307
  92. package/dist/tui/components/CreateEnsembleWizard.d.ts +0 -19
  93. package/dist/tui/components/CreateEnsembleWizard.js +0 -223
  94. package/dist/tui/components/DestroyConfirmModal.d.ts +0 -17
  95. package/dist/tui/components/DestroyConfirmModal.js +0 -62
  96. package/dist/tui/components/ErrorView.d.ts +0 -31
  97. package/dist/tui/components/ErrorView.js +0 -129
  98. package/dist/tui/components/HomeView.d.ts +0 -54
  99. package/dist/tui/components/HomeView.js +0 -306
  100. package/dist/tui/components/LoadLineupModal.d.ts +0 -18
  101. package/dist/tui/components/LoadLineupModal.js +0 -79
  102. package/dist/tui/components/MainView.d.ts +0 -21
  103. package/dist/tui/components/MainView.js +0 -107
  104. package/dist/tui/components/NewEnsembleModal.d.ts +0 -9
  105. package/dist/tui/components/NewEnsembleModal.js +0 -73
  106. package/dist/tui/components/Picker.d.ts +0 -23
  107. package/dist/tui/components/Picker.js +0 -70
  108. package/dist/tui/components/PlayerDetailView.d.ts +0 -26
  109. package/dist/tui/components/PlayerDetailView.js +0 -118
  110. package/dist/tui/components/PromptArea.d.ts +0 -50
  111. package/dist/tui/components/PromptArea.js +0 -303
  112. package/dist/tui/components/RecruitWizard.d.ts +0 -17
  113. package/dist/tui/components/RecruitWizard.js +0 -221
  114. package/dist/tui/components/RestoreConfirmModal.d.ts +0 -18
  115. package/dist/tui/components/RestoreConfirmModal.js +0 -71
  116. package/dist/tui/components/ScheduleWizard.d.ts +0 -19
  117. package/dist/tui/components/ScheduleWizard.js +0 -259
  118. package/dist/tui/components/Splash.d.ts +0 -23
  119. package/dist/tui/components/Splash.js +0 -221
  120. package/dist/tui/components/StatusBar.d.ts +0 -48
  121. package/dist/tui/components/StatusBar.js +0 -128
  122. package/dist/tui/components/StatusOverlay.d.ts +0 -15
  123. package/dist/tui/components/StatusOverlay.js +0 -76
  124. package/dist/tui/components/TitleBar.d.ts +0 -10
  125. package/dist/tui/components/TitleBar.js +0 -21
  126. package/dist/tui/index.d.ts +0 -15
  127. package/dist/tui/index.js +0 -162
  128. package/dist/tui/ink-context.d.ts +0 -18
  129. package/dist/tui/ink-context.js +0 -59
  130. package/dist/tui/ink-loader.d.ts +0 -26
  131. package/dist/tui/ink-loader.js +0 -42
  132. package/dist/tui/removed-commands.d.ts +0 -9
  133. package/dist/tui/removed-commands.js +0 -22
  134. package/dist/tui/sse-handler.d.ts +0 -52
  135. package/dist/tui/sse-handler.js +0 -157
  136. package/dist/tui/store.d.ts +0 -598
  137. package/dist/tui/store.js +0 -753
  138. package/dist/tui/utils/format.d.ts +0 -56
  139. package/dist/tui/utils/format.js +0 -155
  140. package/dist/tui/utils/fullscreen.d.ts +0 -23
  141. package/dist/tui/utils/fullscreen.js +0 -71
  142. package/dist/tui/utils/history.d.ts +0 -10
  143. package/dist/tui/utils/history.js +0 -85
  144. package/dist/tui/utils/platform.d.ts +0 -45
  145. package/dist/tui/utils/platform.js +0 -258
  146. package/dist/tui/utils/theme.d.ts +0 -21
  147. package/dist/tui/utils/theme.js +0 -24
@@ -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
package/dist/types.d.ts CHANGED
@@ -290,7 +290,7 @@ export interface HostProfile {
290
290
  */
291
291
  export interface InstanceInfo {
292
292
  pid: number;
293
- /** Parsed from worker identity. `'unknown'` for legacy-format identities (`<pid>@<hostname>`). */
293
+ /** Parsed from the `agent-tempo:<hostname>:<pid>:<version>` worker identity. */
294
294
  version: string;
295
295
  /** Raw `poller.identity` as Temporal returned it. */
296
296
  identity: string;
@@ -300,8 +300,6 @@ export interface InstanceInfo {
300
300
  hasActivityWorker: boolean;
301
301
  /** `true` when the per-host activity queue (`agent-tempo-<hostname>`) has a live poller. Controls `HostInfo.recruitReady`. */
302
302
  hasHostQueueWorker: boolean;
303
- /** Set only when identity was in the legacy `<pid>@<hostname>` form. */
304
- legacy?: true;
305
303
  }
306
304
  /**
307
305
  * Joined liveness + capability view returned by `src/utils/hosts.ts`.
@@ -346,6 +344,16 @@ export interface PlayerStateEntry {
346
344
  }
347
345
  export interface SessionInput {
348
346
  metadata: SessionMetadata;
347
+ /**
348
+ * #786 — 2.0 cutover protocol stamp. Set to `PROTOCOL_VERSION` (2) at every
349
+ * 2.0 start site so it's in history from run 1 and survives `continueAsNew`
350
+ * (carried by the `...input` spread at the CAN site). The workflow ALSO upserts
351
+ * the `AgentTempoProtocol` memo authoritatively from the constant — a 2.0
352
+ * bundle always stamps 2 regardless of input — so a 1.x run (which never sets
353
+ * this) is detectable by the boot guard. Optional only for type back-compat
354
+ * across the cutover; 2.0 always sets it.
355
+ */
356
+ protocol?: number;
349
357
  /** Restored from continue-as-new */
350
358
  part?: string;
351
359
  /** Restored from continue-as-new (undelivered only) */
@@ -728,9 +736,10 @@ export interface ResetOutboxEntry extends OutboxEntryBase {
728
736
  }
729
737
  export type OutboxEntry = CueOutboxEntry | RecruitOutboxEntry | ReportOutboxEntry | StopOutboxEntry | ReleaseOutboxEntry | SpawnOutboxEntry | DetachOutboxEntry | DestroyOutboxEntry | RestartOutboxEntry | ResetOutboxEntry;
730
738
  /**
731
- * Pending reset flag set on a session workflow by `deliverReset`, polled by the
732
- * Pi extension via `pendingResetQuery` and cleared via `ackResetSignal(resetId)`
733
- * after the extension performs the wipe. Single-slot, latest-wins.
739
+ * Pending reset flag set on a session workflow by `deliverReset`, read by the
740
+ * Pi extension off the combined `pendingIntake` query and cleared via
741
+ * `ackResetSignal(resetId)` after the extension performs the wipe. Single-slot,
742
+ * latest-wins.
734
743
  */
735
744
  export interface PendingReset {
736
745
  /** Correlation id (the originating outbox entry id). Ack clears only on match. */
@@ -0,0 +1,95 @@
1
+ /**
2
+ * #786 — the 2.0 cutover BOOT GUARD (the keystone's centerpiece).
3
+ *
4
+ * A 2.0 Temporal worker can never deterministically replay a 1.x-recorded
5
+ * workflow history — the bundle's command stream has diverged, so a replay
6
+ * faults (or, worse, silently corrupts) deep inside a workflow task. The
7
+ * `upgrade-to-2` cutover (#785) exists to destroy every 1.x run before a 2.0
8
+ * daemon ever polls. This guard is the enforcement: a 2.0 daemon REFUSES to
9
+ * register workers if visibility still shows ANY Running agent-tempo workflow
10
+ * that lacks the protocol-2 stamp.
11
+ *
12
+ * **Stamp source of truth:** every 2.0 workflow upserts the
13
+ * `AgentTempoProtocol` memo = {@link PROTOCOL_VERSION} early + unconditionally
14
+ * on every run (incl. continueAsNew successors) — see `src/workflows/*` and
15
+ * `src/constants.ts`. The memo rides the visibility list result directly
16
+ * ({@link getWorkflowProtocol} reads it off the row), so the scan needs NO
17
+ * per-workflow query. A 1.x run never executes that upsert, so its memo is
18
+ * `undefined` → un-stamped → the guard refuses.
19
+ *
20
+ * **FAIL-CLOSED (the #845 partial-scan lesson):** the scan is bounded by the
21
+ * `protocolGuardBoot` visibility budget. A timeout or any scan error is NOT a
22
+ * best-effort partial — it means "couldn't prove the cutover is clean," and an
23
+ * unverified scan must NEVER be read as clean. Both refuse with
24
+ * {@link ProtocolGuardResult.reason} = `'scan-incomplete'`; the daemon retries
25
+ * on the next boot. The cost of a false-refuse (one retry) is trivial next to
26
+ * the cost of a false-proceed (deterministic history corruption).
27
+ *
28
+ * This module is daemon-side (it holds a Temporal client), distinct from the
29
+ * deliberately SDK-free `snapshot-v1.ts` cross-release contract.
30
+ */
31
+ import type { Client, WorkflowExecutionInfo } from '@temporalio/client';
32
+ /**
33
+ * The four agent-tempo workflow types. A Running workflow of any of these is
34
+ * an agent-tempo coordination workflow that a 2.0 worker would have to replay
35
+ * — so all four must be protocol-2 stamped before boot.
36
+ */
37
+ export declare const AGENT_TEMPO_WORKFLOW_TYPES: readonly ["agentSessionWorkflow", "agentMaestroWorkflow", "agentSchedulerWorkflow", "agentGlobalMaestroWorkflow"];
38
+ /** A Running workflow that failed the protocol-2 stamp check. */
39
+ export interface ProtocolOffender {
40
+ workflowId: string;
41
+ /** Workflow type name from the visibility row (best-effort). */
42
+ workflowType?: string;
43
+ /** The stamp read from the row's memo — `undefined` for an un-stamped 1.x run. */
44
+ protocol: number | undefined;
45
+ }
46
+ /** Outcome of {@link checkProtocolGuard}. */
47
+ export interface ProtocolGuardResult {
48
+ /** `true` → every Running agent-tempo workflow is protocol-2 stamped (or none exist). */
49
+ ok: boolean;
50
+ /**
51
+ * Why boot was refused. Only set when `ok` is `false`.
52
+ * - `'unstamped'` — at least one Running workflow lacks the protocol-2 stamp
53
+ * (a 1.x run survived the cutover). See {@link offenders}.
54
+ * - `'scan-incomplete'` — the visibility scan timed out or errored before
55
+ * completing, so cleanliness could not be PROVEN (fail-closed).
56
+ */
57
+ reason?: 'unstamped' | 'scan-incomplete';
58
+ /** Number of Running agent-tempo workflows examined before the result. */
59
+ scanned: number;
60
+ /** Un-stamped offenders (only when `reason === 'unstamped'`). */
61
+ offenders: ProtocolOffender[];
62
+ /** Operator-facing explanation (always set on refusal). */
63
+ message?: string;
64
+ }
65
+ /** Injectable seams for {@link checkProtocolGuard} — production callers omit. */
66
+ export interface ProtocolGuardDeps {
67
+ /**
68
+ * Returns the visibility iterable for the guard query. Defaults to
69
+ * `client.workflow.list({ query })`. Tests inject a crafted async iterable
70
+ * of `WorkflowExecutionInfo`-shaped rows (only `workflowId`, `type`, `memo`
71
+ * are read) without a live Temporal connection.
72
+ */
73
+ listWorkflows?: (query: string) => AsyncIterable<WorkflowExecutionInfo>;
74
+ /** Scan deadline. Default {@link VISIBILITY_DEADLINES_MS.protocolGuardBoot}. */
75
+ deadlineMs?: number;
76
+ /** Injectable clock — forwarded to {@link iterateWithDeadline}. Default `Date.now`. */
77
+ now?: () => number;
78
+ /** Log sink. Default no-op (the daemon caller logs the structured result). */
79
+ log?: (...args: unknown[]) => void;
80
+ }
81
+ /**
82
+ * Build the namespace-scoped visibility query for the guard. The client is
83
+ * already namespace-scoped, so no namespace clause is needed. Exposed for
84
+ * test introspection.
85
+ */
86
+ export declare function buildProtocolGuardQuery(): string;
87
+ /**
88
+ * Scan every Running agent-tempo workflow and verify each carries the
89
+ * protocol-2 stamp. See the module header for the fail-closed contract.
90
+ *
91
+ * Never throws — every failure (un-stamped run, scan timeout, scan error) is
92
+ * returned as a structured `ok: false` result so the daemon caller decides how
93
+ * to surface it (it prints + `process.exit(1)`).
94
+ */
95
+ export declare function checkProtocolGuard(client: Client, deps?: ProtocolGuardDeps): Promise<ProtocolGuardResult>;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AGENT_TEMPO_WORKFLOW_TYPES = void 0;
4
+ exports.buildProtocolGuardQuery = buildProtocolGuardQuery;
5
+ exports.checkProtocolGuard = checkProtocolGuard;
6
+ const constants_1 = require("../constants");
7
+ const search_attributes_1 = require("../utils/search-attributes");
8
+ const visibility_deadline_1 = require("../utils/visibility-deadline");
9
+ /**
10
+ * The four agent-tempo workflow types. A Running workflow of any of these is
11
+ * an agent-tempo coordination workflow that a 2.0 worker would have to replay
12
+ * — so all four must be protocol-2 stamped before boot.
13
+ */
14
+ exports.AGENT_TEMPO_WORKFLOW_TYPES = [
15
+ 'agentSessionWorkflow',
16
+ 'agentMaestroWorkflow',
17
+ 'agentSchedulerWorkflow',
18
+ 'agentGlobalMaestroWorkflow',
19
+ ];
20
+ /**
21
+ * Build the namespace-scoped visibility query for the guard. The client is
22
+ * already namespace-scoped, so no namespace clause is needed. Exposed for
23
+ * test introspection.
24
+ */
25
+ function buildProtocolGuardQuery() {
26
+ const types = exports.AGENT_TEMPO_WORKFLOW_TYPES.map((t) => `"${t}"`).join(', ');
27
+ return `WorkflowType IN (${types}) AND ExecutionStatus = "Running"`;
28
+ }
29
+ /**
30
+ * Scan every Running agent-tempo workflow and verify each carries the
31
+ * protocol-2 stamp. See the module header for the fail-closed contract.
32
+ *
33
+ * Never throws — every failure (un-stamped run, scan timeout, scan error) is
34
+ * returned as a structured `ok: false` result so the daemon caller decides how
35
+ * to surface it (it prints + `process.exit(1)`).
36
+ */
37
+ async function checkProtocolGuard(client, deps = {}) {
38
+ const log = deps.log ?? (() => { });
39
+ const deadlineMs = deps.deadlineMs ?? visibility_deadline_1.VISIBILITY_DEADLINES_MS.protocolGuardBoot;
40
+ const query = buildProtocolGuardQuery();
41
+ const listWorkflows = deps.listWorkflows ?? ((q) => client.workflow.list({ query: q }));
42
+ const offenders = [];
43
+ let scanned = 0;
44
+ try {
45
+ for await (const wf of (0, visibility_deadline_1.iterateWithDeadline)(listWorkflows(query), deadlineMs, 'protocolGuardBoot', deps.now)) {
46
+ scanned++;
47
+ const protocol = (0, search_attributes_1.getWorkflowProtocol)(wf);
48
+ if (protocol !== constants_1.PROTOCOL_VERSION) {
49
+ offenders.push({
50
+ workflowId: wf.workflowId,
51
+ // `WorkflowExecutionInfo.type` is the workflow type name string.
52
+ workflowType: wf.type,
53
+ protocol,
54
+ });
55
+ }
56
+ }
57
+ }
58
+ catch (err) {
59
+ // FAIL-CLOSED: a timeout OR any other scan error means we could not prove
60
+ // the namespace is clean. Refuse — never proceed on an unverified scan.
61
+ const kind = (0, visibility_deadline_1.isVisibilityTimeout)(err) ? 'timed out' : 'failed';
62
+ const detail = err instanceof Error ? err.message : String(err);
63
+ const message = `agent-tempo 2.0 boot guard could NOT verify a clean cutover — the ` +
64
+ `protocol scan ${kind} after ${scanned} workflow(s): ${detail}. ` +
65
+ `Refusing to boot (fail-closed): a 2.0 worker cannot safely replay a ` +
66
+ `1.x history. Retry once the Temporal namespace is responsive; if this ` +
67
+ `persists, run \`agent-tempo upgrade-to-2\` to complete the cutover.`;
68
+ log(`[boot-guard] scan-incomplete: ${message}`);
69
+ return { ok: false, reason: 'scan-incomplete', scanned, offenders, message };
70
+ }
71
+ if (offenders.length > 0) {
72
+ const sample = offenders
73
+ .slice(0, 5)
74
+ .map((o) => ` - ${o.workflowId} (protocol=${o.protocol ?? 'v1/unset'})`)
75
+ .join('\n');
76
+ const more = offenders.length > 5 ? `\n …and ${offenders.length - 5} more` : '';
77
+ const message = `agent-tempo 2.0 boot guard REFUSED to boot: ${offenders.length} of ` +
78
+ `${scanned} Running agent-tempo workflow(s) are not protocol-${constants_1.PROTOCOL_VERSION} ` +
79
+ `(2.0) stamped — these are pre-cutover 1.x runs that a 2.0 worker cannot ` +
80
+ `safely replay:\n${sample}${more}\n` +
81
+ `Complete the cutover with \`agent-tempo upgrade-to-2\` (it snapshots ` +
82
+ `continuity, then destroys the 1.x runs), then restart the daemon.`;
83
+ log(`[boot-guard] unstamped: refusing (${offenders.length}/${scanned} un-stamped)`);
84
+ return { ok: false, reason: 'unstamped', scanned, offenders, message };
85
+ }
86
+ log(`[boot-guard] clean: ${scanned} Running agent-tempo workflow(s), all protocol-${constants_1.PROTOCOL_VERSION} stamped`);
87
+ return { ok: true, scanned, offenders: [] };
88
+ }