claude-tempo 0.26.0 → 0.28.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 (125) hide show
  1. package/CLAUDE.md +10 -2
  2. package/README.md +21 -21
  3. package/dist/activities/outbox.js +20 -12
  4. package/dist/adapters/base.d.ts +84 -0
  5. package/dist/adapters/base.js +345 -20
  6. package/dist/cli/commands.d.ts +21 -73
  7. package/dist/cli/commands.js +200 -646
  8. package/dist/cli/daemon-command.d.ts +4 -0
  9. package/dist/cli/daemon-command.js +102 -1
  10. package/dist/cli/daemon.d.ts +31 -3
  11. package/dist/cli/daemon.js +58 -16
  12. package/dist/cli/help-text.js +47 -47
  13. package/dist/cli/removed-verbs.d.ts +9 -0
  14. package/dist/cli/removed-verbs.js +75 -0
  15. package/dist/cli/startup.d.ts +103 -0
  16. package/dist/cli/startup.js +615 -0
  17. package/dist/cli.js +54 -170
  18. package/dist/client/core.d.ts +24 -0
  19. package/dist/client/core.js +1033 -0
  20. package/dist/client/ensure-conductor-spawned.d.ts +35 -0
  21. package/dist/client/ensure-conductor-spawned.js +48 -0
  22. package/dist/client/index.d.ts +29 -7
  23. package/dist/client/index.js +15 -583
  24. package/dist/client/interface.d.ts +258 -4
  25. package/dist/client/subscribe.d.ts +108 -0
  26. package/dist/client/subscribe.js +590 -0
  27. package/dist/client/with-spawn.d.ts +27 -0
  28. package/dist/client/with-spawn.js +87 -0
  29. package/dist/config.d.ts +27 -0
  30. package/dist/config.js +57 -7
  31. package/dist/daemon.d.ts +19 -0
  32. package/dist/daemon.js +133 -77
  33. package/dist/ensemble/loader.js +9 -0
  34. package/dist/ensemble/saver.js +3 -1
  35. package/dist/ensemble/schema.d.ts +7 -1
  36. package/dist/http/aggregate.d.ts +161 -0
  37. package/dist/http/aggregate.js +466 -0
  38. package/dist/http/auth.d.ts +67 -0
  39. package/dist/http/auth.js +177 -0
  40. package/dist/http/cors.d.ts +42 -0
  41. package/dist/http/cors.js +111 -0
  42. package/dist/http/event-bus.d.ts +217 -0
  43. package/dist/http/event-bus.js +365 -0
  44. package/dist/http/event-id.d.ts +77 -0
  45. package/dist/http/event-id.js +117 -0
  46. package/dist/http/event-types.d.ts +280 -0
  47. package/dist/http/event-types.js +36 -0
  48. package/dist/http/index.d.ts +21 -0
  49. package/dist/http/index.js +61 -0
  50. package/dist/http/port-file.d.ts +22 -0
  51. package/dist/http/port-file.js +132 -0
  52. package/dist/http/responses.d.ts +27 -0
  53. package/dist/http/responses.js +40 -0
  54. package/dist/http/ring-buffer.d.ts +41 -0
  55. package/dist/http/ring-buffer.js +80 -0
  56. package/dist/http/server.d.ts +101 -0
  57. package/dist/http/server.js +368 -0
  58. package/dist/http/snapshot.d.ts +51 -0
  59. package/dist/http/snapshot.js +109 -0
  60. package/dist/http/sse-handler.d.ts +75 -0
  61. package/dist/http/sse-handler.js +276 -0
  62. package/dist/reconcile/orphans.d.ts +131 -2
  63. package/dist/reconcile/orphans.js +207 -10
  64. package/dist/server.js +32 -11
  65. package/dist/tools/destroy.d.ts +1 -1
  66. package/dist/tools/destroy.js +167 -24
  67. package/dist/tools/{pause-ensemble.d.ts → pause.d.ts} +1 -1
  68. package/dist/tools/pause.js +36 -0
  69. package/dist/tools/{resume-ensemble.d.ts → play.d.ts} +1 -1
  70. package/dist/tools/play.js +57 -0
  71. package/dist/tools/restore.d.ts +4 -0
  72. package/dist/tools/restore.js +107 -0
  73. package/dist/tools/shutdown.d.ts +4 -0
  74. package/dist/tools/shutdown.js +54 -0
  75. package/dist/tui/App.d.ts +63 -0
  76. package/dist/tui/App.js +619 -221
  77. package/dist/tui/bootstrap-types.d.ts +46 -0
  78. package/dist/tui/bootstrap-types.js +7 -0
  79. package/dist/tui/commands.d.ts +48 -17
  80. package/dist/tui/commands.js +383 -197
  81. package/dist/tui/components/DestroyConfirmModal.d.ts +17 -0
  82. package/dist/tui/components/DestroyConfirmModal.js +62 -0
  83. package/dist/tui/components/HomeView.d.ts +54 -0
  84. package/dist/tui/components/HomeView.js +306 -0
  85. package/dist/tui/components/LoadLineupModal.d.ts +18 -0
  86. package/dist/tui/components/LoadLineupModal.js +79 -0
  87. package/dist/tui/components/NewEnsembleModal.d.ts +9 -0
  88. package/dist/tui/components/NewEnsembleModal.js +73 -0
  89. package/dist/tui/components/PromptArea.js +9 -12
  90. package/dist/tui/components/RestoreConfirmModal.d.ts +18 -0
  91. package/dist/tui/components/RestoreConfirmModal.js +71 -0
  92. package/dist/tui/components/StatusBar.d.ts +34 -1
  93. package/dist/tui/components/StatusBar.js +59 -14
  94. package/dist/tui/index.d.ts +8 -0
  95. package/dist/tui/index.js +21 -0
  96. package/dist/tui/removed-commands.d.ts +9 -0
  97. package/dist/tui/removed-commands.js +22 -0
  98. package/dist/tui/sse-handler.d.ts +52 -0
  99. package/dist/tui/sse-handler.js +159 -0
  100. package/dist/tui/store.d.ts +168 -2
  101. package/dist/tui/store.js +213 -3
  102. package/dist/tui/utils/format.d.ts +19 -0
  103. package/dist/tui/utils/format.js +17 -0
  104. package/dist/tui/utils/history.d.ts +0 -5
  105. package/dist/tui/utils/history.js +0 -17
  106. package/dist/tui/utils/platform.d.ts +0 -11
  107. package/dist/tui/utils/platform.js +0 -37
  108. package/dist/utils/ensemble-ops.d.ts +61 -0
  109. package/dist/utils/ensemble-ops.js +77 -0
  110. package/dist/utils/hosts.d.ts +32 -8
  111. package/dist/utils/hosts.js +52 -21
  112. package/dist/workflows/maestro-signals.d.ts +21 -0
  113. package/dist/workflows/maestro-signals.js +19 -1
  114. package/dist/workflows/maestro.js +7 -0
  115. package/dist/workflows/session.js +41 -4
  116. package/package.json +6 -2
  117. package/workflow-bundle.js +68 -6
  118. package/dist/tools/detach.d.ts +0 -4
  119. package/dist/tools/detach.js +0 -45
  120. package/dist/tools/pause-ensemble.js +0 -58
  121. package/dist/tools/resume-ensemble.js +0 -79
  122. package/dist/tui/components/CommandOverlay.d.ts +0 -15
  123. package/dist/tui/components/CommandOverlay.js +0 -34
  124. package/dist/tui/components/ScheduleOverlay.d.ts +0 -13
  125. package/dist/tui/components/ScheduleOverlay.js +0 -113
