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
@@ -50,44 +50,26 @@ export declare function extractBearerToken(authHeader: string | undefined): stri
50
50
  * reading "constant-time" doesn't assume length is also blinded.
51
51
  */
52
52
  export declare function tokensMatch(received: string, expected: string): boolean;
53
- /**
54
- * @deprecated 3e — superseded by {@link loadRbacTokens} (read + admin split).
55
- * Kept only until every caller migrates; do NOT add new callers. Resolves the
56
- * legacy single `httpToken` (env-less) for back-compat shims.
57
- */
58
- export declare function loadOrGenerateHttpToken(opts: {
59
- bearerRequired: boolean;
60
- load?: () => PersistedConfig;
61
- save?: (cfg: PersistedConfig) => void;
62
- }): string | null;
63
53
  /** Resolved RBAC tokens for the daemon HTTP surface (3e). */
64
54
  export interface RbacTokens {
65
55
  /** T1 read token (env > config.json > auto-gen), or `null` when none + not required. */
66
56
  readToken: string | null;
67
57
  /** T1+T2+T3 admin token — ENV-VAR-ONLY, or `null` when unset (→ 503 on T≥2). */
68
58
  adminToken: string | null;
69
- /**
70
- * True when a LEGACY `httpToken` (no `readToken`) was adopted as the read token.
71
- * The daemon emits a one-time startup warning so the operator sets an admin token.
72
- */
73
- legacyMigrated: boolean;
74
59
  }
75
60
  /** Admin token — ENV-VAR-ONLY (never config.json/disk, never auto-generated). */
76
61
  export declare function loadAdminToken(env?: NodeJS.ProcessEnv): string | null;
77
62
  /**
78
63
  * Read token (T1). Priority: env `AGENT_TEMPO_HTTP_READ_TOKEN` > config.json
79
- * `readToken` > LEGACY config.json `httpToken` (adopted `legacy:true`) >
80
- * auto-generate when bearer mode is required (persisted as `readToken`).
64
+ * `readToken` > auto-generate when bearer mode is required (persisted as
65
+ * `readToken`).
81
66
  */
82
67
  export declare function loadReadToken(opts: {
83
68
  bearerRequired: boolean;
84
69
  env?: NodeJS.ProcessEnv;
85
70
  load?: () => PersistedConfig;
86
71
  save?: (cfg: PersistedConfig) => void;
87
- }): {
88
- token: string | null;
89
- legacy: boolean;
90
- };
72
+ }): string | null;
91
73
  /** Load both RBAC tokens. The daemon calls this once at startup. */
