agent-tempo 1.7.0 → 2.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/CLAUDE.md +10 -13
  2. package/README.md +21 -15
  3. package/dashboard/dist/assets/index-BbR7zIdK.js.map +1 -1
  4. package/dashboard/package.json +1 -1
  5. package/dist/activities/hard-terminate.d.ts +2 -2
  6. package/dist/activities/hard-terminate.js +3 -9
  7. package/dist/activities/maestro.d.ts +8 -13
  8. package/dist/activities/maestro.js +3 -10
  9. package/dist/activities/outbox.js +16 -2
  10. package/dist/activities/resolve.d.ts +18 -7
  11. package/dist/activities/resolve.js +58 -46
  12. package/dist/adapters/base.js +3 -0
  13. package/dist/adapters/claude-code/adapter.js +7 -0
  14. package/dist/cli/command-center-command.js +15 -1
  15. package/dist/cli/commands.d.ts +8 -0
  16. package/dist/cli/commands.js +79 -11
  17. package/dist/cli/daemon-command.js +19 -33
  18. package/dist/cli/help-text.js +9 -7
  19. package/dist/cli/home-command.d.ts +26 -0
  20. package/dist/cli/home-command.js +156 -0
  21. package/dist/cli/removed-verbs.d.ts +0 -4
  22. package/dist/cli/removed-verbs.js +23 -12
  23. package/dist/cli/sa-preflight.d.ts +0 -14
  24. package/dist/cli/sa-preflight.js +1 -17
  25. package/dist/cli/startup.d.ts +1 -10
  26. package/dist/cli/startup.js +0 -40
  27. package/dist/cli.js +34 -80
  28. package/dist/client/core.js +13 -15
  29. package/dist/client/index.d.ts +2 -1
  30. package/dist/client/interface.d.ts +5 -14
  31. package/dist/client/with-spawn.d.ts +9 -4
  32. package/dist/client/with-spawn.js +7 -10
  33. package/dist/config.d.ts +14 -13
  34. package/dist/config.js +14 -0
  35. package/dist/constants.d.ts +50 -0
  36. package/dist/constants.js +60 -1
  37. package/dist/daemon.js +81 -0
  38. package/dist/http/auth.d.ts +3 -21
  39. package/dist/http/auth.js +10 -31
  40. package/dist/http/event-types.d.ts +38 -4
  41. package/dist/http/orphans.d.ts +3 -3
  42. package/dist/http/orphans.js +3 -3
  43. package/dist/http/server.d.ts +0 -5
  44. package/dist/http/server.js +79 -29
  45. package/dist/http/snapshot.js +3 -0
  46. package/dist/http/writes.d.ts +1 -1
  47. package/dist/http/writes.js +25 -2
  48. package/dist/observability/nondeterminism-alarm.d.ts +113 -0
  49. package/dist/observability/nondeterminism-alarm.js +162 -0
  50. package/dist/palette/index.d.ts +6 -7
  51. package/dist/palette/index.js +6 -7
  52. package/dist/pi/mission-control/actions.d.ts +67 -1
  53. package/dist/pi/mission-control/actions.js +61 -0
  54. package/dist/pi/mission-control/extension.d.ts +77 -0
  55. package/dist/pi/mission-control/extension.js +171 -0
  56. package/dist/pi/workflow-client.d.ts +14 -16
  57. package/dist/pi/workflow-client.js +18 -44
  58. package/dist/server-tools.js +30 -29
  59. package/dist/server.js +42 -0
  60. package/dist/tools/action-guard.d.ts +23 -0
  61. package/dist/tools/action-guard.js +33 -0
  62. package/dist/tools/coat-check.d.ts +20 -0
  63. package/dist/tools/coat-check.js +129 -0
  64. package/dist/tools/gate.d.ts +13 -0
  65. package/dist/tools/gate.js +88 -0
  66. package/dist/tools/schedule.d.ts +18 -0
  67. package/dist/tools/schedule.js +93 -1
  68. package/dist/tools/stage.d.ts +17 -0
  69. package/dist/tools/stage.js +85 -1
  70. package/dist/tools/state.d.ts +14 -0
  71. package/dist/tools/state.js +95 -0
  72. package/dist/types.d.ts +54 -7
  73. package/dist/upgrade/boot-guard.d.ts +95 -0
  74. package/dist/upgrade/boot-guard.js +88 -0
  75. package/dist/upgrade/from-upgrade.d.ts +95 -0
  76. package/dist/upgrade/from-upgrade.js +330 -0
  77. package/dist/upgrade/phase-engine.js +4 -1
  78. package/dist/utils/duration.d.ts +3 -6
  79. package/dist/utils/duration.js +3 -6
  80. package/dist/utils/format-hosts.d.ts +1 -1
  81. package/dist/utils/format-hosts.js +0 -2
  82. package/dist/utils/grpc-shutdown-guard.js +0 -4
  83. package/dist/utils/hosts.d.ts +4 -10
  84. package/dist/utils/hosts.js +12 -28
  85. package/dist/utils/orphan-guard.d.ts +37 -0
  86. package/dist/utils/orphan-guard.js +26 -0
  87. package/dist/utils/search-attributes.d.ts +28 -18
  88. package/dist/utils/search-attributes.js +48 -18
  89. package/dist/utils/visibility-deadline.d.ts +16 -0
  90. package/dist/utils/visibility-deadline.js +16 -0
  91. package/dist/workflows/maestro-signals.d.ts +0 -13
  92. package/dist/workflows/maestro-signals.js +6 -14
  93. package/dist/workflows/maestro.js +27 -32
  94. package/dist/workflows/scheduler.js +7 -1
  95. package/dist/workflows/session.js +237 -153
  96. package/dist/workflows/signals.d.ts +9 -1
  97. package/dist/workflows/signals.js +8 -6
  98. package/package.json +1 -6
  99. package/workflow-bundle.js +478 -225
  100. package/dist/cli/legacy-migration.d.ts +0 -35
  101. package/dist/cli/legacy-migration.js +0 -341
  102. package/dist/client/ensure-conductor-spawned.d.ts +0 -35
  103. package/dist/client/ensure-conductor-spawned.js +0 -48
  104. package/dist/tui/App.d.ts +0 -85
  105. package/dist/tui/App.js +0 -1791
  106. package/dist/tui/bootstrap-types.d.ts +0 -46
  107. package/dist/tui/bootstrap-types.js +0 -7
  108. package/dist/tui/commands.d.ts +0 -71
  109. package/dist/tui/commands.js +0 -1375
  110. package/dist/tui/components/ChatView.d.ts +0 -35
  111. package/dist/tui/components/ChatView.js +0 -54
  112. package/dist/tui/components/CommandPalette.d.ts +0 -21
  113. package/dist/tui/components/CommandPalette.js +0 -67
  114. package/dist/tui/components/ConversationStream.d.ts +0 -114
  115. package/dist/tui/components/ConversationStream.js +0 -307
  116. package/dist/tui/components/CreateEnsembleWizard.d.ts +0 -19
  117. package/dist/tui/components/CreateEnsembleWizard.js +0 -223
  118. package/dist/tui/components/DestroyConfirmModal.d.ts +0 -17
  119. package/dist/tui/components/DestroyConfirmModal.js +0 -62
  120. package/dist/tui/components/ErrorView.d.ts +0 -31
  121. package/dist/tui/components/ErrorView.js +0 -129
  122. package/dist/tui/components/HomeView.d.ts +0 -54
  123. package/dist/tui/components/HomeView.js +0 -306
  124. package/dist/tui/components/LoadLineupModal.d.ts +0 -18
  125. package/dist/tui/components/LoadLineupModal.js +0 -79
  126. package/dist/tui/components/MainView.d.ts +0 -21
  127. package/dist/tui/components/MainView.js +0 -107
  128. package/dist/tui/components/NewEnsembleModal.d.ts +0 -9
  129. package/dist/tui/components/NewEnsembleModal.js +0 -73
  130. package/dist/tui/components/Picker.d.ts +0 -23
  131. package/dist/tui/components/Picker.js +0 -70
  132. package/dist/tui/components/PlayerDetailView.d.ts +0 -26
  133. package/dist/tui/components/PlayerDetailView.js +0 -118
  134. package/dist/tui/components/PromptArea.d.ts +0 -50
  135. package/dist/tui/components/PromptArea.js +0 -303
  136. package/dist/tui/components/RecruitWizard.d.ts +0 -17
  137. package/dist/tui/components/RecruitWizard.js +0 -221
  138. package/dist/tui/components/RestoreConfirmModal.d.ts +0 -18
  139. package/dist/tui/components/RestoreConfirmModal.js +0 -71
  140. package/dist/tui/components/ScheduleWizard.d.ts +0 -19
  141. package/dist/tui/components/ScheduleWizard.js +0 -259
  142. package/dist/tui/components/Splash.d.ts +0 -23
  143. package/dist/tui/components/Splash.js +0 -221
  144. package/dist/tui/components/StatusBar.d.ts +0 -48
  145. package/dist/tui/components/StatusBar.js +0 -128
  146. package/dist/tui/components/StatusOverlay.d.ts +0 -15
  147. package/dist/tui/components/StatusOverlay.js +0 -76
  148. package/dist/tui/components/TitleBar.d.ts +0 -10
  149. package/dist/tui/components/TitleBar.js +0 -21
  150. package/dist/tui/index.d.ts +0 -15
  151. package/dist/tui/index.js +0 -162
  152. package/dist/tui/ink-context.d.ts +0 -18
  153. package/dist/tui/ink-context.js +0 -59
  154. package/dist/tui/ink-loader.d.ts +0 -26
  155. package/dist/tui/ink-loader.js +0 -42
  156. package/dist/tui/removed-commands.d.ts +0 -9
  157. package/dist/tui/removed-commands.js +0 -22
  158. package/dist/tui/sse-handler.d.ts +0 -52
  159. package/dist/tui/sse-handler.js +0 -157
  160. package/dist/tui/store.d.ts +0 -598
  161. package/dist/tui/store.js +0 -753
  162. package/dist/tui/utils/format.d.ts +0 -56
  163. package/dist/tui/utils/format.js +0 -155
  164. package/dist/tui/utils/fullscreen.d.ts +0 -23
  165. package/dist/tui/utils/fullscreen.js +0 -71
  166. package/dist/tui/utils/history.d.ts +0 -10
  167. package/dist/tui/utils/history.js +0 -85
  168. package/dist/tui/utils/platform.d.ts +0 -45
  169. package/dist/tui/utils/platform.js +0 -258
  170. package/dist/tui/utils/theme.d.ts +0 -21
  171. package/dist/tui/utils/theme.js +0 -24
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-tempo-dashboard",
3
3
  "private": true,