package/CLAUDE.md CHANGED
@@ -28,6 +28,8 @@ src/
28
28
  │ ├── mcp.ts # MCP server registration helpers (init, global vs project)
29
29
  │ ├── output.ts # Shared CLI output formatting helpers
30
30
  │ ├── preflight.ts # Environment preflight checks
31
+ │ ├── removed-verbs.ts # lookup table for the 10 CLI verbs removed in #288 — dispatches migration hints before loading Temporal surface
32
+ │ ├── startup.ts # auto-provisioning bootstrap state machine (#289) — six-step idempotent sequence used by bare `claude-tempo` invocation
31
33
  │ └── upgrade-command.ts # upgrade subcommand — crash-proof; dynamic-imports Temporal only for active-session warning
32
34
  ├── adapters/
33
35
  │ ├── README.md # Adapter contract documentation
@@ -69,12 +71,11 @@ src/
69
71
  │ ├── worktree.ts / stage.ts / stages.ts / cancel-stage.ts
70
72
  │ ├── load-lineup.ts / save-lineup.ts / agent-types.ts / resolve.ts
71
73
  │ ├── set-name.ts / set-part.ts / who-am-i.ts / release.ts
72
- │ ├── pause-ensemble.ts / resume-ensemble.ts
74
+ │ ├── pause.ts / play.ts / shutdown.ts / restore.ts
73
75
  │ ├── hosts.ts
