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
@@ -42,9 +42,9 @@ exports.createTempoClientCore = createTempoClientCore;
42
42
  * future SSE event source, and any external SDK consumer that wants a
43
43
  * headless surface (no `child_process` dependency).
44
44
  *
45
- * The two spawn methods (`createEnsemble`, `spawnConductor`) and the
46
- * `runTempoCli` helper live in `./with-spawn.ts`, which composes this
47
- * factory and adds the TTY-bound surface.
45
+ * The spawn method (`createEnsemble`) and the `runTempoCli` helper live in
46
+ * `./with-spawn.ts`, which composes this factory and adds the TTY-bound surface.
47
+ * (`spawnConductor` was removed in #789 with the Ink TUI.)
48
48
  *
49
49
  * See `docs/adr/0007-tempoclient-core-withspawn-split.md` and
50
50
  * `docs/design/tempoclient-core-spawn-split.md`.
@@ -207,10 +207,10 @@ function createTempoClientCore(client, opts = {}) {
207
207
  continue;
208
208
  const entry = ensembleMap.get(name) || { count: 0, hasConductor: false };
209
209
  entry.count++;
210
- // Preferred: memo `AgentTempoIsConductor` (v1.8 SA diet) with
211
- // legacy-SA fallback via the dual-read helper. Final fallback:
212
- // workflow ID convention — covers the brief window after a
213
- // conductor spawn before visibility propagates.
210
+ // Preferred: memo `AgentTempoIsConductor` (memo-only after the #788
211
+ // legacy-SA prune). Final fallback: workflow ID convention covers
212
+ // the brief window after a conductor spawn before visibility
213
+ // propagates.
214
214
  const isConductorFromSA = (0, search_attributes_1.getIsConductor)(wf) === true;
215
215
  const isConductorFromId = wf.workflowId?.endsWith('-conductor') ?? false;
216
216
  if (isConductorFromSA || isConductorFromId) {
@@ -257,10 +257,9 @@ function createTempoClientCore(client, opts = {}) {
257
257
  // agent — counting it produced confusing "(2 players)" rows on
258
258
  // a fresh ensemble with one real player. Mirrors the
259
259
  // `filterRealPlayers` rule used in StatusBar (cf6becd). Detect
260
- // via the `AgentTempoPlayerType` memo (v1.8 SA diet; legacy-SA
261
- // fallback via the dual-read helper), with a workflow-id-suffix
262
- // fallback for the brief post-start window before visibility
263
- // propagates.
260
+ // via the `AgentTempoPlayerType` memo (memo-only after the #788
261
+ // legacy-SA prune), with a workflow-id-suffix fallback for the
262
+ // brief post-start window before visibility propagates.
264
263
  const playerType = (0, search_attributes_1.getPlayerType)(wf);
265
264
  const isMaestroSession = playerType === 'maestro'
266
265
  || (wf.workflowId?.endsWith('-maestro') ?? false);
@@ -379,10 +378,9 @@ function createTempoClientCore(client, opts = {}) {
379
378
  for await (const wf of listWorkflows({ query })) {
380
379
  const sa = wf.searchAttributes || {};
381
380
  const playerId = Array.isArray(sa.AgentTempoPlayerId) ? String(sa.AgentTempoPlayerId[0]) : wf.workflowId;
382
- // Preferred: memo (v1.8 SA diet) with legacy-SA fallback via the
383
- // dual-read helper. Final fallback: workflow ID convention —
384
- // covers the brief window after a conductor spawn before
385
- // visibility propagates.
381
+ // Preferred: memo (memo-only after the #788 legacy-SA prune). Final
382
+ // fallback: workflow ID convention — covers the brief window after a
383
+ // conductor spawn before visibility propagates.
386
384
  const isConductorFromSA = (0, search_attributes_1.getIsConductor)(wf) === true;
387
385
  const isConductorFromId = wf.workflowId?.endsWith('-conductor') ?? false;
388
386
  players.push({
@@ -7,7 +7,8 @@
7
7
  * - {@link createTempoClientCore} — pure Temporal RPC, headless-safe
8
8
  * (daemon, MCP server, SSE event source, future SDK consumers).
9
9
  * - {@link createTempoClientWithSpawn} — the TTY-bound superset adding
10
- * `createEnsemble` and `spawnConductor`. Use from the TUI/CLI only.
10
+ * `createEnsemble` (the CLI `up` path + the command-center board's
11
+ * `/ensemble-up`). `spawnConductor` was removed in #789 with the Ink TUI.
11
12
  *
12
13
  * `createTempoClient` is preserved as a permanent alias for
13
14
  * `createTempoClientWithSpawn` so every existing call site keeps the same
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * TempoClient — public interface and related types.
3
3
  *
4
- * Extracted from `src/tui/client.ts` so that non-TUI consumers (CLI, tests,
5
- * external integrations) can depend on the interface without pulling in Ink/React.
4
+ * Originally extracted from the former `src/tui/client.ts` so that headless
5
+ * consumers (CLI, tests, external integrations) could depend on the interface
6
+ * without pulling in Ink/React. (The Ink TUI was deleted in #789; the interface
7
+ * remains the shared contract for the CLI, daemon, command-center board, and
8
+ * external integrations.)
6
9
  */
7
10
  import type { AgentType, MaestroPlayerInfo, MaestroRelayMessage, HistoryEntry, Message, SentMessage, SessionMetadata, ScheduleEntry, QualityGate, StageEntry, WorktreeEntry, EnsembleChatResult, AttachmentInfo, HostInfo, AnswerEntry, CoatCheckEntry } from '../types';
8
11
  import type { CoatCheckPutInput, CoatCheckPutResult, CoatCheckGetInput } from '../workflows/maestro-signals';
@@ -515,18 +518,6 @@ export interface TempoClientWithSpawn extends TempoClientCore {
515
518
  * daemon, or other headless processes.
516
519
  */
517
520
  createEnsemble(opts: CreateEnsembleOpts): Promise<void>;
518
- /**
519
- * Spawn a conductor terminal for an existing ensemble — the restore-
520
- * after-shutdown path. Shells out to `agent-tempo up <name>` which is
521
- * idempotent at the workflow layer. Semantically distinct from
522
- * {@link TempoClientWithSpawn.createEnsemble}: this fires on an ensemble
523
- * that already exists; a "create" contradiction would mislead future
524
- * readers. **Requires a TTY context.**
525
- */
526
- spawnConductor(opts: {
527
- ensemble: string;
528
- workDir?: string;
529
- }): Promise<void>;
530
521
  }
531
522
  /**
532
523
  * Backwards-compatible alias preserved indefinitely (per ADR 0007's
@@ -7,8 +7,12 @@
7
7
  * no TTY and a stray spawn would launch a terminal nothing can render.
8
8
  *
9
9
  * The boundary is enforced by the type system: headless callers depend on
10
- * `TempoClientCore`; only the TUI (and its `ensure-conductor-spawned`
11
- * helper) opts into the spawn surface.
10
+ * `TempoClientCore`; the spawn surface is opt-in.
11
+ *
12
+ * NOTE (#789): `spawnConductor` was removed with the Ink TUI (its only caller
13
+ * was the TUI's `ensure-conductor-spawned` helper). WithSpawn's only distinct
14
+ * method is now `createEnsemble` — still live (CLI `up`, command-center board).
15
+ * The Core-vs-WithSpawn split STAYS (WithSpawn is the universal client alias).
12
16
  *
13
17
  * See `docs/adr/0007-tempoclient-core-withspawn-split.md`.
14
18
  */
@@ -18,8 +22,9 @@ import type { TempoClientWithSpawn } from './interface';
18
22
  export type { CreateTempoClientOpts } from './core';
19
23
  /**
20
24
  * Build a `TempoClientWithSpawn` over a configured Temporal `Client`.
21
- * Composes {@link createTempoClientCore} and adds the two TTY-bound
22
- * spawn methods. Use this for TUI-side consumers; headless callers
25
+ * Composes {@link createTempoClientCore} and adds the TTY-bound
26
+ * `createEnsemble` spawn method. Use this for spawn-capable consumers (the CLI
27
+ * `up` path, the command-center board's `/ensemble-up`); pure-headless callers
23
28
  * should use {@link createTempoClientCore} directly.
24
29
  *
25
30
  * `opts` (e.g. `subscribeDeps`) is forwarded to the Core factory.
@@ -59,8 +59,9 @@ async function runTempoCli(args, workDir) {
59
59
  }
60
60
  /**
61
61
  * Build a `TempoClientWithSpawn` over a configured Temporal `Client`.
62
- * Composes {@link createTempoClientCore} and adds the two TTY-bound
63
- * spawn methods. Use this for TUI-side consumers; headless callers
62
+ * Composes {@link createTempoClientCore} and adds the TTY-bound
63
+ * `createEnsemble` spawn method. Use this for spawn-capable consumers (the CLI
64
+ * `up` path, the command-center board's `/ensemble-up`); pure-headless callers
64
65
  * should use {@link createTempoClientCore} directly.
65
66
  *
66
67
  * `opts` (e.g. `subscribeDeps`) is forwarded to the Core factory.
@@ -75,13 +76,9 @@ function createTempoClientWithSpawn(client, opts = {}) {
75
76
  : ['up', opts.ensemble];
76
77
  await runTempoCli(args, opts.workDir);
77
78
  },
78
- async spawnConductor(opts) {
79
- // `agent-tempo up <ensemble>` is idempotent at the workflow layer —
80
- // re-invoking it on a live ensemble reuses the existing conductor
81
- // workflow (deterministic workflow ID). Distinct from
82
- // `createEnsemble` so call sites read as "make sure the conductor
83
- // terminal is running", not "create a new ensemble".
84
- await runTempoCli(['up', opts.ensemble], opts.workDir);
85
- },
79
+ // #789: `spawnConductor` removed with the Ink TUI — its only caller was the
80
+ // TUI's `ensure-conductor-spawned` helper (also deleted). WithSpawn's only
81
+ // distinct method is now `createEnsemble` (still used by the command-center
82
+ // board's `/ensemble-up` + the CLI `up` path).
86
83
  };
87
84
  }
package/dist/config.d.ts CHANGED
@@ -18,6 +18,20 @@ export declare const ENV: {
18
18
  readonly DEFAULT_AGENT: "AGENT_TEMPO_DEFAULT_AGENT";
19
19
  readonly PLAYER_TYPE: "AGENT_TEMPO_PLAYER_TYPE";
20
20
  readonly CLAUDE_BIN: "AGENT_TEMPO_CLAUDE_BIN";
21
+ /**
22
+ * #704 — override (ms) for the booting attach-timeout watchdog deadline. Read
23
+ * at spawn time (`startRecruitedSession`) and threaded onto durable metadata
24
+ * (`SessionMetadata.bootingDeadlineMs`) because workflows can't read env.
25
+ * Default 180s. For tests/tuning.
26
+ */
27
+ readonly BOOTING_DEADLINE_MS: "AGENT_TEMPO_BOOTING_DEADLINE_MS";
28
+ /**
29
+ * #704 — override (ms) for how long a destroyed / boot-timed-out run's
30
+ * close-reason tombstone is honored by the bootstrap orphan-guard
31
+ * (`server.ts`). A late-launching orphan within this window self-exits.
32
+ * Default 6h (the observed orphan spawn delay was ~100min). For tuning.
33
+ */
34
+ readonly ORPHAN_TOMBSTONE_TTL_MS: "AGENT_TEMPO_ORPHAN_TOMBSTONE_TTL_MS";
21
35
  /**
22
36
  * #753 — cadence (ms) of the periodic `[agent-tempo:action-counters]` log
23
37
  * line each instrumented process emits. Default 5 minutes; `0` disables.
@@ -257,19 +271,6 @@ export interface PersistedConfig {
257
271
  temporalTlsKeyPath?: string;
258
272
  defaultAgent?: AgentType;
259
273
  claudeBin?: string;
260
- /**
261
- * Bearer token for the daemon's HTTP/SSE event source (#94, #95,
262
- * SSE-PROTOCOL.md §3.1). Auto-generated on first daemon boot when
263
- * bearer mode is required (`AGENT_TEMPO_HTTP_BIND` non-loopback OR
264
- * a request with a non-loopback `Origin`) and no token is set:
265
- * `crypto.randomBytes(32).toString('base64url')`, 0600 on POSIX.
266
- * Rotation = delete this field; next daemon boot regenerates.
267
- *
268
- * 3e: this LEGACY single token is migrated to the READ tier (T1) — a daemon
269
- * with only `httpToken` set keeps read access and emits a one-time startup
270
- * warning to set an admin token for writes/gate/inner. Prefer `readToken`.
271
- */
272
- httpToken?: string;
273
274
  /**
274
275
  * 3e RBAC — the READ-tier (T1) bearer token. Env `AGENT_TEMPO_HTTP_READ_TOKEN`
275
276
  * takes precedence over this; auto-generated here on first bearer-mode boot if
package/dist/config.js CHANGED
@@ -47,6 +47,20 @@ exports.ENV = {
47
47
  DEFAULT_AGENT: 'AGENT_TEMPO_DEFAULT_AGENT',
48
48
  PLAYER_TYPE: 'AGENT_TEMPO_PLAYER_TYPE',
49
49
  CLAUDE_BIN: 'AGENT_TEMPO_CLAUDE_BIN',
50
+ /**
51
+ * #704 — override (ms) for the booting attach-timeout watchdog deadline. Read
52
+ * at spawn time (`startRecruitedSession`) and threaded onto durable metadata
53
+ * (`SessionMetadata.bootingDeadlineMs`) because workflows can't read env.
54
+ * Default 180s. For tests/tuning.
55
+ */
56
+ BOOTING_DEADLINE_MS: 'AGENT_TEMPO_BOOTING_DEADLINE_MS',
57
+ /**
58
+ * #704 — override (ms) for how long a destroyed / boot-timed-out run's
59
+ * close-reason tombstone is honored by the bootstrap orphan-guard
60
+ * (`server.ts`). A late-launching orphan within this window self-exits.
61
+ * Default 6h (the observed orphan spawn delay was ~100min). For tuning.
62
+ */
63
+ ORPHAN_TOMBSTONE_TTL_MS: 'AGENT_TEMPO_ORPHAN_TOMBSTONE_TTL_MS',
50
64
  /**
51
65
  * #753 — cadence (ms) of the periodic `[agent-tempo:action-counters]` log
52
66
  * line each instrumented process emits. Default 5 minutes; `0` disables.
@@ -16,6 +16,28 @@
16
16
  * for the kill regex to match.
17
17
  */
18
18
  export declare const ENSEMBLE_SENTINEL_FLAG = "--remote-control-session-name-prefix";
19
+ /**
20
+ * 2.0 wire-protocol version (#786 — the cutover keystone). Single source of truth
21
+ * for the protocol stamp, the boot guard, and the `claimAttachment` adapter
22
+ * handshake.
23
+ *
24
+ * - **Stamp:** every 2.0-created workflow records `protocol: PROTOCOL_VERSION` on
25
+ * its START input (so it's in history from run 1 and survives `continueAsNew`
26
+ * via the input spread) and upserts the memo key {@link
27
+ * MEMO_KEYS.protocol | `AgentTempoProtocol`} = PROTOCOL_VERSION.
28
+ * - **Boot guard:** a 2.0 daemon refuses to register workers if visibility shows
29
+ * any Running agent-tempo workflow lacking the stamp — a 2.0 worker can never
30
+ * deterministically replay a 1.x-recorded history, so we fail LOUD at boot
31
+ * (printing `agent-tempo upgrade-to-2`) instead of non-determinism-faulting
32
+ * deep in a workflow task later.
33
+ * - **claimAttachment:** the adapter sends `protocolVersion`; a 2.0 workflow
34
+ * rejects any value `!== PROTOCOL_VERSION` (incl. a v1 adapter that omits it).
35
+ *
36
+ * This module is runtime-free, so the constant is safe to import from BOTH the
37
+ * sandboxed workflow bundle (the stamp + claim validator) AND the daemon guard /
38
+ * CLI (the check). Bumping it is a hard cutover — see `docs/design/v2-scoping.md`.
39
+ */
40
+ export declare const PROTOCOL_VERSION: 2;
19
41
  /**
20
42
  * Escape the two regex metacharacters that can appear in validated player/ensemble
21
43
  * names (which are constrained to `[A-Za-z0-9._-]+`). Safe to interpolate the
@@ -48,3 +70,31 @@ export declare function ensembleReadyBanner(name: string, playerCount: number):
48
70
  * actually stops other players from acting while the conductor waits.
49
71
  */
50
72
  export declare function ensembleReadyDirective(name: string, playerCount: number): string;
73
+ /** One command-center access tier — its short label + the source-of-truth wording. */
74
+ export interface CommandCenterAccessTier {
75
+ /** Short label shown at the head of the line (e.g. `No auth`, `/login`, `API key`). */
76
+ label: string;
77
+ /** Tier wording — mirrored VERBATIM into docs/cli.md (drift-guarded). */
78
+ description: string;
79
+ }
80
+ /**
81
+ * #791 — command-center access tiers. THE SINGLE SOURCE OF TRUTH for the three
82
+ * access levels the `command-center` board offers, shared between the launch
83
+ * banner (`src/cli/command-center-command.ts`) and `docs/cli.md`.
84
+ *
85
+ * The tiers are independent, NOT a strict ladder: tier 1 (board + operator
86
+ * controls) needs NO auth on a local loopback daemon; tiers 2/3 only add the
87
+ * LLM PLANNER (ask / handoff / recruit), which needs either a Claude
88
+ * subscription via in-session `/login` (zero API key) or an `ANTHROPIC_API_KEY`.
89
+ *
90
+ * `tests/cli/command-center-access-tiers.test.ts` asserts the banner renders
91
+ * every tier AND that `docs/cli.md` carries each tier's label + description
92
+ * verbatim — so the launch text and the docs can never drift.
93
+ */
94
+ export declare const COMMAND_CENTER_ACCESS_TIERS: readonly CommandCenterAccessTier[];
95
+ /**
96
+ * Render {@link COMMAND_CENTER_ACCESS_TIERS} as plain indented ` • label — description`
97
+ * lines for the `command-center` launch banner. One string per tier; the caller
98
+ * styles them and prints a heading. Runtime-free, so it stays importable anywhere.
99
+ */
100
+ export declare function commandCenterAccessTierLines(): string[];
package/dist/constants.js CHANGED
@@ -6,10 +6,11 @@
6
6
  * code without dragging Node-only modules into the sandboxed bundle.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.ENSEMBLE_SENTINEL_FLAG = void 0;
9
+ exports.COMMAND_CENTER_ACCESS_TIERS = exports.PROTOCOL_VERSION = exports.ENSEMBLE_SENTINEL_FLAG = void 0;
10
10
  exports.escapeNameForRegex = escapeNameForRegex;
11
11
  exports.ensembleReadyBanner = ensembleReadyBanner;
12
12
  exports.ensembleReadyDirective = ensembleReadyDirective;
13
+ exports.commandCenterAccessTierLines = commandCenterAccessTierLines;
13
14
  /**
14
15
  * CLI flag injected into every Claude Code spawn to carry the ensemble name as a
15
16
  * sentinel in the process's CommandLine. `src/activities/hard-terminate.ts`
@@ -22,6 +23,28 @@ exports.ensembleReadyDirective = ensembleReadyDirective;
22
23
  * for the kill regex to match.
23
24
  */
24
25
  exports.ENSEMBLE_SENTINEL_FLAG = '--remote-control-session-name-prefix';
26
+ /**
27
+ * 2.0 wire-protocol version (#786 — the cutover keystone). Single source of truth
28
+ * for the protocol stamp, the boot guard, and the `claimAttachment` adapter
29
+ * handshake.
30
+ *
31
+ * - **Stamp:** every 2.0-created workflow records `protocol: PROTOCOL_VERSION` on
32
+ * its START input (so it's in history from run 1 and survives `continueAsNew`
33
+ * via the input spread) and upserts the memo key {@link
34
+ * MEMO_KEYS.protocol | `AgentTempoProtocol`} = PROTOCOL_VERSION.
35
+ * - **Boot guard:** a 2.0 daemon refuses to register workers if visibility shows
36
+ * any Running agent-tempo workflow lacking the stamp — a 2.0 worker can never
37
+ * deterministically replay a 1.x-recorded history, so we fail LOUD at boot
38
+ * (printing `agent-tempo upgrade-to-2`) instead of non-determinism-faulting
39
+ * deep in a workflow task later.
40
+ * - **claimAttachment:** the adapter sends `protocolVersion`; a 2.0 workflow
41
+ * rejects any value `!== PROTOCOL_VERSION` (incl. a v1 adapter that omits it).
42
+ *
43
+ * This module is runtime-free, so the constant is safe to import from BOTH the
44
+ * sandboxed workflow bundle (the stamp + claim validator) AND the daemon guard /
45
+ * CLI (the check). Bumping it is a hard cutover — see `docs/design/v2-scoping.md`.
46
+ */
47
+ exports.PROTOCOL_VERSION = 2;
25
48
  /**
26
49
  * Escape the two regex metacharacters that can appear in validated player/ensemble
27
50
  * names (which are constrained to `[A-Za-z0-9._-]+`). Safe to interpolate the
@@ -72,3 +95,39 @@ function ensembleReadyDirective(name, playerCount) {
72
95
  'If the user has not spoken yet, wait silently.',
73
96
  ].join('\n');
74
97
  }
98
+ /**
99
+ * #791 — command-center access tiers. THE SINGLE SOURCE OF TRUTH for the three
100
+ * access levels the `command-center` board offers, shared between the launch
101
+ * banner (`src/cli/command-center-command.ts`) and `docs/cli.md`.
102
+ *
103
+ * The tiers are independent, NOT a strict ladder: tier 1 (board + operator
104
+ * controls) needs NO auth on a local loopback daemon; tiers 2/3 only add the
105
+ * LLM PLANNER (ask / handoff / recruit), which needs either a Claude
106
+ * subscription via in-session `/login` (zero API key) or an `ANTHROPIC_API_KEY`.
107
+ *
108
+ * `tests/cli/command-center-access-tiers.test.ts` asserts the banner renders
109
+ * every tier AND that `docs/cli.md` carries each tier's label + description
110
+ * verbatim — so the launch text and the docs can never drift.
111
+ */
112
+ exports.COMMAND_CENTER_ACCESS_TIERS = [
113
+ {
114
+ label: 'No auth',
115
+ description: 'board + operator controls (cue, pause, play, restart, destroy) — works out of the box on a local (loopback) daemon; no token or login required.',
116
+ },
117
+ {
118
+ label: '/login',
119
+ description: 'LLM planner on your Claude Pro/Max subscription — run /login inside the board to enable the planner (ask, handoff, recruit) with zero API key.',
120
+ },
121
+ {
122
+ label: 'API key',
123
+ description: 'LLM planner on an API key — set ANTHROPIC_API_KEY before launch to run the planner against the Anthropic API.',
124
+ },
125
+ ];
126
+ /**
127
+ * Render {@link COMMAND_CENTER_ACCESS_TIERS} as plain indented ` • label — description`
128
+ * lines for the `command-center` launch banner. One string per tier; the caller
129
+ * styles them and prints a heading. Runtime-free, so it stays importable anywhere.
130
+ */
131
+ function commandCenterAccessTierLines() {
132
+ return exports.COMMAND_CENTER_ACCESS_TIERS.map((t) => ` • ${t.label} — ${t.description}`);
133
+ }
package/dist/daemon.js CHANGED
@@ -75,6 +75,8 @@ const inner_loop_1 = require("./http/inner-loop");
75
75
  const doorbell_1 = require("./http/doorbell");
76
76
  const ingest_registry_1 = require("./http/ingest-registry");
77
77
  const grpc_shutdown_guard_1 = require("./utils/grpc-shutdown-guard");
78
+ const worker_2 = require("@temporalio/worker");
79
+ const nondeterminism_alarm_1 = require("./observability/nondeterminism-alarm");
78
80
  const action_counters_1 = require("./utils/action-counters");
79
81
  const daemon_1 = require("./cli/daemon");
80
82
  const client_3 = require("./client");
@@ -847,6 +849,33 @@ async function main() {
847
849
  // close race so a stray retry timer can't kill the long-lived daemon. See
848
850
  // src/utils/grpc-shutdown-guard.ts.
849
851
  (0, grpc_shutdown_guard_1.installGrpcShutdownGuard)();
852
+ // #886 slice 1 — install the nondeterminism alarm BEFORE any Worker.create.
853
+ // Wraps Temporal's Runtime logger so a nondeterminism / determinism-violation
854
+ // flap (the #801 incident: 57 workflow-task failures in 3min with ZERO
855
+ // operator signal) is NAMED instantly — a prominent `[agent-tempo:ALARM]`
856
+ // line with a running count — and surfaced on `GET /v1/health`
857
+ // (`HealthV1.nondeterminism`). Runtime.install must precede worker creation
858
+ // and can only run once per process; both hold here (daemon entry point).
859
+ {
860
+ const alarm = new nondeterminism_alarm_1.NondeterminismAlarm({
861
+ onHit: (count, sample) => {
862
+ // Prominent + greppable (`ALARM`), distinct from normal daemon chatter.
863
+ log(`[agent-tempo:ALARM] nondeterminism #${count} — ${sample.detail}`);
864
+ },
865
+ });
866
+ (0, nondeterminism_alarm_1.setGlobalNondeterminismAlarm)(alarm);
867
+ try {
868
+ // Match the SDK default logger level ('INFO') so verbosity is unchanged;
869
+ // the wrapper only ADDS alarm detection on WARN/ERROR records.
870
+ worker_2.Runtime.install({ logger: (0, nondeterminism_alarm_1.wrapLoggerWithAlarm)(new worker_2.DefaultLogger('INFO'), alarm) });
871
+ }
872
+ catch (err) {
873
+ // Runtime.install throws if a Runtime already exists this process. Non-
874
+ // fatal: the alarm singleton still backs /v1/health; we just couldn't
875
+ // intercept Core logs. (Shouldn't happen — this is the daemon entry.)
876
+ log('nondeterminism alarm: Runtime.install skipped (runtime already initialized):', err instanceof Error ? err.message : err);
877
+ }
878
+ }
850
879
  // ADR 0014 §5.4 / gate 4 — dev daemon log self-identifies. Banner fires
851
880
  // first so it lands at the top of `~/.agent-tempo-dev/daemon.log` for
852
881
  // grep-friendly identification regardless of subsequent log volume.
@@ -952,6 +981,58 @@ async function main() {
952
981
  log('search-attribute preflight probe failed (non-fatal — createWorkers will surface the real error):', result.probeError);
953
982
  }
954
983
  }
984
+ // #786 — 2.0 cutover BOOT GUARD. Runs AFTER the search-attribute preflight
985
+ // and BEFORE createWorkers(): a 2.0 worker cannot deterministically replay a
986
+ // 1.x-recorded history, so refuse to register workers if visibility still
987
+ // shows ANY Running agent-tempo workflow lacking the protocol-2 stamp (a
988
+ // pre-cutover 1.x run). FAIL-CLOSED — a timed-out/errored scan, or a failure
989
+ // to even run the scan, refuses too (we couldn't PROVE the cutover is clean;
990
+ // see the #845 partial-scan lesson). Namespace-scoped via the client below.
991
+ // See src/upgrade/boot-guard.ts.
992
+ {
993
+ const { checkProtocolGuard } = await Promise.resolve().then(() => __importStar(require('./upgrade/boot-guard')));
994
+ let result;
995
+ let guardConn = null;
996
+ try {
997
+ guardConn = await (0, connection_1.createTemporalConnection)(config);
998
+ const guardClient = new client_1.Client({
999
+ connection: guardConn,
1000
+ namespace: config.temporalNamespace,
1001
+ interceptors: (0, action_counters_1.actionCountingInterceptors)(),
1002
+ });
1003
+ result = await checkProtocolGuard(guardClient, { log });
1004
+ }
1005
+ catch (err) {
1006
+ // FAIL-CLOSED — couldn't even run the scan (e.g. connection failure).
1007
+ // Without a completed scan we can't prove a clean cutover, so refuse
1008
+ // rather than risk a 2.0 worker replaying a 1.x history.
1009
+ result = {
1010
+ ok: false,
1011
+ reason: 'scan-incomplete',
1012
+ message: `agent-tempo 2.0 boot guard could not run the protocol scan ` +
1013
+ `(${err instanceof Error ? err.message : String(err)}). Refusing to boot ` +
1014
+ `(fail-closed). Verify Temporal at ${config.temporalAddress} ` +
1015
+ `(namespace ${config.temporalNamespace}) is reachable, then retry.`,
1016
+ };
1017
+ }
1018
+ finally {
1019
+ if (guardConn)
1020
+ await guardConn.close().catch(() => { });
1021
+ }
1022
+ if (!result.ok) {
1023
+ process.stderr.write('ERROR: ' + result.message + '\n');
1024
+ log(`Daemon refused to boot — ${result.reason} (#786 protocol guard)`);
1025
+ try {
1026
+ fs.unlinkSync(daemon_1.DAEMON_PID_PATH);
1027
+ }
1028
+ catch { /* ignore */ }
1029
+ try {
1030
+ fs.unlinkSync(daemon_1.DAEMON_HEARTBEAT_PATH);
1031
+ }
1032
+ catch { /* ignore */ }
1033
+ process.exit(1);
1034
+ }
1035
+ }
955
1036
  // Use mutable refs so signal handlers can be registered before workers
956
1037
  // are created — closes the narrow window where a SIGTERM during
957
1038
  // createWorkers() would be missed.
@@ -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).