claude-tempo 0.26.0-beta.6 → 0.26.0

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.
package/CLAUDE.md CHANGED
@@ -70,6 +70,7 @@ src/
70
70
  │ ├── load-lineup.ts / save-lineup.ts / agent-types.ts / resolve.ts
71
71
  │ ├── set-name.ts / set-part.ts / who-am-i.ts / release.ts
72
72
  │ ├── pause-ensemble.ts / resume-ensemble.ts
73
+ │ ├── hosts.ts
73
74
  │ └── helpers.ts # Zod/MCP tool registration wrapper
74
75
  ├── tui/
75
76
  │ ├── App.tsx / store.ts / commands.ts # TUI root, state, slash commands
@@ -78,6 +79,8 @@ src/
78
79
  │ └── utils/ # format, platform, theme, fullscreen, history
79
80
  ├── utils/
80
81
  │ ├── validation.ts / worktree.ts / safe-path.ts / duration.ts / search-attributes.ts
82
+ │ ├── attachment-format.ts / recall-format.ts # Shared display formatters (attachment-info, recall)
83
+ │ ├── hosts.ts / format-hosts.ts # Host enumeration + shared hosts display formatter (#274)
81
84
  ├── types.ts # Shared type definitions
82
85
  ├── git-info.ts # Git repository detection helper
83
86
  └── config.ts # Env var handling
@@ -118,11 +121,12 @@ daemon worker notes, `npx ts-node` dev runner).
118
121
  - **Outbox**: Outbound requests (cue, report, recruit, restart, detach, destroy, …) go through the session's workflow outbox instead of directly signaling other workflows. The dispatch loop processes entries via activities, decoupling tools from cross-workflow signaling.
119
122
  - **Attachment phase** (v0.26): Seven phases tracked on the session workflow — `booting → attached → processing | awaiting → draining → detached → gone`. The phase is authoritative for lifecycle truth: adapters drive it via `claimAttachment` / `adapterExited` / `forceDetach` / `destroy`, and the workflow publishes it on the `ClaudeTempoAttachmentState` search attribute. Replaced the v0.25 `ClaudeTempoStatus` heuristic (removed in v0.26). See [docs/concepts.md](docs/concepts.md) for the phase table and [docs/ops/v0.26-migration.md](docs/ops/v0.26-migration.md) for the upgrade path.