74
76
  │ └── helpers.ts # Zod/MCP tool registration wrapper
75
77
  ├── tui/
76
78
  │ ├── App.tsx / store.ts / commands.ts # TUI root, state, slash commands
77
- │ ├── client.ts # Backward-compat shim → src/client/
78
79
  │ ├── components/ # Ink components — see docs/tui.md for inventory
79
80
  │ └── utils/ # format, platform, theme, fullscreen, history
80
81
  ├── utils/
@@ -106,6 +107,13 @@ npm test
106
107
  > surveys or migrations, always grep **both** `test/` and `tests/` or you will miss
107
108
  > mocks and assertions that only live in one directory.
108
109
 
110
+ > **Test-only hooks live with the module they reset and follow the
111
+ > `__<verb><Noun>ForTests` naming convention** — see
112
+ > [docs/adr/0006-test-hooks-naming.md](docs/adr/0006-test-hooks-naming.md). The
113
+ > double-underscore prefix telegraphs "test escape hatch, do not call from
114
+ > production code"; the hook's doc-comment should restate that explicitly. Hooks
115
+ > are never surfaced through barrels or `TempoClient`.
116
+
109
117
  See [docs/development.md](docs/development.md) for full setup (Temporal dev server command,
110
118
  daemon worker notes, `npx ts-node` dev runner).
111
119
 
package/README.md CHANGED
@@ -59,11 +59,10 @@ claude-tempo up
59
59
  This starts Temporal, registers the MCP server, launches the daemon, and opens a conductor session. Then add players:
60
60
 
61
61
  ```bash
62
- claude-tempo start # open a player session
63
62
  claude-tempo status # see who's active
64
63
  ```
65
64
 
66
- Or ask the conductor to `recruit` players from inside Claude Code.
65
+ Or use the TUI to recruit players, or ask the conductor to `recruit` from inside Claude Code.
67
66
 
68
67
  ### Manual setup
69
68
 
@@ -71,8 +70,7 @@ Or ask the conductor to `recruit` players from inside Claude Code.
71
70
  claude-tempo server # start Temporal dev server
72
71
  claude-tempo init # register MCP server globally
73
72
  claude-tempo preflight # verify environment
74
- claude-tempo conduct # start a conductor
75
- claude-tempo start # start a player
73
+ claude-tempo up # launch conductor via auto-provisioning
76
74
  ```
77
75
 
78
76
  ## Upgrading
@@ -90,12 +88,15 @@ claude-tempo upgrade 0.22.0
90
88
  ## Stopping & Tear Down
91
89
 
92
90
  ```bash
93
- # Stop a specific player session
94
- claude-tempo stop my-ensemble player-name
91
+ # Terminate all sessions in an ensemble
92
+ claude-tempo destroy my-ensemble
95
93
 
96
94
  # Tear down everything (all sessions, schedulers, and Maestro workflows)
97
95
  claude-tempo down --all
98
96
 
97
+ # Tear down and terminate all workflows in one step
98
+ claude-tempo down --destroy -y
99
+
99
100
  # Stop the background daemon
100
101
  claude-tempo daemon stop
101
102
  ```
@@ -107,17 +108,17 @@ claude-tempo daemon stop
107
108
  ## Core Concepts
108
109
 
109
110
  - **Player** — A Claude Code session registered as a Temporal workflow
110
- - **Conductor** — An optional orchestration hub (one per ensemble); receives `report` calls and connects to external interfaces
111
+ - **Conductor** — Required orchestration hub (one per ensemble); receives `report` calls and connects to external interfaces. Lineup schema enforces its presence.
111
112
  - **Ensemble** — A named group of players isolated from other ensembles; defaults to `default`
112
113
  - **Cue** — A message sent to a player by name via Temporal signal
113
114
  - **Lineup** — A YAML file that defines a full team and recruits them in one step
114
115
  - **Player Type** — A reusable agent definition (`.md` with YAML frontmatter) that gives a player a named role
115
116
 
116
- Players in one ensemble cannot see or message players in another:
117
+ Players in one ensemble cannot see or message players in another. Launch `claude-tempo` to open the TUI and switch between ensembles, or target a specific ensemble directly:
117
118
 
118
119
  ```bash