92
74
  export declare function loadRbacTokens(opts: {
93
75
  bearerRequired: boolean;
package/dist/http/auth.js CHANGED
@@ -38,7 +38,6 @@ exports.originHost = originHost;
38
38
  exports.bearerRequired = bearerRequired;
39
39
  exports.extractBearerToken = extractBearerToken;
40
40
  exports.tokensMatch = tokensMatch;
41
- exports.loadOrGenerateHttpToken = loadOrGenerateHttpToken;
42
41
  exports.loadAdminToken = loadAdminToken;
43
42
  exports.loadReadToken = loadReadToken;
44
43
  exports.loadRbacTokens = loadRbacTokens;
@@ -63,9 +62,10 @@ exports.requireTier = requireTier;
63
62
  * by reverse proxies, supervisord, and the TUI bootstrap state machine.
64
63
  *
65
64
  * **Token storage** (§3.1) — `~/.agent-tempo/config.json` field
66
- * `httpToken`. Auto-generated on first daemon boot when bearer mode is
65
+ * `readToken` (T1). Auto-generated on first daemon boot when bearer mode is
67
66
  * required and no token is set: `crypto.randomBytes(32).toString('base64url')`,
68
67
  * 0600 on POSIX. Rotation = delete the field; next daemon boot regenerates.
68
+ * The admin token (T2/T3) is env-var-only (`AGENT_TEMPO_HTTP_ADMIN_TOKEN`).
69
69
  */
70
70
  const crypto = __importStar(require("crypto"));
71
71
  const config_1 = require("../config");
@@ -156,24 +156,6 @@ function tokensMatch(received, expected) {
156
156
  const b = Buffer.from(expected, 'utf8');
157
157
  return crypto.timingSafeEqual(a, b);
158
158
  }
159
- /**
160
- * @deprecated 3e — superseded by {@link loadRbacTokens} (read + admin split).
161
- * Kept only until every caller migrates; do NOT add new callers. Resolves the
162
- * legacy single `httpToken` (env-less) for back-compat shims.
163
- */
164
- function loadOrGenerateHttpToken(opts) {
165
- const load = opts.load ?? config_1.loadConfigFile;
166
- const save = opts.save ?? config_1.saveConfigFile;
167
- const cfg = load();
168
- if (cfg.httpToken && typeof cfg.httpToken === 'string' && cfg.httpToken.length > 0) {
169
- return cfg.httpToken;
170
- }
171
- if (!opts.bearerRequired)
172
- return null;
173
- const token = crypto.randomBytes(32).toString('base64url');
174
- save({ ...cfg, httpToken: token });
175
- return token;
176
- }
177
159
  /** Admin token — ENV-VAR-ONLY (never config.json/disk, never auto-generated). */
178
160
  function loadAdminToken(env = process.env) {
179
161
  const t = env[config_1.ENV.HTTP_ADMIN_TOKEN];
@@ -181,8 +163,8 @@ function loadAdminToken(env = process.env) {
181
163
  }
182
164
  /**
183
165
  * Read token (T1). Priority: env `AGENT_TEMPO_HTTP_READ_TOKEN` > config.json
184
- * `readToken` > LEGACY config.json `httpToken` (adopted `legacy:true`) >
185
- * auto-generate when bearer mode is required (persisted as `readToken`).
166
+ * `readToken` > auto-generate when bearer mode is required (persisted as
167
+ * `readToken`).
186
168
  */
187
169
  function loadReadToken(opts) {
188
170
  const env = opts.env ?? process.env;
@@ -190,24 +172,21 @@ function loadReadToken(opts) {
190
172
  const save = opts.save ?? config_1.saveConfigFile;
191
173
  const envTok = env[config_1.ENV.HTTP_READ_TOKEN];
192
174
  if (envTok && envTok.length > 0)
193
- return { token: envTok, legacy: false };
175
+ return envTok;
194
176
  const cfg = load();
195
177
  if (cfg.readToken && cfg.readToken.length > 0)
196
- return { token: cfg.readToken, legacy: false };
197
- // LEGACY: a pre-3e single `httpToken` becomes the READ token (T1) — NOT admin.
198
- if (cfg.httpToken && cfg.httpToken.length > 0)
199
- return { token: cfg.httpToken, legacy: true };
178
+ return cfg.readToken;
200
179
  if (!opts.bearerRequired)
201
- return { token: null, legacy: false };
180
+ return null;
202
181
  const token = crypto.randomBytes(32).toString('base64url');
203
182
  save({ ...cfg, readToken: token });
204
- return { token, legacy: false };
183
+ return token;
205
184
  }
206
185
  /** Load both RBAC tokens. The daemon calls this once at startup. */
207
186
  function loadRbacTokens(opts) {
208
- const { token: readToken, legacy } = loadReadToken(opts);
187
+ const readToken = loadReadToken(opts);
209
188
  const adminToken = loadAdminToken(opts.env);
210
- return { readToken, adminToken, legacyMigrated: legacy };
189
+ return { readToken, adminToken };
211
190
  }
212
191
  /**
213
192
  * Granted tier for a presented bearer, given the RBAC tokens (timing-safe).
@@ -242,10 +242,11 @@ export type EventIdToken = string;
242
242
  * - `'stale'` — matches a host with `freshness === 'stale'`
243
243
  * - `'missing'` — `preferredHost` is null OR no matching host record
244
244
  *
245
- * `migrateCommand` is the TUI slash-command the operator pastes into their
246
- * own session on the migrate target. `--yes-steal=` (NOT
247
- * `--confirm-steal-from-host`) is the actual flag accepted by
248
- * `src/tui/commands.ts:handleMigrate`. When `preferredHost` is null the
245
+ * `migrateCommand` is the `/migrate` slash-command the operator pastes into
246
+ * their own session on the migrate target (the command-center board; the Ink
247
+ * TUI that originally handled it was removed in #789). `--yes-steal=` (NOT
248
+ * `--confirm-steal-from-host`) is the actual flag the `/migrate` handler
249
+ * accepts. When `preferredHost` is null the
249
250
  * command targets the local host and includes the steal guard pre-filled
250
251
  * with the last-known host (or a literal `'(unknown)'` when even that is
251
252
  * missing — the operator must edit it before submit).
@@ -30,9 +30,9 @@ import type { TempoClient } from '../client/interface';
30
30
  import type { HostInfo } from '../types';
31
31
  import type { OrphanCandidate } from '../reconcile/orphans';
32
32
  /**
33
- * Render the TUI `/migrate` slash command the operator pastes into their
34
- * own session to recover an orphan. Wording mirrors
35
- * `src/tui/commands.ts:handleMigrate` exactly:
33
+ * Render the `/migrate` slash command the operator pastes into their own
34
+ * session to recover an orphan (the command-center board; the Ink TUI that
35
+ * originally handled it was removed in #789). Wording:
36
36
  * - positional `<playerId> <host>`
37
37
  * - flag form `--yes-steal=<currentHost>` (NOT `--confirm-steal-from-host`)
38
38
  *
@@ -6,9 +6,9 @@ exports.buildOrphansResponse = buildOrphansResponse;
6
6
  exports.handleOrphans = handleOrphans;
7
7
  const responses_1 = require("./responses");
8
8
  /**
9
- * Render the TUI `/migrate` slash command the operator pastes into their
10
- * own session to recover an orphan. Wording mirrors
11
- * `src/tui/commands.ts:handleMigrate` exactly:
9
+ * Render the `/migrate` slash command the operator pastes into their own
10
+ * session to recover an orphan (the command-center board; the Ink TUI that
11
+ * originally handled it was removed in #789). Wording:
12
12
  * - positional `<playerId> <host>`
13
13
  * - flag form `--yes-steal=<currentHost>` (NOT `--confirm-steal-from-host`)
14
14
  *
@@ -61,11 +61,6 @@ export interface HttpServerOptions {
61
61
  * don't fight production daemons running on the same machine.
62
62
  */
63
63
  portFilePath?: string;
64
- /**
65
- * @deprecated 3e — back-compat alias for {@link readToken}. A single injected
66
- * bearer is treated as the READ token (T1). Prefer `readToken`/`adminToken`.
67
- */
68
- httpToken?: string;
69
64
  /** 3e — inject the read-tier (T1) token directly (tests). Overrides config/env. */
70
65
  readToken?: string;
71
66
  /** 3e — inject the admin (T1+T2+T3) token directly (tests). Overrides env. */
@@ -103,42 +103,22 @@ async function startHttpServer(opts) {
103
103
  // generation now so the daemon doesn't crash mid-request when the first
104
104
  // bearer-required call shows up.
105
105
  const bindIsLoopback = (0, auth_1.isLoopbackBindAddr)(bindAddr);
106
- // 3e RBAC token resolution. Back-compat: a single `httpToken` option (or a
107
- // legacy config.json `httpToken`) is adopted as the READ token (T1); the ADMIN
108
- // token is env-var-only. Explicit `readToken`/`adminToken` options override
109
- // (used by tests). loopback bind ⇒ no bearer required ⇒ tokens may be null.
110
- let readToken;
111
- let legacyMigrated = false;
112
- if (opts.readToken !== undefined) {
113
- readToken = opts.readToken;
114
- }
115
- else if (opts.httpToken !== undefined) {
116
- // Back-compat: a single injected bearer is treated as the READ token (T1).
117
- readToken = opts.httpToken;
118
- }
119
- else {
120
- const loaded = (0, auth_1.loadReadToken)({ bearerRequired: !bindIsLoopback });
121
- readToken = loaded.token;
122
- legacyMigrated = loaded.legacy;
123
- }
106
+ // 3e RBAC token resolution. The READ token (T1) comes from the explicit
107
+ // `readToken` option (tests) or env/config/auto-gen; the ADMIN token is
108
+ // env-var-only. loopback bind no bearer required ⇒ tokens may be null.
109
+ const readToken = opts.readToken !== undefined
110
+ ? opts.readToken
111
+ : (0, auth_1.loadReadToken)({ bearerRequired: !bindIsLoopback });
124
112
  const adminToken = opts.adminToken ?? (0, auth_1.loadAdminToken)();
125
113
  if (!bindIsLoopback && !readToken) {
126
114
  throw new Error('Bearer token required for non-loopback bind but none configured. ' +
127
115
  'Set AGENT_TEMPO_HTTP_READ_TOKEN (or readToken in ~/.agent-tempo/config.json), ' +
128
116
  'or unset AGENT_TEMPO_HTTP_BIND.');
129
117
  }
130
- // 3e MD-E — one-time startup warnings (non-blocking).
118
+ // 3e MD-E — one-time startup warning (non-blocking).
131
119
  //
132
- // (1) Legacy migration: a pre-3e single `httpToken` was adopted as the READ
133
- // token (T1) and no admin token is configured, so writes / inner-tail
134
- // (all Tier ≥ 2) will 503 until an admin token is set.
135
- if (legacyMigrated && adminToken === null) {
136
- log('NOTICE: adopted legacy config.json `httpToken` as the read-tier token. ' +
137
- 'Writes and the inner-tail are admin-only and will return ' +
138
- '503 until you set AGENT_TEMPO_HTTP_ADMIN_TOKEN (env-var only).');
139
- }
140
- // (2) Plaintext-bearer exposure: binding to a non-loopback address serves the
141
- // bearer token over cleartext HTTP. Suppressible, never blocking.
120
+ // Plaintext-bearer exposure: binding to a non-loopback address serves the
121
+ // bearer token over cleartext HTTP. Suppressible, never blocking.
142
122
  if (!bindIsLoopback && process.env[config_1.ENV.TLS_ACKNOWLEDGED] !== '1') {
143
123
  log(`WARNING: binding to non-loopback ${bindAddr} serves the bearer token over ` +
144
124
  'plaintext HTTP. Terminate TLS at a reverse proxy, or tunnel via SSH/Tailscale. ' +
@@ -433,6 +413,37 @@ async function handle(req, res, ctx) {
433
413
  }
434
414
  return (0, coat_check_1.handleCoatCheckGet)(res, ctx.client, ensemble, ticket);
435
415
  }
416
+ // #742 — per-player attachment-info read (`/attachment-info` board command).
417
+ // 2-segment GET; MUST be matched BEFORE the generic writeMatch below, which
418
+ // would otherwise 404 `attachment-info` as an unknown write action. The
419
+ // player is a `?playerId=` query param (the path stays ensemble-scoped).
420
+ const attachmentInfoMatch = pathname.match(/^\/v1\/ensembles\/([^/]+)\/attachment-info$/);
421
+ if (attachmentInfoMatch) {
422
+ const ensemble = decodeURIComponent(attachmentInfoMatch[1]);
423
+ if (!gateTier(1))
424
+ return; // L3 — read (Tier 1).
425
+ if (method !== 'GET') {
426
+ return (0, responses_1.errorResponse)(res, 405, { error: 'method-not-allowed' }, { Allow: 'GET, OPTIONS' });
427
+ }
428
+ const playerId = url.searchParams.get('playerId');
429
+ if (!playerId)
430
+ return (0, responses_1.errorResponse)(res, 400, { error: 'missing-field', field: 'playerId' });
431
+ return handleAttachmentInfo(res, ctx, ensemble, playerId);
432
+ }
433
+ // #742 — active-schedule listing (`/schedule list` board command). 2-segment
434
+ // GET; same ordering rationale as attachment-info (precede writeMatch). The
435
+ // schedule WRITE path is the `unschedule` write action below + the MCP
436
+ // `schedule` tool for create (no thin-shim create method on the client).
437
+ const schedulesMatch = pathname.match(/^\/v1\/ensembles\/([^/]+)\/schedules$/);
438
+ if (schedulesMatch) {
439
+ const ensemble = decodeURIComponent(schedulesMatch[1]);
440
+ if (!gateTier(1))
441
+ return; // L3 — read (Tier 1).
442
+ if (method !== 'GET') {
443
+ return (0, responses_1.errorResponse)(res, 405, { error: 'method-not-allowed' }, { Allow: 'GET, OPTIONS' });
444
+ }
445
+ return handleSchedules(res, ctx, ensemble);
446
+ }
436
447
  // Write surface (PR-7a of #340) — POST `/v1/ensembles/:ensemble/<action>`
437
448
  // Match BEFORE the GET-only method gate; everything else (POST to a
438
449
  // read endpoint, GET to a write endpoint) flows into the 405 fallback
@@ -718,6 +729,35 @@ async function handleHosts(res, ctx) {
718
729
  const hosts = await ctx.client.listHosts();
719
730
  (0, responses_1.jsonResponse)(res, 200, hosts);
720
731
  }
732
+ /**
733
+ * #742 — a player's V2 attachment-lifecycle snapshot. Thin shim over
734
+ * `client.attachmentInfo`; the command-center `/attachment-info` board command
735
+ * renders the payload via the shared `formatAttachmentInfoForDisplay`. A 404 is
736
+ * surfaced when the player's session can't be resolved (it throws "No session
737
+ * found …", mirroring the recall not-found contract).
738
+ */
739
+ async function handleAttachmentInfo(res, ctx, ensemble, playerId) {
740
+ try {
741
+ const info = await ctx.client.attachmentInfo(ensemble, playerId);
742
+ (0, responses_1.jsonResponse)(res, 200, info);
743
+ }
744
+ catch (err) {
745
+ const message = err instanceof Error ? err.message : String(err);
746
+ if (/no session found|not found/i.test(message)) {
747
+ return (0, responses_1.errorResponse)(res, 404, { error: 'session-not-found', ensemble, playerId, detail: message });
748
+ }
749
+ throw err;
750
+ }
751
+ }
752
+ /**
753
+ * #742 — active-schedule listing. Thin shim over `client.getSchedules` (which
754
+ * already degrades to `[]` when no scheduler is running). The command-center
755
+ * `/schedule [list]` board command renders the entries.
756
+ */
757
+ async function handleSchedules(res, ctx, ensemble) {
758
+ const schedules = await ctx.client.getSchedules(ensemble);
759
+ (0, responses_1.jsonResponse)(res, 200, schedules);
760
+ }
721
761
  /**
722
762
  * #753 — daemon-process per-source Temporal action counters. Pure in-memory
723
763
  * read (zero Temporal calls); adapter and Pi processes self-report via their
@@ -42,7 +42,7 @@ export { WRITE_BODY_MAX };
42
42
  * mutations. Bodies are uniform `{ playerId, reason? }` (plus per-action
43
43
  * extras); the ensemble lives in the URL.
44
44
  */
45
- export declare const WRITE_ACTIONS: readonly ["cue", "pause", "play", "release", "recruit", "restart", "reset", "destroy", "detach", "recall", "shutdown"];
45
+ export declare const WRITE_ACTIONS: readonly ["cue", "pause", "play", "release", "recruit", "restart", "reset", "destroy", "detach", "recall", "unschedule", "shutdown"];
46
46
  export type WriteAction = (typeof WRITE_ACTIONS)[number];
47
47
  /** Type guard — narrows an arbitrary string to a known `WriteAction`. */
48
48
  export declare function isWriteAction(s: string): s is WriteAction;
@@ -31,6 +31,7 @@ exports.WRITE_ACTIONS = [
31
31
  'destroy',
32
32
  'detach',
33
33
  'recall',
34
+ 'unschedule',
34
35
  'shutdown',
35
36
  ];
36
37
  /** Type guard — narrows an arbitrary string to a known `WriteAction`. */
@@ -69,6 +70,7 @@ async function handleWriteRoute(req, res, client, ensemble, action) {
69
70
  case 'destroy': return await handleDestroy(res, client, ensemble, body);
70
71
  case 'detach': return await handleDetach(res, client, ensemble, body);
71
72
  case 'recall': return await handleRecall(res, client, ensemble, body);
73
+ case 'unschedule': return await handleUnschedule(res, client, ensemble, body);
72
74
  case 'shutdown': return await handleShutdown(res, client, ensemble, body);
73
75
  }
74
76
  }
@@ -242,12 +244,33 @@ async function handleRecall(res, client, ensemble, body) {
242
244
  // The dashboard's `RecallResult` consumer expects a count, not the raw
243
245
  // arrays — projecting `received` + `sent` lengths here keeps the wire
244
246
  // payload tight and avoids leaking inbox + sent-history shape into the
245
- // browser-side type. Callers wanting the full timeline use the MCP
246
- // `recall` tool / TempoClient method directly.
247
+ // browser-side type. Callers wanting the full timeline pass `{ full: true }`
248
+ // (the command-center `/recall` board command, #742) or use the MCP `recall`
249
+ // tool / TempoClient method directly.
247
250
  const result = await client.recall(ensemble, playerId);
251
+ if (body.full === true) {
252
+ // Full timeline — the operator board feeds this through the shared
253
+ // `buildTimeline`/`formatRecall` helpers, same as the MCP tool. Kept behind
254
+ // an opt-in flag so the count-only default stays the dashboard's shape.
255
+ return (0, responses_1.jsonResponse)(res, 200, { ok: true, ensemble, playerId, received: result.received, sent: result.sent });
256
+ }
248
257
  const messages = result.received.length + result.sent.length;
249
258
  (0, responses_1.jsonResponse)(res, 200, { ok: true, ensemble, playerId, messages });
250
259
  }
260
+ /**
261
+ * Cancel a named schedule (#742 — the command-center `/unschedule` &
262
+ * `/schedule delete`). Thin shim over `client.cancelSchedule`. Body is
263
+ * `{ name }` (the schedule key), distinct from the per-player `{ playerId }`
264
+ * shape — schedules are ensemble-scoped, keyed by name. 200 (the cancel IS
265
+ * the operation, not a queued effect).
266
+ */
267
+ async function handleUnschedule(res, client, ensemble, body) {
268
+ const name = (0, body_1.stringField)(body, 'name');
269
+ if (!name)
270
+ return (0, responses_1.errorResponse)(res, 400, { error: 'missing-field', field: 'name' });
271
+ await client.cancelSchedule(ensemble, name);
272
+ (0, responses_1.jsonResponse)(res, 200, { ok: true, ensemble, name });
273
+ }
251
274
  /**
252
275
  * Ensemble teardown (#700 P1) — the HTTP verb the command-center `/ensemble-down`
253
276
  * needs (the CLI `down` + the `shutdown` MCP tool cover this, but neither was
@@ -1,14 +1,13 @@
1
1
  /**
2
2
  * Pure palette-input classification + filtering helpers.
3
3
  *
4
- * Shared between the TUI (`src/tui/commands.ts`) and the dashboard
5
- * (`dashboard/src/components/chat/`) so the two surfaces' chat-input
6
- * autocomplete machinery stays in lockstep — same prefix detection,
7
- * same filtering semantics, same quote-aware tokenizer.
4
+ * Used by the dashboard (`dashboard/src/components/chat/`) chat-input
5
+ * autocomplete machinery prefix detection, filtering semantics, and the
6
+ * quote-aware tokenizer.
8
7
  *
9
- * Extracted from `src/tui/commands.ts` in #471/#472 (dashboard chat-input
10
- * TUI parity bundle). The TUI re-exports every symbol from here so existing
11
- * callers see no behaviour change. The dashboard imports directly from
8
+ * Originally extracted from the former `src/tui/commands.ts` in #471/#472
9
+ * (dashboard ↔ TUI parity bundle); the Ink TUI was deleted in #789, so the
10
+ * dashboard is now the sole consumer. The dashboard imports directly from
12
11
  * `agent-tempo/palette` (Vite alias `agent-tempo` → `../src`).
13
12
  *
14
13
  * **No imports allowed.** Keep this module dependency-free so it
@@ -2,14 +2,13 @@
2
2
  /**
3
3
  * Pure palette-input classification + filtering helpers.
4
4
  *
5
- * Shared between the TUI (`src/tui/commands.ts`) and the dashboard
6
- * (`dashboard/src/components/chat/`) so the two surfaces' chat-input
7
- * autocomplete machinery stays in lockstep — same prefix detection,
8
- * same filtering semantics, same quote-aware tokenizer.
5
+ * Used by the dashboard (`dashboard/src/components/chat/`) chat-input
6
+ * autocomplete machinery prefix detection, filtering semantics, and the
7
+ * quote-aware tokenizer.
9
8
  *
10
- * Extracted from `src/tui/commands.ts` in #471/#472 (dashboard chat-input
11
- * TUI parity bundle). The TUI re-exports every symbol from here so existing
12
- * callers see no behaviour change. The dashboard imports directly from
9
+ * Originally extracted from the former `src/tui/commands.ts` in #471/#472
10
+ * (dashboard ↔ TUI parity bundle); the Ink TUI was deleted in #789, so the
11
+ * dashboard is now the sole consumer. The dashboard imports directly from
13
12
  * `agent-tempo/palette` (Vite alias `agent-tempo` → `../src`).
14
13
  *
15
14
  * **No imports allowed.** Keep this module dependency-free so it
@@ -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
  }