120
123
  - **Adapter heartbeat observability** (#249): After `claimAttachment`, the base adapter logs `first heartbeat scheduled in Xms` then `heartbeat#1 delivered` on the first tick. Every 10 ticks it emits `heartbeats-delivered=N / phase-ticks=N` breadcrumbs. Any silent guard trip in `tickHeartbeat` / `tickPhaseWatcher` now emits a structured `guard tripped: {stopped, reconnecting, …}` log instead of silently orphaning the timer. The phase-watcher emits `WARNING: heartbeat staleness` when `lastHeartbeatAt` falls more than 2× `heartbeatMs` behind `now`. Grep `[claude-tempo:adapter]` to confirm loop health without parsing Temporal history.
121
- - **Per-host task queues**: `host` param on `recruit`/`restart`/`migrate` routes to `claude-tempo-{hostname}` task queue. See [docs/concepts.md](docs/concepts.md) for cross-machine recruiting details.
124
+ - **Per-host task queues**: `host` param on `recruit`/`restart`/`migrate` routes to `claude-tempo-{hostname}` task queue. When `host` is set on `recruit`, a pre-flight check validates the target daemon is live and supports the requested agent type (`force: true` bypasses). Daemon boot profiles (hostname, platform, available player types) are advertised via the `hostProfile` signal and maintained in the global maestro's `hostProfiles` map — surfaced by the `hosts` MCP tool, `claude-tempo hosts` CLI, and `/hosts` TUI command (#274). See [docs/concepts.md](docs/concepts.md) for cross-machine recruiting details.
122
125
  - **Wire protocol**: All signal/query/update names are documented in [`docs/WIRE-PROTOCOL.md`](docs/WIRE-PROTOCOL.md) and are stable — renaming or removing any is a breaking change. **Process**: update `docs/WIRE-PROTOCOL.md` in the same commit as any new signal, query, or update.
123
126
  - **Daemon**: Standalone background process (`src/daemon.ts`) that runs all Temporal workers. Auto-started by any `claude-tempo` command. PID at `~/.claude-tempo/daemon.pid`; logs at `~/.claude-tempo/daemon.log`.
124
127
  - **Player types**: Reusable agent definitions in Claude Code's subagent format (`.md` files with YAML frontmatter). Three-tier lookup: project `.claude/agents/` → user `~/.claude/agents/` → shipped `examples/agents/`. Discover via `agent_types` tool or `claude-tempo agent-types` CLI. Shipped types: tempo-conductor, tempo-composer, tempo-soloist, tempo-tuner, tempo-critic, tempo-roadie, tempo-improv, tempo-liner.
125
128
  - **Lineup examples**: Four pre-built ensemble YAML files in `examples/ensembles/` — `tempo-big-band`, `tempo-dev-team`, `tempo-review-squad`, `tempo-jam-session`. Load with `claude-tempo up --lineup <name>` or the `load_lineup` tool.
129
+ - **GitHub App identity** (`claude-tempo[bot]`): When a player writes to GitHub — issue comments, PR creation/merge, commits, labels, check runs — **use `./scripts/ensemble-gh`** instead of `gh`. The wrapper mints a short-lived installation token so the action is attributed to `claude-tempo[bot]`, not to the human maintainer, making the AI authorship visible. Plain `gh` is still correct for read-only local dev (`gh pr view`, `gh repo clone`, `gh auth status`). Every bot-authored comment/PR body must include the AI attribution footer documented in [docs/github-app.md](docs/github-app.md).
126
130
 
127
131
  See [docs/concepts.md](docs/concepts.md) for the full glossary (Adapter, Attachment phases, Restart, Detach/Destroy, Migrate, Broadcast, Recall, Schedule, Lineup, Quality Gate, Worktree, Stage, Hold/Release, Pause/Resume, Maestro, TempoClient, and more).
128
132
 
package/README.md CHANGED
@@ -143,6 +143,7 @@ claude-tempo up [ensemble] # first-time setup
143
143
  claude-tempo conduct [ensemble] # start a conductor
144
144
  claude-tempo start [ensemble] # start a player
145
145
  claude-tempo status [ensemble] # list active sessions
146
+ claude-tempo hosts # list daemons polling this Temporal namespace (--all/--json)
146
147
  claude-tempo recall <name> # read a player's message history (--limit/--offset/--preview/--from/--since/--include-sent/--json)
147
148
  claude-tempo attachment-info <name> # inspect a session's phase, holder, lease, and heartbeat age
148
149
  claude-tempo release [ensemble] # release held players (unlock + deliver tasks)
Binary file
@@ -106,6 +106,30 @@ export declare function detach(opts: DetachCliOpts): Promise<void>;
106
106
  export declare function destroy(opts: DestroyCliOpts): Promise<void>;
107
107
  export declare function migrate(opts: MigrateCliOpts): Promise<void>;
108
108
  export declare function attachmentInfo(opts: VerbOpts): Promise<void>;
109
+ export interface HostsCliOpts extends CliOverrides {
110
+ ensemble?: string;
111
+ /** Include stale hosts (those not seen in the last minute). CLI default: false. */
112
+ all?: boolean;
113
+ /** Emit raw `HostInfo[]` JSON instead of the formatted table. */
114
+ json?: boolean;
115
+ }
116
+ export declare function hosts(opts: HostsCliOpts): Promise<void>;
117
+ export interface RefreshHostProfileCliOpts extends CliOverrides {
118
+ ensemble?: string;
119
+ /** Max wait for the new profile to appear in the maestro `hostProfiles` map. Default 10s. */
120
+ confirmTimeoutMs?: number;
121
+ }
122
+ /**
123
+ * #274 AC5d (M12) — manual re-signal of this host's profile to the
124
+ * global maestro. The daemon otherwise only re-signals on boot; this
125
+ * subcommand re-computes the profile + signals fresh.
126
+ *
127
+ * Exit semantics (per my implementation-time call to the conductor,
128
+ * approved): await ensureGlobalMaestro → signal → short poll on
129
+ * `hostProfiles()` to confirm the new version is visible → exit 0.
130
+ * Exits nonzero if the poll timeout elapses without confirmation.
131
+ */
132
+ export declare function refreshHostProfile(opts: RefreshHostProfileCliOpts): Promise<void>;
109
133
  export interface RecallCliOpts extends VerbOpts {
110
134
  limit?: number;
111
135
  offset?: number;
@@ -47,6 +47,8 @@ exports.detach = detach;
47
47
  exports.destroy = destroy;
48
48
  exports.migrate = migrate;
49
49
  exports.attachmentInfo = attachmentInfo;
50
+ exports.hosts = hosts;
51
+ exports.refreshHostProfile = refreshHostProfile;
50
52
  exports.recall = recall;
51
53
  exports.restore = restore;
52
54
  exports.ensembleCommand = ensembleCommand;
@@ -2128,6 +2130,83 @@ async function attachmentInfo(opts) {
2128
2130
  await connection.close();
2129
2131
  }
2130
2132
  }
2133
+ async function hosts(opts) {
2134
+ const { config, connection, client } = await verbClient(opts);
2135
+ try {
2136
+ const { listHosts } = await Promise.resolve().then(() => __importStar(require('../utils/hosts')));
2137
+ const { formatHostList } = await Promise.resolve().then(() => __importStar(require('../utils/format-hosts')));
2138
+ const list = await listHosts(client, {
2139
+ force: true, // CLI always bypasses the cache — freshness expectation is "right now".
2140
+ namespace: config.temporalNamespace,
2141
+ taskQueue: config.taskQueue,
2142
+ });
2143
+ if (opts.json) {
2144
+ process.stdout.write(JSON.stringify(list, null, 2) + '\n');
2145
+ }
2146
+ else {
2147
+ out.log(formatHostList(list, { includeStale: opts.all }));
2148
+ }
2149
+ }
2150
+ catch (err) {
2151
+ out.error(err?.message || String(err));
2152
+ process.exit(1);
2153
+ }
2154
+ finally {
2155
+ await connection.close();
2156
+ }
2157
+ }
2158
+ /**
2159
+ * #274 AC5d (M12) — manual re-signal of this host's profile to the
2160
+ * global maestro. The daemon otherwise only re-signals on boot; this
2161
+ * subcommand re-computes the profile + signals fresh.
2162
+ *
2163
+ * Exit semantics (per my implementation-time call to the conductor,
2164
+ * approved): await ensureGlobalMaestro → signal → short poll on
2165
+ * `hostProfiles()` to confirm the new version is visible → exit 0.
2166
+ * Exits nonzero if the poll timeout elapses without confirmation.
2167
+ */
2168
+ async function refreshHostProfile(opts) {
2169
+ const { config, connection, client } = await verbClient(opts);
2170
+ try {
2171
+ const { computeHostProfile, scrubHostProfile, advertiseHostProfile } = await Promise.resolve().then(() => __importStar(require('../daemon')));
2172
+ const { GLOBAL_MAESTRO_WORKFLOW_ID } = await Promise.resolve().then(() => __importStar(require('../config')));
2173
+ const profile = scrubHostProfile(computeHostProfile(config));
2174
+ const result = await advertiseHostProfile(client, profile, { log: (...a) => out.log(a.map(String).join(' ')) });
2175
+ if (!result.ok) {
2176
+ out.error(`hostProfile signal failed after ${result.attempts} attempts. Global Maestro may be unreachable.`);
2177
+ process.exit(1);
2178
+ }
2179
+ // Short confirmation poll — give the workflow a moment to apply the
2180
+ // signal and respond to the query with the fresh version. If the
2181
+ // maestro is absent entirely, the query will throw and we exit 1.
2182
+ const deadline = Date.now() + (opts.confirmTimeoutMs ?? 10_000);
2183
+ const target = profile.version;
2184
+ const handle = client.workflow.getHandle(GLOBAL_MAESTRO_WORKFLOW_ID);
2185
+ while (Date.now() < deadline) {
2186
+ try {
2187
+ const profiles = (await handle.query('hostProfiles'));
2188
+ const live = profiles[profile.hostname];
2189
+ if (live && live.version === target) {
2190
+ out.success(`Host profile for "${profile.hostname}" refreshed (version ${target}).`);
2191
+ return;
2192
+ }
2193
+ }
2194
+ catch {
2195
+ // retry until deadline
2196
+ }
2197
+ await new Promise((r) => setTimeout(r, 500));
2198
+ }
2199
+ out.error(`Signal sent but not yet reflected in hostProfiles() query after ${opts.confirmTimeoutMs ?? 10_000}ms. May succeed shortly; re-run to confirm.`);
2200
+ process.exit(1);
2201
+ }
2202
+ catch (err) {
2203
+ out.error(err?.message || String(err));
2204
+ process.exit(1);
2205
+ }
2206
+ finally {
2207
+ await connection.close();
2208
+ }
2209
+ }
2131
2210
  async function recall(opts) {
2132
2211
  const { config, connection, client } = await verbClient(opts);
2133
2212
  const ensemble = opts.ensemble || config.ensemble;
@@ -76,6 +76,8 @@ ${out.bold('Commands:')}
76
76
  ${out.cyan('migrate')} <name> --host Move a session to a different host
77
77
  ${out.cyan('attachment-info')} <name> Inspect the V2 attachment phase + current holder
78
78
  ${out.cyan('recall')} <name> Read a player's message history (--limit/--offset/--preview/--from/--since/--include-sent/--json)
79
+ ${out.cyan('hosts')} List daemons polling this Temporal namespace with advertised capabilities (--all/--json)
80
+ ${out.cyan('refresh-host-profile')} Re-advertise this daemon's capability profile to the global Maestro
79
81
  ${out.cyan('restore')} [name] Restore orphaned session(s) — interactive picker, or --all / --from-host / --dry-run
80
82
  ${out.cyan('release')} [ensemble] Release all held players (unlock outbox, deliver messages)
81
83
  ${out.cyan('pause')} [ensemble] Pause an ensemble (sessions, scheduler, maestro)
package/dist/cli.js CHANGED
@@ -186,13 +186,20 @@ function parseArgs(argv) {
186
186
  result.dryRun = true;
187
187
  }
188
188
  else if (arg === '--limit' && i + 1 < argv.length) {
189
- const n = Number(argv[++i]);
190
- if (Number.isInteger(n) && n >= 1)
191
- result.limit = n;
192
- else {
193
- out.error(`Invalid --limit: ${argv[i]}`);
189
+ const raw = argv[++i];
190
+ const n = Number(raw);
191
+ if (!Number.isInteger(n) || n < 1) {
192
+ out.error(`Invalid --limit: ${raw}`);
193
+ process.exit(1);
194
+ }
195
+ // #270: cap at 100 to match the MCP Zod schema. Recall queries load
196
+ // the full inbox/sent history from the workflow; the cap bounds the
197
+ // worst-case payload across every surface. Use `--offset` to page.
198
+ if (n > 100) {
199
+ out.error(`--limit exceeds max (100). Use --offset N to page through more results.`);
194
200
  process.exit(1);
195
201
  }
202
+ result.limit = n;
196
203
  }
197
204
  else if (arg === '--offset' && i + 1 < argv.length) {
198
205
  const n = Number(argv[++i]);
@@ -319,7 +326,7 @@ async function main() {
319
326
  return;
320
327
  }
321
328
  // All other commands: lazy-load the full command surface now.
322
- const { start, status, init, server, up, down, stop, ensembleCommand, agentTypesCommand, broadcast, release, pause, resume, restart, detach, destroy, migrate, attachmentInfo, recall, restore, } = await Promise.resolve().then(() => __importStar(require('./cli/commands')));
329
+ const { start, status, init, server, up, down, stop, ensembleCommand, agentTypesCommand, broadcast, release, pause, resume, restart, detach, destroy, migrate, attachmentInfo, recall, hosts, refreshHostProfile, restore, } = await Promise.resolve().then(() => __importStar(require('./cli/commands')));
323
330
  const ensemble = args.positional[1] || process.env[config_1.ENV.ENSEMBLE] || 'default';
324
331
  // Resolve the default agent from config (only needed for commands that use it)
325
332
  const resolvedAgent = () => args.agent ?? (0, config_1.getConfig)(overrides).defaultAgent;
@@ -498,6 +505,22 @@ async function main() {
498
505
  });
499
506
  break;
500
507
  }
508
+ case 'hosts': {
509
+ await hosts({
510
+ ensemble: args.ensemble || ensemble,
511
+ ...(args.all ? { all: true } : {}),
512
+ ...(args.json ? { json: true } : {}),
513
+ ...overrides,
514
+ });
515
+ break;
516
+ }
517
+ case 'refresh-host-profile': {
518
+ await refreshHostProfile({
519
+ ensemble: args.ensemble || ensemble,
520
+ ...overrides,
521
+ });
522
+ break;
523
+ }
501
524
  case 'recall': {
502
525
  // Positional player is required — matches the #128 design.
503
526
  const name = args.positional[1] || args.name;
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.createTempoClient = createTempoClient;
4
37
  /**
@@ -311,6 +344,15 @@ function createTempoClient(client) {
311
344
  throw new Error(`No session found with name "${playerId}" in ensemble "${ensemble}".`);
312
345
  return target.query(signals_1.attachmentInfoQuery);
313
346
  },
347
+ async listHosts(opts = {}) {
348
+ // Lazy import so this doesn't drag utils/hosts into every
349
+ // consumer of TempoClient at module-load time.
350
+ const { listHosts } = await Promise.resolve().then(() => __importStar(require('../utils/hosts')));
351
+ return listHosts(client, {
352
+ force: Boolean(opts.force),
353
+ namespace: client.options.namespace,
354
+ });
355
+ },
314
356
  async recall(ensemble, playerId) {
315
357
  // #128: direct session queries, no maestro round-trip. Throws rather
316
358
  // than returning empties so the CLI / TUI wrappers can surface a
@@ -4,7 +4,7 @@
4
4
  * Extracted from `src/tui/client.ts` so that non-TUI consumers (CLI, tests,
5
5
  * external integrations) can depend on the interface without pulling in Ink/React.
6
6
  */
7
- import type { MaestroPlayerInfo, MaestroRelayMessage, HistoryEntry, Message, SentMessage, SessionMetadata, ScheduleEntry, QualityGate, StageEntry, WorktreeEntry, EnsembleChatResult, AttachmentInfo } from '../types';
7
+ import type { MaestroPlayerInfo, MaestroRelayMessage, HistoryEntry, Message, SentMessage, SessionMetadata, ScheduleEntry, QualityGate, StageEntry, WorktreeEntry, EnsembleChatResult, AttachmentInfo, HostInfo } from '../types';
8
8
  /**
9
9
  * Raw (unfiltered, unsorted, unsliced) output of `TempoClient.recall`.
10
10
  * The shared formatter at `src/utils/recall-format.ts` turns this into a
@@ -79,6 +79,15 @@ export interface TempoClient {
79
79
  * slice / render; the client stays presentation-free.
80
80
  */
81
81
  recall(ensemble: string, playerId: string): Promise<RecallClientResult>;
82
+ /**
83
+ * #274: List all daemons polling this Temporal namespace, joined with
84
+ * their boot-signaled capability profiles. Consumers typically feed
85
+ * the result through `formatHostList` for a consistent UX across
86
+ * CLI / TUI / MCP. `force: true` bypasses the 3-second result cache.
87
+ */
88
+ listHosts(opts?: {
89
+ force?: boolean;
90
+ }): Promise<HostInfo[]>;
82
91
  /** Get active schedules for an ensemble. */
83
92
  getSchedules(ensemble: string): Promise<ScheduleEntry[]>;
84
93
  /** Cancel a named schedule in an ensemble. */
package/dist/daemon.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { Client } from '@temporalio/client';
3
- import { type DaemonConfig } from './config';
3
+ import { type Config, type DaemonConfig } from './config';
4
4
  import { type OrphanCandidate } from './reconcile/orphans';
5
+ import type { HostProfile } from './types';
5
6
  /**
6
7
  * Atomically write the daemon PID file via `writeFile(tmp) + rename(tmp, final)`.
7
8
  *
@@ -15,6 +16,105 @@ import { type OrphanCandidate } from './reconcile/orphans';
15
16
  * Exported for unit testing.
16
17
  */
17
18
  export declare function writePidFileAtomic(pidFilePath: string, pid: number): Promise<void>;
19
+ /**
20
+ * Build the daemon's capability profile from its config + runtime env.
21
+ * Result is NOT scrubbed — call `scrubHostProfile` before signaling.
22
+ *
23
+ * Exported for testability; production callers go through
24
+ * `runDaemonBoot(client, deps)` which provides this as the default
25
+ * `computeHostProfile` dep.
26
+ */
27
+ export declare function computeHostProfile(config: Config): HostProfile;
28
+ /**
29
+ * #274 AC5c / M10 — HARD REQUIREMENT privacy scrub.
30
+ *
31
+ * Strips absolute paths and file extensions from every `HostProfile` field
32
+ * before the payload crosses the signal boundary. The global maestro is
33
+ * namespace-wide; a multi-tenant or multi-ensemble corporate setup would
34
+ * leak username-containing paths across ensembles if this is ever
35
+ * violated. Unit-tested in `test/daemon-boot.test.ts` with a dedicated
36
+ * "no `/` or `\\` in any string" invariant assertion against pathological
37
+ * inputs.
38
+ *
39
+ * Contract per architect AC5c:
40
+ * - `claudeBin` — basename only (e.g. `claude`), never absolute
41
+ * - `availableAgentTypes` — names only, never paths
42
+ * - `availablePlayerTypes` — names only, never paths
43
+ * - No env var values, no `workDir`, no user directories in any field
44
+ *
45
+ * The scrub is defense-in-depth: production callers (`computeHostProfile`)
46
+ * already produce clean inputs from `listAgentTypes().map(a => a.name)`.
47
+ * If a future code path accidentally passes a path, this function catches
48
+ * it before the workflow handler ever sees it.
49
+ */
50
+ export declare function scrubHostProfile(raw: HostProfile): HostProfile;
51
+ /**
52
+ * Signal `hostProfile` with bounded retry (AC5b / M11).
53
+ *
54
+ * Default backoff: `[0, 5000, 15000]` ms → 3 attempts, ≤20 s wall-clock,
55
+ * well under the 30 s budget. Tests override to `[0, 0, 0]` for fast
56
+ * execution. On total failure, logs a warning and returns — the daemon
57
+ * stays alive without its profile advertised.
58
+ *
59
+ * Exported for reuse by the Phase 5 `claude-tempo refresh-host-profile`
60
+ * CLI subcommand, which re-signals without needing the full
61
+ * `runDaemonBoot` sequence (the global maestro is already up).
62
+ */
63
+ export declare function advertiseHostProfile(client: Client, profile: HostProfile, opts?: {
64
+ retryBackoffsMs?: number[];
65
+ log?: (...args: unknown[]) => void;
66
+ sendSignal?: (client: Client, profile: HostProfile) => Promise<void>;
67
+ }): Promise<{
68
+ ok: boolean;
69
+ attempts: number;
70
+ lastError?: unknown;
71
+ }>;
72
+ /**
73
+ * #274 M14 — boot-sequence deps. Injected at `runDaemonBoot` so the
74
+ * ordering + retry + scrub invariants are all unit-testable without
75
+ * subprocess fixtures. Production callers pass the default impls
76
+ * already exported from this module.
77
+ */
78
+ export interface DaemonBootDeps {
79
+ /** Ensure the global maestro workflow is running. Awaited before any signal. */
80
+ ensureGlobalMaestro: () => Promise<void>;
81
+ /** Signal the global maestro with the scrubbed host profile. */
82
+ sendHostProfileSignal: (client: Client, profile: HostProfile) => Promise<void>;
83
+ /**
84
+ * Compute the daemon's capability profile. Output is fed into
85
+ * `scrubHostProfile` before signaling — computeHostProfile itself does
86
+ * not need to scrub, the dep-swap pattern makes the pipeline testable.
87
+ */
88
+ computeHostProfile: () => HostProfile;
89
+ /**
90
+ * Retry backoffs for the `hostProfile` signal (ms). Production uses
91
+ * `[0, 5000, 15000]`; tests override to `[0, 0, 0]` for speed.
92
+ */
93
+ retryBackoffsMs?: number[];
94
+ /** Log sink. Tests stub to capture output. Defaults to module-level `log`. */
95
+ log?: (...args: unknown[]) => void;
96
+ }
97
+ /**
98
+ * #274 M14 — daemon boot sequence: ensure global maestro is running,
99
+ * then advertise the (scrubbed) capability profile with bounded retry.
100
+ *
101
+ * Ordering is load-bearing (AC5a / M11): the `hostProfile` signal MUST
102
+ * NOT fire until `ensureGlobalMaestro` has resolved. Otherwise the
103
+ * signal races the workflow-start and gets silently dropped by Temporal
104
+ * (WorkflowNotFound on an unknown workflow id).
105
+ *
106
+ * Hard-failure behavior (AC5b): if `ensureGlobalMaestro` rejects, the
107
+ * daemon stays alive WITHOUT advertising its profile. Next opportunity
108
+ * is the next daemon restart OR a manual `claude-tempo refresh-host-profile`
109
+ * invocation (Phase 5).
110
+ *
111
+ * Tests in `test/daemon-boot.test.ts` exercise:
112
+ * - ensure-before-signal ordering via deferred promises
113
+ * - retry success on 3rd attempt
114
+ * - all-retries-exhausted stays alive
115
+ * - ensure-fails-stays-alive
116
+ */
117
+ export declare function runDaemonBoot(client: Client, deps: DaemonBootDeps): Promise<void>;
18
118
  /**
19
119
  * PR-E reconcile-on-boot — design §10.1.
20
120
  *