119
- claude-tempo conduct frontend # conduct the "frontend" ensemble
120
- claude-tempo start backend # join the "backend" ensemble
120
+ claude-tempo up frontend # provision and launch conductor in "frontend"
121
+ claude-tempo up backend # provision and launch conductor in "backend"
121
122
  ```
122
123
 
123
124
  ## MCP Tools
@@ -139,17 +140,16 @@ Tools available inside Claude Code sessions connected to claude-tempo:
139
140
  ## CLI
140
141
 
141
142
  ```bash
142
- claude-tempo up [ensemble] # first-time setup
143
- claude-tempo conduct [ensemble] # start a conductor
144
- claude-tempo start [ensemble] # start a player
143
+ claude-tempo # launch TUI (auto-provisions on first run)
144
+ claude-tempo up [ensemble] # provision infrastructure and launch conductor
145
+ claude-tempo down [--destroy] # tear down infrastructure (--destroy also terminates workflows)
145
146
  claude-tempo status [ensemble] # list active sessions
147
+ claude-tempo destroy <ensemble> # terminate all sessions in an ensemble
148
+ claude-tempo restore <ensemble> # restore orphaned sessions on this host
146
149
  claude-tempo hosts # list daemons polling this Temporal namespace (--all/--json)
147
- claude-tempo recall <name> # read a player's message history (--limit/--offset/--preview/--from/--since/--include-sent/--json)
150
+ claude-tempo recall <name> # read a player's message history (--limit/--offset/--preview/--json)
148
151
  claude-tempo attachment-info <name> # inspect a session's phase, holder, lease, and heartbeat age
149
152
  claude-tempo release [ensemble] # release held players (unlock + deliver tasks)
150
- claude-tempo pause [ensemble] # pause all sessions and the scheduler
151
- claude-tempo resume [ensemble] # resume a paused ensemble (--release also releases held players)
152
- claude-tempo tui # open the terminal UI
153
153
  claude-tempo daemon <sub> # manage the worker daemon
154
154
  claude-tempo upgrade # update to latest
155
155
  ```
@@ -240,7 +240,7 @@ claude-tempo tui --ensemble my-ensemble # direct ensemble mode
240
240
  The TUI provides a chat-focused shell for managing your ensemble:
241
241
 
242
242
  - **Ensemble chat feed** — live aggregated view of conductor + player traffic; type bare text to message the conductor, `@player message` to message directly
243
- - **Slash commands** — `/recruit`, `/status`, `/schedule`, `/gates`, `/stages`, `/worktree`, `/go` (release held), `/pause`, `/resume`, and more; type `/help` for the full list
243
+ - **Slash commands** — `/recruit`, `/status`, `/schedule`, `/gates`, `/stages`, `/worktree`, `/go` (release held), `/pause`, `/play`, `/shutdown`, `/restore`, `/home`, and more; type `/help` for the full list
244
244
  - **Interactive overlays and wizards** — step-by-step flows for recruiting players, creating schedules, and managing ensembles
245
245
 
246
246
  📖 [TUI reference → docs/tui.md](docs/tui.md)
@@ -249,10 +249,10 @@ The TUI provides a chat-focused shell for managing your ensemble:
249
249
 
250
250
  > **Experimental** — subject to breaking changes.
251
251
 
252
- GitHub Copilot CLI sessions can join an ensemble using `--agent copilot`:
252
+ GitHub Copilot CLI sessions can join an ensemble using `--agent copilot`. Recruit one from the TUI:
253
253
 
254
- ```bash
255
- claude-tempo start myband --agent copilot -n copilot-1
254
+ ```
255
+ /recruit copilot-1 --agent copilot
256
256
  ```
257
257
 
258
258
  📖 [Copilot bridge setup and limitations → docs/copilot.md](docs/copilot.md)