4
- "version": "1.7.0",
4
+ "version": "2.0.0-beta.2",
5
5
  "type": "module",
6
6
  "description": "Web dashboard for agent-tempo. Bundled into the npm package; served by the daemon at /dashboard/*.",
7
7
  "scripts": {
@@ -12,9 +12,9 @@ export interface HardTerminateInput {
12
12
  playerName: string;
13
13
  /** Adapter type — controls PID-file lookup and expected process-name verification. */
14
14
  agent: AgentType;
15
- /** Session's workDir — used to locate the copilot bridge PID file (`<workDir>/logs/<playerName>.pid`). */
15
+ /** Session's workDir — carried on the activity input as a session attribute. */
16
16
  workDir: string;
17
- /** Optional explicit logDir override; falls back to `<workDir>/logs`. */
17
+ /** Optional explicit logDir override for the central bridge log path. */
18
18
  logDir?: string;
19
19
  }
20
20
  export interface HardTerminateResult {
@@ -29,7 +29,6 @@ exports.hardTerminateAttachment = hardTerminateAttachment;
29
29
  */
30
30
  const child_process_1 = require("child_process");
31
31
  const fs_1 = require("fs");
32
- const path_1 = require("path");
33
32
  const config_1 = require("../config");
34
33
  const constants_1 = require("../constants");
35
34
  const log = (...args) => console.error('[agent-tempo:hard-terminate]', ...args);
@@ -38,19 +37,14 @@ const log = (...args) => console.error('[agent-tempo:hard-terminate]', ...args);
38
37
  * what happened so callers can record it in workflow history without blocking state flips.
39
38
  */
40
39
  async function hardTerminateAttachment(input) {
41
- const { ensemble, playerName, agent, workDir, logDir } = input;
40
+ const { ensemble, playerName, agent, logDir } = input;
42
41
  const notes = [];
43
42
  const killedPids = [];
44
43
  log(`hardTerminate start — ensemble=${ensemble} player=${playerName} agent=${agent}`);
45
44
  // ── Copilot bridge: PID file is authoritative ──
46
45
  if (agent === 'copilot') {
47
- // #690 — pid lives at the CENTRAL ~/.agent-tempo/logs/<ensemble>/ path now.
48
- // Transitional (one version, v1.6.2) READ-ONLY fallback to the legacy per-cwd
49
- // <workDir>/logs so we can still find+kill an orphan from a pre-upgrade spawn.
50
- // TODO(v1.7): drop the legacy fallback.
51
- const centralPid = (0, config_1.bridgeLogPaths)(ensemble, playerName, logDir).pidPath;
52
- const legacyPid = (0, path_1.join)(logDir || (0, path_1.join)(workDir, 'logs'), `${playerName}.pid`);
53
- const pidPath = (0, fs_1.existsSync)(centralPid) ? centralPid : legacyPid;
46
+ // #690 — pid lives at the CENTRAL ~/.agent-tempo/logs/<ensemble>/ path.
47
+ const pidPath = (0, config_1.bridgeLogPaths)(ensemble, playerName, logDir).pidPath;
54
48
  if ((0, fs_1.existsSync)(pidPath)) {
55
49
  try {
56
50
  const pidStr = (0, fs_1.readFileSync)(pidPath, 'utf8').trim();
@@ -86,19 +86,14 @@ export interface MaestroActivityOptions {
86
86
  /** Activity interface — used by proxyActivities in the Maestro workflow. */
87
87
  export interface MaestroActivities {
88
88
  /**
89
- * Legacy V1 refresh the `costProfile: 'local'` path AND the replay
90
- * path for every maestro started before #748.
91
- * TODO(next major, #748): remove once the minimum deployment age
92
- * exceeds the longest-lived pre-#748 maestro history (they replay V1);
93
- * remove together with the V1 branch in workflows/maestro.ts.
94
- */
95
- refreshEnsembleState(ensemble: string): Promise<MaestroPlayerInfo[]>;
96
- /**
97
- * T0.1 (#748) — additive V2: called only by maestros started with
98
- * `costProfile: 'cloud'` in their input. SA/memo-based ensemble-scoped
99
- * scan + in-process observer presence for workflow-side cadence
100
- * stretching. V1 stays for in-flight pre-#748 maestros (replay safety)
101
- * and the local profile.
89
+ * Ensemble-scoped player scan. Returns the player rows plus `observersPresent`
90
+ * (SSE subscriber presence) for workflow-side cadence stretching on the cloud
91
+ * profile. Honors the daemon's cost profile internally: cloud → SA/memo
92
+ * ensemble-scoped scan, local the legacy visibility scan.
93
+ *
94
+ * 2.0 (#788): the only refresh activity — the former V1 `refreshEnsembleState`
95
+ * was removed; both profiles call this (the A2 cutover means no pre-#748
96
+ * maestro survives to need the V1 replay path).
102
97
  */
103
98
  refreshEnsembleStateV2(input: {
104
99
  ensemble: string;
@@ -30,6 +30,9 @@ function createMaestroActivities(client, opts = {}) {
30
30
  // tempo bucket can diff across refreshes.
31
31
  activityCount: s.activityCount,
32
32
  lastActivityAt: s.lastActivityAt,
33
+ // #886 slice 2 — carry the degraded flag through to the wire so the
34
+ // dashboard/board can render an uncertain row instead of dropping it.
35
+ ...(s.degraded ? { degraded: true } : {}),
33
36
  });
34
37
  // #753 — attribute every Temporal call made by these activities (however
35
38
  // deep, e.g. scanEnsembleSessions → queryHandleWithTimeout) to the maestro.
@@ -55,16 +58,6 @@ function createMaestroActivities(client, opts = {}) {
55
58
  }
56
59
  };
57
60
  return (0, action_counters_1.tagActionSource)(maestroSource, {
58
- async refreshEnsembleState(ensemble) {
59
- try {
60
- const sessions = await (0, resolve_1.scanEnsembleSessions)(client, ensemble);
61
- return sessions.map(toPlayerInfo(ensemble));
62
- }
63
- catch (err) {
64
- log('refreshEnsembleState failed:', err);
65
- throw activity_1.ApplicationFailure.nonRetryable(`Failed to scan ensemble: ${err instanceof Error ? err.message : String(err)}`);
66
- }
67
- },
68
61
  async refreshEnsembleStateV2(input) {
69
62
  try {
70
63
  // Honor the DAEMON's configured profile for the scan strategy: a
@@ -292,6 +292,18 @@ function createOutboxActivities(client, config, ingestTokens, doorbells) {
292
292
  // session workflow and dispatch path can resolve the adapter descriptor
293
293
  // from the registry without falling back to the legacy agentType field.
294
294
  adapterId: adapters_1.registry.resolveFromAgentType(agent),
295
+ // #704 — resolve the adapter descriptor's dialog-blocking property at
296
+ // spawn time and thread it onto durable metadata so the session workflow
297
+ // can ARM/DISARM the booting watchdog structurally (no agentType
298
+ // hardcode). Headless adapters omit it ⇒ falsy ⇒ ARMED; interactive
299
+ // `claude-code` sets it true ⇒ disarmed until #890.
300
+ canBlockOnDialog: adapters_1.registry.get(adapters_1.registry.resolveFromAgentType(agent))?.canBlockOnDialog === true,
301
+ // #704 — optional env override for the booting deadline (workflows
302
+ // can't read env). Only set when a valid positive integer is provided.
303
+ ...(() => {
304
+ const ms = Number(process.env[config_2.ENV.BOOTING_DEADLINE_MS]);
305
+ return Number.isFinite(ms) && ms > 0 ? { bootingDeadlineMs: ms } : {};
306
+ })(),
295
307
  sessionId,
296
308
  // #131 / #449 Phase C — persist the claude-api / opencode model
297
309
  // on durable metadata so restart can recover the original choice
@@ -764,8 +776,9 @@ function createOutboxActivities(client, config, ingestTokens, doorbells) {
764
776
  /**
765
777
  * D14 `deliverReset` — set a `pendingReset` flag on the target via
766
778
  * `setPendingResetSignal` (a signal, like deliverCue — NOT a direct
767
- * subprocess call). The Pi extension polls `pendingResetQuery`, performs the
768
- * clean-wipe (`newSession`), then clears it via `ackResetSignal(resetId)`.
779
+ * subprocess call). The Pi extension reads the pending reset off the
780
+ * combined `pendingIntake` query, performs the clean-wipe (`newSession`),
781
+ * then clears it via `ackResetSignal(resetId)`.
769
782
  */
770
783
  async deliverReset(input) {
771
784
  const { ensemble, targetPlayerId, resetId, fresh, reason, requestedBy } = input;
@@ -892,6 +905,7 @@ function createOutboxActivities(client, config, ingestTokens, doorbells) {
892
905
  adapterId,
893
906
  adapterClass,
894
907
  leaseMs: validation_1.DEFAULT_RESTART_LEASE_MS,
908
+ protocolVersion: constants_1.PROTOCOL_VERSION, // #786 — 2.0 wire handshake
895
909
  }],
896
910
  });
897
911
  // Step 5 — context seed (saved state and/or transcript replay).
@@ -87,6 +87,17 @@ export interface EnsembleSessionInfo {
87
87
  * session. Undefined when the session predates W2 or the query failed.
88
88
  */
89
89
  lastActivityAt?: string;
90
+ /**
91
+ * #886 slice 2 — `true` when this row was produced from a DEGRADED scan:
92
+ * the visibility list returned the workflow, but extracting its observation
93
+ * fields threw, so the rich metadata (part/workDir/hostname/…) is a
94
+ * best-effort blank rather than authoritative. The row's IDENTITY
95
+ * (`workflowId` + best-effort `playerId`) is preserved so the player stays
96
+ * in the roster marked uncertain — instead of being silently dropped, which
97
+ * reads as "player absent" and causes roster flapping (contra #777). Absent
98
+ * / `false` on every healthy row.
99
+ */
100
+ degraded?: boolean;
90
101
  }
91
102
  /**
92
103
  * T0.1 (#748) — cloud-profile ensemble scan. Observation path ONLY (see the
@@ -96,13 +107,13 @@ export interface EnsembleSessionInfo {
96
107
  * - The visibility query is **ensemble-scoped** via the `AgentTempoEnsemble`
97
108
  * filter SA — no more cluster-wide list + per-session `getMetadata`
98
109
  * pre-filtering (the unfiltered scan was the dominant idle-burn driver).
99
- * - For v1.8+ runs (memo-complete: `AgentTempoWorkDir` present), the entire
100
- * player row is read from the list result (SAs + memo) — **zero**
101
- * per-player queries except the BPM `getActivityState` query, which is
102
- * intentionally kept per the architect's ruling (deriving BPM from phase
103
- * transitions would change the metric's meaning).
104
- * - Pre-v1.8 runs (no observation memo) fall back to the legacy per-player
105
- * `getMetadata` + `getPart` queries cost shrinks as old runs cycle out.
110
+ * - The entire player row is read from the list result (SAs + memo)
111
+ * **zero** per-player queries except the BPM `getActivityState` query,
112
+ * which is intentionally kept per the architect's ruling (deriving BPM
113
+ * from phase transitions would change the metric's meaning). Every 2.0
114
+ * session seeds the observation memo at workflow start (T0.5 #747 /
115
+ * #757), so `AgentTempoWorkDir` is always present; the pre-v1.8 memo-
116
+ * absent per-player `getMetadata`/`getPart` fallback was removed in #874.
106
117
  *
107
118
  * Staleness: SA/memo reads are eventually consistent (tens of seconds worst
108
119
  * case under backlog) — acceptable for the observation path per the design
@@ -160,13 +160,13 @@ async function resolveByDerivedId(client, ensemble, playerName) {
160
160
  * - The visibility query is **ensemble-scoped** via the `AgentTempoEnsemble`
161
161
  * filter SA — no more cluster-wide list + per-session `getMetadata`
162
162
  * pre-filtering (the unfiltered scan was the dominant idle-burn driver).
163
- * - For v1.8+ runs (memo-complete: `AgentTempoWorkDir` present), the entire
164
- * player row is read from the list result (SAs + memo) — **zero**
165
- * per-player queries except the BPM `getActivityState` query, which is
166
- * intentionally kept per the architect's ruling (deriving BPM from phase
167
- * transitions would change the metric's meaning).
168
- * - Pre-v1.8 runs (no observation memo) fall back to the legacy per-player
169
- * `getMetadata` + `getPart` queries cost shrinks as old runs cycle out.
163
+ * - The entire player row is read from the list result (SAs + memo)
164
+ * **zero** per-player queries except the BPM `getActivityState` query,
165
+ * which is intentionally kept per the architect's ruling (deriving BPM
166
+ * from phase transitions would change the metric's meaning). Every 2.0
167
+ * session seeds the observation memo at workflow start (T0.5 #747 /
168
+ * #757), so `AgentTempoWorkDir` is always present; the pre-v1.8 memo-
169
+ * absent per-player `getMetadata`/`getPart` fallback was removed in #874.
170
170
  *
171
171
  * Staleness: SA/memo reads are eventually consistent (tens of seconds worst
172
172
  * case under backlog) — acceptable for the observation path per the design
@@ -179,52 +179,64 @@ async function scanEnsembleSessionsCloud(client, ensemble, log = () => { }) {
179
179
  try {
180
180
  for await (const workflow of (0, visibility_deadline_1.iterateWithDeadline)(client.workflow.list({ query }), visibility_deadline_1.VISIBILITY_DEADLINES_MS.scanEnsembleSessions, 'scanEnsembleSessionsCloud')) {
181
181
  try {
182
- const handle = client.workflow.getHandle(workflow.workflowId);
183
182
  const phase = (0, search_attributes_1.getAttachmentPhase)(workflow);
184
183
  const playerId = (0, search_attributes_1.getSearchAttrString)(workflow, 'AgentTempoPlayerId') ?? workflow.workflowId;
185
184
  const hostname = (0, search_attributes_1.getSearchAttrString)(workflow, 'AgentTempoHostname') ?? '';
186
- // v1.8-memo-observation-fields runs carry workDir on the memo use
187
- // its presence as the "memo-complete row" discriminator.
188
- const workDir = (0, search_attributes_1.getMemoString)(workflow, search_attributes_1.MEMO_KEYS.workDir);
189
- let row;
190
- if (workDir !== undefined) {
191
- row = {
192
- playerId,
193
- part: (0, search_attributes_1.getPart)(workflow) ?? '',
194
- hostname,
195
- workDir,
196
- gitRoot: (0, search_attributes_1.getWorkflowMetaString)(workflow, search_attributes_1.MEMO_KEYS.gitRoot),
197
- gitBranch: (0, search_attributes_1.getMemoString)(workflow, search_attributes_1.MEMO_KEYS.gitBranch),
198
- isConductor: (0, search_attributes_1.getIsConductor)(workflow)
199
- ?? (workflow.workflowId?.endsWith('-conductor') ?? false),
200
- agentType: (0, search_attributes_1.getMemoString)(workflow, search_attributes_1.MEMO_KEYS.agentType) || 'claude',
201
- playerType: (0, search_attributes_1.getPlayerType)(workflow),
202
- };
203
- }
204
- else {
205
- // Legacy run (pre-v1.8 memo) — per-player query fallback, bounded
206
- // (#433). Same two queries the legacy scan used.
207
- const metadata = await (0, query_timeout_1.queryHandleWithTimeout)(handle, 'getMetadata');
208
- const part = await (0, query_timeout_1.queryHandleWithTimeout)(handle, 'getPart');
209
- row = {
210
- playerId: metadata.playerId,
211
- part,
212
- hostname: metadata.hostname,
213
- workDir: metadata.workDir,
214
- gitRoot: metadata.gitRoot,
215
- gitBranch: metadata.gitBranch,
216
- isConductor: metadata.isConductor,
217
- agentType: metadata.agentType || 'claude',
218
- playerType: metadata.playerType,
219
- };
220
- }
185
+ // Every 2.0 session seeds the observation memo at workflow start
186
+ // (T0.5 #747 / #757), so the entire player row is read straight from
187
+ // the visibility list result — SAs + memo, zero per-player queries
188
+ // (the parallel BPM `getActivityState` query below aside). The
189
+ // pre-v1.8 memo-absent `getMetadata`/`getPart` fallback was removed
190
+ // in #874 (dead under the 2.0 clean cutover).
191
+ const row = {
192
+ playerId,
193
+ part: (0, search_attributes_1.getPart)(workflow) ?? '',
194
+ hostname,
195
+ workDir: (0, search_attributes_1.getMemoString)(workflow, search_attributes_1.MEMO_KEYS.workDir) ?? '',
196
+ gitRoot: (0, search_attributes_1.getWorkflowMetaString)(workflow, search_attributes_1.MEMO_KEYS.gitRoot),
197
+ gitBranch: (0, search_attributes_1.getMemoString)(workflow, search_attributes_1.MEMO_KEYS.gitBranch),
198
+ isConductor: (0, search_attributes_1.getIsConductor)(workflow)
199
+ ?? (workflow.workflowId?.endsWith('-conductor') ?? false),
200
+ agentType: (0, search_attributes_1.getMemoString)(workflow, search_attributes_1.MEMO_KEYS.agentType) || 'claude',
201
+ playerType: (0, search_attributes_1.getPlayerType)(workflow),
202
+ };
221
203
  // BPM fields filled in below — kept out of the enumeration loop so
222
204
  // per-player query latency can't eat the visibility deadline.
223
205
  sessions.push({ workflowId: workflow.workflowId, ...row, phase });
224
206
  }
225
- catch {
226
- // Workflow may have just completed, or a legacy-fallback query timed
227
- // out (#433) skip this row; the next tick fills it in.
207
+ catch (rowErr) {
208
+ // #886 slice 2 observation extraction threw for this workflow. The
209
+ // OLD behaviour silently dropped the row, which makes a transient
210
+ // failure read as "player absent" → roster flapping (contra #777). The
211
+ // workflow IS in the visibility list, so the player exists; emit a
212
+ // DEGRADED row that preserves identity (workflowId + best-effort
213
+ // playerId) and flags `degraded: true`, instead of dropping it.
214
+ //
215
+ // playerId is salvaged in its own guard so a throwing getter can't
216
+ // escape to the outer (visibility-timeout-only) catch and fail the
217
+ // whole scan; it falls back to the workflowId. Identity preservation
218
+ // is the point — a row keyed on the real playerId avoids the
219
+ // remove+add churn that dropping (or re-keying on workflowId) causes.
220
+ let salvagedPlayerId = workflow.workflowId;
221
+ try {
222
+ salvagedPlayerId =
223
+ (0, search_attributes_1.getSearchAttrString)(workflow, 'AgentTempoPlayerId') ?? workflow.workflowId;
224
+ }
225
+ catch {
226
+ /* keep workflowId */
227
+ }
228
+ sessions.push({
229
+ workflowId: workflow.workflowId,
230
+ playerId: salvagedPlayerId,
231
+ part: '',
232
+ hostname: '',
233
+ workDir: '',
234
+ isConductor: workflow.workflowId.endsWith('-conductor'),
235
+ agentType: 'claude',
236
+ degraded: true,
237
+ });
238
+ log(`scanEnsembleSessionsCloud: degraded row for ${workflow.workflowId} ` +
239
+ `(observation extraction failed: ${rowErr instanceof Error ? rowErr.message : String(rowErr)})`);
228
240
  }
229
241
  }
230
242
  }
@@ -6,6 +6,7 @@ exports.installProcessLifecycleTelemetry = installProcessLifecycleTelemetry;
6
6
  exports._resetProcessLifecycleTelemetryForTest = _resetProcessLifecycleTelemetryForTest;
7
7
  exports._liveAdaptersForTest = _liveAdaptersForTest;
8
8
  const signals_1 = require("../workflows/signals");
9
+ const constants_1 = require("../constants");
9
10
  const terminal_error_1 = require("./terminal-error");
10
11
  const action_counters_1 = require("../utils/action-counters");
11
12
  const log = (...args) => console.error('[agent-tempo:adapter]', ...args);
@@ -389,6 +390,7 @@ class BaseAttachment {
389
390
  adapterId: this.descriptor.adapterId,
390
391
  adapterClass: this.descriptor.adapterClass,
391
392
  leaseMs: 3 * this.descriptor.heartbeatMs,
393
+ protocolVersion: constants_1.PROTOCOL_VERSION, // #786 — 2.0 wire handshake
392
394
  ...(expectedAttachmentId ? { expectedAttachmentId } : {}),
393
395
  }],
394
396
  });
@@ -1147,6 +1149,7 @@ class BaseAttachment {
1147
1149
  adapterId: this.descriptor.adapterId,
1148
1150
  adapterClass: this.descriptor.adapterClass,
1149
1151
  leaseMs: 3 * this.descriptor.heartbeatMs,
1152
+ protocolVersion: constants_1.PROTOCOL_VERSION, // #786 — 2.0 wire handshake
1150
1153
  }],
1151
1154
  });
1152
1155
  // Success — rebuild pinned handle from the NEW runId and hand it to the subclass.
@@ -22,6 +22,13 @@ exports.claudeCodeDescriptor = {
22
22
  // Interactive class — 60s cadence per design §4.3. The base class drives the
23
23
  // heartbeat loop at this interval when the V2 lifecycle path is active.
24
24
  heartbeatMs: 60_000,
25
+ // #704 — the interactive spawn passes `--dangerously-load-development-channels`,
26
+ // which shows a BLOCKING dev-channels dialog that requires a human click and
27
+ // can park the session pre-attach. So the booting attach-timeout watchdog must
28
+ // NOT arm for this adapter (an operator-away false-kill is worse than the hang).
29
+ // The 6 headless adapters omit this (⇒ falsy ⇒ armed). Flip to remove once #890
30
+ // dissolves the dialog. See docs/design/704-is-demo-companion-brief.md §2.
31
+ canBlockOnDialog: true,
25
32
  };
26
33
  // #749: one family of backoff curves across the codebase — this poller's
27
34
  // error backoff shares the SDK idle-backoff constants (values unchanged:
@@ -55,6 +55,7 @@ const config_1 = require("../config");
55
55
  const spawn_1 = require("../spawn");
56
56
  const probe_1 = require("../pi/probe");
57
57
  const install_1 = require("../pi/install");
58
+ const constants_1 = require("../constants");
58
59
  const out = __importStar(require("./output"));
59
60
  /**
60
61
  * Public entry point — invoked by the CLI dispatcher. Launches the board in a
@@ -95,6 +96,14 @@ async function commandCenterCommand(args) {
95
96
  process.exit(1);
96
97
  }
97
98
  }
99
+ // #791 — surface the three access tiers up front so the subscription-friendly
100
+ // posture is discoverable: the board + operator controls need NO auth on a
101
+ // local daemon; only the LLM planner needs a Claude subscription (`/login`,
102
+ // zero API key) or an API key. Wording is the single source of truth in
103
+ // constants.ts (mirrored into docs/cli.md, drift-guarded).
104
+ out.log(out.dim(' Access tiers (the board works with no auth — only the LLM planner needs a key/login):'));
105
+ for (const line of (0, constants_1.commandCenterAccessTierLines)())
106
+ out.log(out.dim(line));
98
107
  // Admin (T3) token — mission-control's operator write/gate surface reads it.
99
108
  // #54: a LOCAL (loopback) daemon grants full trust tokenless, so a tokenless
100
109
  // board is fully functional locally; only a REMOTE / 0.0.0.0 daemon requires the
@@ -105,8 +114,13 @@ async function commandCenterCommand(args) {
105
114
  out.log(out.dim(` ${config_1.ENV.HTTP_ADMIN_TOKEN} not set — fine for a local (loopback) daemon (full trust). ` +
106
115
  'Set it only if this board drives a remote / 0.0.0.0 daemon.'));
107
116
  }
117
+ // #791 — NOT a warning: a missing API key is an expected, fully-supported
118
+ // state. The board + operator controls work regardless; point at the `/login`
119
+ // zero-API-key planner path (Claude Pro/Max OAuth) rather than implying a
120
+ // degraded fallback.
108
121
  if (!process.env.ANTHROPIC_API_KEY) {
109
- out.warn('ANTHROPIC_API_KEY is not set — the Pi command-center will fall back to Pi\'s own auth/default model.');
122
+ out.log(out.dim(' ANTHROPIC_API_KEY not set — the board + operator controls still work. ' +
123
+ 'Run /login inside the board to enable the LLM planner on your Claude subscription (zero API key).'));
110
124
  }
111
125
  const temporalEnvVars = {
112
126
  [config_1.ENV.TEMPORAL_ADDRESS]: config.temporalAddress,
@@ -50,6 +50,14 @@ interface UpOpts extends CliOverrides {
50
50
  * can't exist there anyway (gate 3 in recruit pre-flight).
51
51
  */
52
52
  scenario?: string;
53
+ /**
54
+ * #786 — `up --from-upgrade`. Recreate protocol-2 workflows from the
55
+ * `upgrade-snapshot-v1.json` left by `upgrade-to-2`, instead of starting a
56
+ * fresh ensemble. Recreates the roster (structural essentials only),
57
+ * surfaces undelivered cues for review, and archives the snapshot on
58
+ * success. Ignores `--lineup` / conductor-spawn flags.
59
+ */
60
+ fromUpgrade?: boolean;
53
61
  }
54
62
  /**
55
63
  * #832 — the operator-facing "ensemble ready" lines for `up` / `conduct`.
@@ -1060,6 +1060,71 @@ function formatEnsembleReadyLines(ensemble, lineupName, playerCount) {
1060
1060
  `Connect: agent-tempo command-center ${ensemble}`,
1061
1061
  ];
1062
1062
  }
1063
+ /**
1064
+ * #786 — `up --from-upgrade` orchestration. Opens a Temporal client, recreates
1065
+ * protocol-2 workflows from `upgrade-snapshot-v1.json`, prints the outcome
1066
+ * (incl. undelivered cues surfaced for review — NEVER redelivered), and
1067
+ * archives the snapshot on success. Separated from `up()` so the I/O is in one
1068
+ * place; the recreation logic itself lives (testably) in
1069
+ * `src/upgrade/from-upgrade.ts`.
1070
+ */
1071
+ async function runFromUpgradeFlow(config) {
1072
+ const { runFromUpgrade } = await Promise.resolve().then(() => __importStar(require('../upgrade/from-upgrade')));
1073
+ const { SnapshotValidationError } = await Promise.resolve().then(() => __importStar(require('../upgrade/snapshot-v1')));
1074
+ out.heading('agent-tempo up --from-upgrade');
1075
+ const connection = await (0, connection_1.createTemporalConnection)(config);
1076
+ const client = new client_1.Client({ connection, namespace: config.temporalNamespace });
1077
+ try {
1078
+ const result = await runFromUpgrade(client, {
1079
+ home: config_1.AGENT_TEMPO_HOME,
1080
+ taskQueue: config.taskQueue,
1081
+ temporalAddress: config.temporalAddress,
1082
+ temporalNamespace: config.temporalNamespace,
1083
+ log: (...args) => out.dim(' ' + args.map(String).join(' ')),
1084
+ });
1085
+ if (!result.snapshotFound) {
1086
+ out.warn(`No upgrade snapshot found in ${config_1.AGENT_TEMPO_HOME}.`);
1087
+ out.log(` Run \`agent-tempo upgrade-to-2\` on the 1.x install first.`);
1088
+ return;
1089
+ }
1090
+ out.success(`Recreated ${result.workflowsCreated} protocol-2 workflow(s) across ` +
1091
+ `${result.ensemblesRecreated} ensemble(s).`);
1092
+ if (result.undeliveredCues.length > 0) {
1093
+ out.log('');
1094
+ out.warn(`${result.undeliveredCues.length} undelivered cue(s) — review (NOT redelivered):`);
1095
+ for (const c of result.undeliveredCues) {
1096
+ const preview = c.text.length > 120 ? c.text.slice(0, 117) + '…' : c.text;
1097
+ out.log(` [${c.ensemble}] ${c.from} → ${c.playerId} (${c.timestamp}): ${preview}`);
1098
+ }
1099
+ }
1100
+ if (result.failures.length > 0) {
1101
+ out.log('');
1102
+ out.error(`${result.failures.length} workflow(s) failed to recreate — snapshot left in place for retry:`);
1103
+ for (const f of result.failures)
1104
+ out.log(` ${f.workflowId}: ${f.error}`);
1105
+ process.exitCode = 1;
1106
+ return;
1107
+ }
1108
+ if (result.archived) {
1109
+ out.dim(` Snapshot archived → ${result.archivePath}`);
1110
+ }
1111
+ out.log('');
1112
+ out.log(' Continuity seeding (state slots / schedules / sessionId / model) is a');
1113
+ out.log(' tracked #786 fast-follow — recreated sessions are structural skeletons.');
1114
+ }
1115
+ catch (err) {
1116
+ // A malformed snapshot is a LOUD failure (never a silent skip).
1117
+ if (err instanceof SnapshotValidationError) {
1118
+ out.error(`Upgrade snapshot is corrupt — refusing to proceed: ${err.message}`);
1119
+ process.exitCode = 1;
1120
+ return;
1121
+ }
1122
+ throw err;
1123
+ }
1124
+ finally {
1125
+ await connection.close().catch(() => { });
1126
+ }
1127
+ }
1063
1128
  async function up(opts) {
1064
1129
  const config = (0, config_1.getConfig)(opts);
1065
1130
  // #689 — best-effort sweep of stale 0600 secret env files (residual from a shell
@@ -1111,6 +1176,16 @@ async function up(opts) {
1111
1176
  out.log(` ${out.dim('You can start it manually: agent-tempo daemon start')}`);
1112
1177
  process.exit(1);
1113
1178
  }
1179
+ // #786 — `up --from-upgrade`: recreate protocol-2 workflows from the snapshot
1180
+ // left by `upgrade-to-2`, then stop. Runs AFTER ensureInfra (Temporal + daemon
1181
+ // up, so the recreated workflows have live workers AND the daemon's boot guard
1182
+ // has already verified a clean cutover) but instead of the lineup/conductor
1183
+ // spawn flow. Continuity seeding (#334 state / schedules / sessionId / model)
1184
+ // is a tracked #786 fast-follow — this MVP recreates structural skeletons only.
1185
+ if (opts.fromUpgrade) {
1186
+ await runFromUpgradeFlow(config);
1187
+ return;
1188
+ }
1114
1189
  // Step 4: Register MCP server if needed
1115
1190
  if ((0, mcp_1.isMcpConfigured)(process.cwd())) {
1116
1191
  out.check('MCP configured', true);
@@ -1907,12 +1982,8 @@ async function down(opts) {
1907
1982
  * Returns a formatted string like " (pid 12345)" or "" if no PID file found.
1908
1983
  */
1909
1984
  function getBridgePidInfo(ensemble, name) {
1910
- // #690 — pid lives at the CENTRAL ~/.agent-tempo/logs/<ensemble>/ path; transitional
1911
- // READ-ONLY fallback to the legacy per-cwd ./logs for a pre-upgrade bridge.
1912
- // TODO(v1.7): drop the legacy fallback.
1913
- const centralPid = (0, config_1.bridgeLogPaths)(ensemble, name).pidPath;
1914
- const legacyPid = (0, path_1.join)(process.cwd(), 'logs', `${name}.pid`);
1915
- const pidPath = (0, fs_1.existsSync)(centralPid) ? centralPid : legacyPid;
1985
+ // #690 — pid lives at the CENTRAL ~/.agent-tempo/logs/<ensemble>/ path.
1986
+ const pidPath = (0, config_1.bridgeLogPaths)(ensemble, name).pidPath;
1916
1987
  if (!(0, fs_1.existsSync)(pidPath))
1917
1988
  return '';
1918
1989
  try {
@@ -1946,13 +2017,10 @@ function getBridgePidInfo(ensemble, name) {
1946
2017
  * — do NOT reuse this global scan-all from a scoped op: it would kill OTHER live
1947
2018
  * ensembles' bridges. The param is intentionally NOT added now (no caller needs it
1948
2019
  * = speculative; backlogged per the architect's deviation ruling).
1949
- *
1950
- * Plus a transitional READ of the legacy per-cwd `./logs` for a pre-upgrade
1951
- * bridge. TODO(v1.7): drop the legacy `./logs` dir.
1952
2020
  */
1953
2021
  function killBridgeProcesses() {
1954
2022
  const centralRoot = (0, config_1.bridgeLogsRoot)();
1955
- const dirs = [(0, path_1.join)(process.cwd(), 'logs')]; // legacy (transitional)
2023
+ const dirs = [];
1956
2024
  try {
1957
2025
  for (const ent of (0, fs_1.readdirSync)(centralRoot, { withFileTypes: true })) {
1958
2026
  if (ent.isDirectory())
@@ -1960,7 +2028,7 @@ function killBridgeProcesses() {
1960
2028
  }
1961
2029
  }
1962
2030
  catch {
1963
- // no central logs root yet — nothing central to scan
2031
+ // no central logs root yet — nothing to scan
1964
2032
  }
1965
2033
  for (const logsDir of dirs) {
1966
2034
  if (!(0, fs_1.existsSync)(logsDir))
@@ -371,28 +371,20 @@ async function daemonInstall() {
371
371
  async function daemonUninstall() {
372
372
  const platform = process.platform;
373
373
  if (platform === 'linux') {
374
- // Backward-compat: clean up either the new (`agent-tempo.service`) or
375
- // legacy (`agent-tempo.service`) unit so v0.x installs migrate cleanly.
376
374
  const dirSystemd = (0, path_1.join)((0, os_1.homedir)(), '.config', 'systemd', 'user');
377
- const dstNew = (0, path_1.join)(dirSystemd, 'agent-tempo.service');
378
- const dstLegacy = (0, path_1.join)(dirSystemd, 'agent-tempo.service');
379
- for (const svc of ['agent-tempo', 'agent-tempo']) {
380
- try {
381
- (0, child_process_1.execFileSync)('systemctl', ['--user', 'disable', '--now', svc], { stdio: 'ignore' });
382
- }
383
- catch { /* may not be running */ }
375
+ const dst = (0, path_1.join)(dirSystemd, 'agent-tempo.service');
376
+ try {
377
+ (0, child_process_1.execFileSync)('systemctl', ['--user', 'disable', '--now', 'agent-tempo'], { stdio: 'ignore' });
384
378
  }
385
- let removed = false;
379
+ catch { /* may not be running */ }
386
380
  try {
387
- for (const dst of [dstNew, dstLegacy]) {
388
- if ((0, fs_1.existsSync)(dst)) {
389
- (0, fs_1.unlinkSync)(dst);
390
- out.success(`Removed ${dst}`);
391
- removed = true;
392
- }
381
+ if ((0, fs_1.existsSync)(dst)) {
382
+ (0, fs_1.unlinkSync)(dst);
383
+ out.success(`Removed ${dst}`);
393
384
  }
394
- if (!removed)
385
+ else {
395
386
  out.log('No systemd unit file found.');
387
+ }
396
388
  }
397
389
  catch (err) {
398
390
  out.error(`Failed to remove systemd unit: ${err?.message ?? err}`);
@@ -401,24 +393,18 @@ async function daemonUninstall() {
401
393
  return;
402
394
  }
403
395
  if (platform === 'darwin') {
404
- // Backward-compat: clean up either the new (`com.agent.tempo.plist`) or
405
- // legacy (`com.claude.tempo.plist`) plist so v0.x installs migrate cleanly.
406
- const dstNew = (0, path_1.join)((0, os_1.homedir)(), 'Library', 'LaunchAgents', 'com.agent.tempo.plist');
407
- const dstLegacy = (0, path_1.join)((0, os_1.homedir)(), 'Library', 'LaunchAgents', 'com.claude.tempo.plist');
408
- let removed = false;
409
- for (const dst of [dstNew, dstLegacy]) {
410
- try {
411
- (0, child_process_1.execFileSync)('launchctl', ['unload', dst], { stdio: 'ignore' });
412
- }
413
- catch { /* may not be loaded */ }
414
- if ((0, fs_1.existsSync)(dst)) {
415
- (0, fs_1.unlinkSync)(dst);
416
- out.success(`Removed ${dst}`);
417
- removed = true;
418
- }
396
+ const dst = (0, path_1.join)((0, os_1.homedir)(), 'Library', 'LaunchAgents', 'com.agent.tempo.plist');
397
+ try {
398
+ (0, child_process_1.execFileSync)('launchctl', ['unload', dst], { stdio: 'ignore' });
419
399
  }
420
- if (!removed)
400
+ catch { /* may not be loaded */ }
401
+ if ((0, fs_1.existsSync)(dst)) {
402
+ (0, fs_1.unlinkSync)(dst);
403
+ out.success(`Removed ${dst}`);
404
+ }
405
+ else {
421
406
  out.log('No launchd plist found.');
407
+ }
422
408
  return;
423
409
  }
424
410
  if (platform === 'win32') {