@@ -575,15 +575,22 @@ function createOutboxActivities(client, config) {
575
575
  // already exists at `~/.claude/projects/<encoded-path>/<uuid>.jsonl`
576
576
  // ("Session ID already in use"). A prior failed spawn can leave that
577
577
  // file behind, wedging every subsequent `fresh` restart that reuses the
578
- // stored sessionId. Since `fresh` already skips `--resume`, we mint a
579
- // new UUID and persist it on the target's metadata so later non-fresh
580
- // restarts resume against the new transcript. Non-fresh restarts keep
581
- // the stored sessionId for deterministic `--resume`.
582
- let spawnSessionId = metadata.sessionId;
583
- if (fresh) {
584
- spawnSessionId = crypto.randomUUID();
585
- await handle.signal(signals_1.updateMetadataSignal, { sessionId: spawnSessionId });
586
- }
578
+ // stored sessionId.
579
+ //
580
+ // #306: `/restart` ALWAYS spawns a fresh Claude Code process — never
581
+ // `--resume <id>`. The transcript `.jsonl` for the prior `spawnSessionId`
582
+ // is NOT guaranteed to have been flushed to disk before the prior
583
+ // adapter was hard-terminated (Windows `taskkill /T /F` is synchronous
584
+ // and unconditional). Claude Code then errors out with "No conversation
585
+ // found with session ID" and the new terminal drops to shell.
586
+ //
587
+ // Context preservation is already handled by the Step 5 replay above —
588
+ // we re-send recent messages to the fresh session. That's authoritative;
589
+ // the session-id `--resume` path was only ever a bonus on top. So we
590
+ // mint a new UUID on every restart, persist it to metadata, and never
591
+ // pass `resume: true` to the spawn.
592
+ const spawnSessionId = crypto.randomUUID();
593
+ await handle.signal(signals_1.updateMetadataSignal, { sessionId: spawnSessionId });
587
594
  // Issue #184: re-resolve on the invoker host against the session's
588
595
  // workDir (NOT the daemon's process.cwd — daemon runs elsewhere than
589
596
  // the session's project, so the project-tier lookup needs the session's
@@ -598,8 +605,9 @@ function createOutboxActivities(client, config) {
598
605
  host: targetHost,
599
606
  attachmentId: token.attachmentId,
600
607
  runId: token.runId,
601
- resume: !fresh,
602
- ...(spawnSessionId ? { sessionId: spawnSessionId } : {}),
608
+ // #306: `/restart` is always a fresh spawn (see comment above).
609
+ resume: false,
610
+ sessionId: spawnSessionId,
603
611
  adapterId,
604
612
  ...(resolved ? {
605
613
  agentDefinition: resolved.name,
@@ -608,7 +616,7 @@ function createOutboxActivities(client, config) {
608
616
  } : {}),
609
617
  }],
610
618
  });
611
- log(`Restart prepared for "${targetPlayerId}" — attachmentId=${token.attachmentId}, spawnEntryId=${spawnEntryId}, host=${targetHost}${fresh ? ` (fresh sessionId=${spawnSessionId})` : ''}`);
619
+ log(`Restart prepared for "${targetPlayerId}" — attachmentId=${token.attachmentId}, spawnEntryId=${spawnEntryId}, host=${targetHost}, fresh sessionId=${spawnSessionId}${fresh ? ' (context replay skipped)' : ''}`);
612
620
  return { success: true };
613
621
  }
614
622
  catch (err) {
@@ -14,6 +14,35 @@
14
14
  */
15
15
  import type { Client, WorkflowHandle } from '@temporalio/client';
16
16
  import type { AdapterClass, AdapterDescriptor, AttachmentToken, AttachmentPhase, DetachReason } from '../types';
17
+ /** Snapshot of adapter state included in every telemetry frame. */
18
+ interface AdapterTelemetrySnapshot {
19
+ attachmentId: string | null;
20
+ workflowId: string | null;
21
+ runId: string | null;
22
+ heartbeatsSent: number;
23
+ phaseTicksDone: number;
24
+ }
25
+ /**
26
+ * Build the structured frame emitted by every lifecycle handler. Pure
27
+ * function — exposed for unit tests that don't want to spawn a child
28
+ * process.
29
+ */
30
+ export declare function buildProcessTerminatingFrame(signal: string, errorMessage?: string, snapshot?: AdapterTelemetrySnapshot[]): string;
31
+ /**
32
+ * Install the process-lifecycle telemetry handlers. Idempotent. Skipped
33
+ * by default in test environments (see {@link shouldInstallLifecycleTelemetry}).
34
+ *
35
+ * Production callers (and the first `startV2Lifecycle()` call on any
36
+ * adapter) invoke without arguments. Unit tests pass `{ force: true }`
37
+ * to bypass the env gate.
38
+ */
39
+ export declare function installProcessLifecycleTelemetry(opts?: {
40
+ force?: boolean;
41
+ }): void;
42
+ /** Test-only — uninstall handlers + reset state. */
43
+ export declare function _resetProcessLifecycleTelemetryForTest(): void;
44
+ /** Test-only — direct access to the live-adapter set. */
45
+ export declare function _liveAdaptersForTest(): ReadonlySet<BaseAttachment>;
17
46
  /**
18
47
  * Override bundle for the reconnect loop timing (#201). Production defaults are
19
48
  * tuned for laptop-sleep cycles (15-min elapsed budget, 10s base, 60s cap). Tests
@@ -124,6 +153,13 @@ export declare abstract class BaseAttachment {
124
153
  onPhaseChange(listener: (phase: AttachmentPhase) => void): () => void;
125
154
  /** Subscribe to lease-revocation events (§9.3 split-brain resolution). */
126
155
  onLeaseRevoked(listener: (reason: DetachReason) => void): () => void;
156
+ /**
157
+ * Hypothesis A telemetry — capture the adapter state included in
158
+ * process-lifecycle log frames. Public so the module-level
159
+ * `snapshotLiveAdapters()` helper can read private fields without an
160
+ * `any` cast; consumers other than the telemetry path should not call it.
161
+ */
162
+ _captureTelemetrySnapshot(): AdapterTelemetrySnapshot;
127
163
  /**
128
164
  * Subscribe to terminal events — `WorkflowNotFound` (§9.4) and phase `gone`.
129
165
  * Terminal fires at most once per instance. Subclasses stop delivery + exit.
@@ -216,7 +252,55 @@ export declare abstract class BaseAttachment {
216
252
  * once per terminal — not on every tick.
217
253
  */
218
254
  private findCanSuccessorRunId;
255
+ /**
256
+ * Fire the terminal hook — the adapter is going dark and won't recover.
257
+ *
258
+ * #258: emits a structured log line on every fire so the next post-CAN
259
+ * silence incident is unambiguous in logs. Pre-#258, a `fireTerminal`
260
+ * from an unexpected source (the root cause was a silent destroy from
261
+ * the reconnect-loop pre-check on a transient terminal-class error) was
262
+ * indistinguishable from process death in workflow history — both produced
263
+ * "no further heartbeats." The structured log includes:
264
+ *
265
+ * - `reason` — the existing DetachReason
266
+ * - `callsite` — the calling function or rationale (passed by every
267
+ * callsite so the source is grep-able without parsing stack traces)
268
+ * - `attachmentId` / `workflowId` / `runId` — for cross-referencing
269
+ * against workflow history when bisecting an incident
270
+ * - `heartbeatsSent` / `phaseTicksDone` — the existing #249 counters
271
+ * so an operator can correlate "loop alive at N heartbeats, then
272
+ * terminal fired at this callsite" without external context
273
+ *
274
+ * Idempotent — repeat calls (e.g. reconnect-exhausted re-fires after
275
+ * destroy) early-return without re-logging. The first fire wins.
276
+ */
219
277
  private fireTerminal;
278
+ /**
279
+ * #258 tiebreaker: confirm whether a workflow is genuinely terminal after
280
+ * the reconnect-loop pre-check threw a terminal-class error. Used to
281
+ * distinguish a real workflow-gone state from a transient gRPC /
282
+ * visibility-API blip that classified as terminal.
283
+ *
284
+ * Returns:
285
+ * - `{ kind: 'running', statusName }` — workflow is alive (any
286
+ * non-terminal status). Caller should treat the original error as
287
+ * transient and continue the reconnect loop.
288
+ * - `{ kind: 'terminal', statusName }` — workflow is in a terminal
289
+ * status (`COMPLETED` / `FAILED` / `CANCELLED` / `TERMINATED` /
290
+ * `CONTINUED_AS_NEW` / `TIMED_OUT`). Caller should fire destroy.
291
+ * - `{ kind: 'describe-threw' }` — `describe()` itself failed. Treat
292
+ * as terminal (fire destroy) — consistent with pre-#258 semantics
293
+ * when classification is ambiguous, and avoids spinning forever on
294
+ * a workflow we can't reach.
295
+ * - `{ kind: 'timed-out' }` — `describe()` exceeded
296
+ * {@link DESCRIBE_TIMEOUT_MS}. Treat as terminal (fire destroy) —
297
+ * same rationale: prefer clean shutdown to a hung loop.
298
+ *
299
+ * The unpinned handle follows any CAN chain to the latest run, so
300
+ * `desc.status.name === 'CONTINUED_AS_NEW'` here means the workflow
301
+ * id itself is closed (no successor) — genuinely terminal.
302
+ */
303
+ private confirmWorkflowTerminal;
220
304
  /**
221
305
  * Opt-in reconnect policy. Default: return `false` — the base class behaves
222
306
  * exactly as it did before #201 (fire terminal, tear down). Subclasses that