@userreason/cli 0.2.0 → 0.2.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.
@@ -4,7 +4,9 @@ import { lstat, open, opendir, rename, rm } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { SetupError, setupPrivateDirectory, setupProjectFingerprint, withSetupProjectLock } from './setup-store.js';
6
6
  import { parseSetupCommandResultInput, parseSetupLocalPlan } from './setup-wire.js';
7
+ import { SetupHeartbeat, observeSetupClient, setupAbortable } from './setup-heartbeat.js';
7
8
  import { readSetupFile, setupDigest, setupHash, verifySetupFiles } from './setup-files.js';
9
+ import { sanitizeSetupDiagnostic } from './setup-diagnostics.js';
8
10
  const terminal = new Set(['complete', 'cancelled', 'expired']);
9
11
  const stopped = new Set(['ready_for_recommendations', 'analyzing', 'selecting_drafts', 'creating_drafts', 'agent_test']);
10
12
  const limit = 8_000_000;
@@ -93,7 +95,8 @@ function validateJournal(data, identity, active = true) {
93
95
  data.plan = validatePrepared(data.plan);
94
96
  for (const [operationId, receipt] of Object.entries(data.receipts)) {
95
97
  if (!uuid(operationId) || !receipt || typeof receipt !== 'object' || Array.isArray(receipt) ||
96
- Object.keys(receipt).some(key => !['input_digest', 'stage', 'outcome', 'outputs', 'request_id'].includes(key)) ||
98
+ Object.keys(receipt).some(key => !['input_digest', 'stage', 'outcome', 'diagnostic', 'outputs', 'request_id'].includes(key)) ||
99
+ (receipt.diagnostic !== undefined && (typeof receipt.diagnostic !== 'string' || receipt.diagnostic.length > 8000)) ||
97
100
  !/^[a-f0-9]{64}$/.test(receipt.input_digest) || !['claimed', 'running', 'settled'].includes(receipt.stage) || !uuid(receipt.request_id) ||
98
101
  (receipt.stage === 'settled') !== Boolean(receipt.outcome))
99
102
  throw new Error('invalid operation receipt');
@@ -179,151 +182,169 @@ async function verified(directory, operation, runtime, receipt) {
179
182
  return runtime.verify ? runtime.verify(operation) : operation.effect.kind !== 'install_packages';
180
183
  }
181
184
  /** Runs only after browser approval. Durable intents precede effects and durable outcomes precede reports. */
182
- export async function runSetupExecutor(client, directory, runtime, options = {}) {
185
+ export async function runSetupExecutor(client, directory, runtime, options = {}, sharedHeartbeat) {
183
186
  if (await setupProjectFingerprint(directory) !== client.identity.project_fingerprint)
184
187
  throw new SetupError('setup_binding_mismatch', 'This executor belongs to another project.');
185
- return withSetupProjectLock(directory, async () => {
186
- const path = await journalPath(client), journal = await loadJournal(path, client);
187
- let session = await client.poll(), polls = 0;
188
- const scope = { schema_version: 1, setup_id: client.identity.setup_id };
189
- const pause = async () => { if (options.sleep)
190
- await options.sleep(options.pollMs ?? 2000);
191
- else
192
- await new Promise((resolve, reject) => { const timer = setTimeout(done, options.pollMs ?? 2000); function done() { options.signal?.removeEventListener('abort', abort); resolve(); } function abort() { clearTimeout(timer); reject(options.signal?.reason); } options.signal?.addEventListener('abort', abort, { once: true }); }); };
193
- while (true) {
194
- options.signal?.throwIfAborted();
195
- options.onSession?.(session);
196
- if (terminal.has(session.phase) || stopped.has(session.phase) || options.maxPolls !== undefined && polls++ >= options.maxPolls) {
197
- if (session.phase === 'cancelled') {
198
- const known = session.commands.find(command => command.status === 'running' && journal.receipts[command.operation_id]?.outcome);
199
- if (known)
200
- session = await client.report({ ...scope, operation_id: known.operation_id, executor_generation: known.executor_generation, lease_generation: known.lease_generation, ...journal.receipts[known.operation_id].outcome });
188
+ const heartbeat = sharedHeartbeat ?? new SetupHeartbeat(client, options);
189
+ client = observeSetupClient(client, heartbeat, options.signal);
190
+ try {
191
+ return await withSetupProjectLock(directory, async () => {
192
+ const path = await journalPath(client), journal = await loadJournal(path, client);
193
+ const shownErrors = new Set();
194
+ const showError = (id, receipt) => {
195
+ if (!receipt.diagnostic || shownErrors.has(id))
196
+ return;
197
+ const operation = journal.plan?.operations.find(item => setupDigest(item.effect) === receipt.input_digest);
198
+ if (!operation)
199
+ return;
200
+ shownErrors.add(id);
201
+ try {
202
+ options.onOperationError?.({ kind: operation.effect.kind, message: sanitizeSetupDiagnostic(receipt.diagnostic), receiptPath: path });
201
203
  }
202
- return session;
203
- }
204
- if (session.phase === 'configuring') {
205
- if (!session.site_id || !session.workspace_id) {
206
- session = await client.heartbeat({ ...scope, executor_generation: session.executor_generation, operation_id: null, lease_generation: null });
204
+ catch { /* A display callback cannot prevent the durable result report. */ }
205
+ };
206
+ for (const [id, receipt] of Object.entries(journal.receipts))
207
+ showError(id, receipt);
208
+ let session = await client.poll(), polls = 0;
209
+ const scope = { schema_version: 1, setup_id: client.identity.setup_id };
210
+ const pause = async () => { if (options.sleep)
211
+ await options.sleep(options.pollMs ?? 2000);
212
+ else
213
+ await new Promise((resolve, reject) => { const timer = setTimeout(done, options.pollMs ?? 2000); function done() { options.signal?.removeEventListener('abort', abort); resolve(); } function abort() { clearTimeout(timer); reject(options.signal?.reason); } options.signal?.addEventListener('abort', abort, { once: true }); }); };
214
+ while (true) {
215
+ heartbeat.check();
216
+ session = heartbeat.latest(session);
217
+ options.onSession?.(session);
218
+ if (terminal.has(session.phase) || stopped.has(session.phase) || options.maxPolls !== undefined && polls++ >= options.maxPolls) {
219
+ if (session.phase === 'cancelled') {
220
+ const known = session.commands.find(command => command.status === 'running' && journal.receipts[command.operation_id]?.outcome);
221
+ if (known)
222
+ session = await client.report({ ...scope, operation_id: known.operation_id, executor_generation: known.executor_generation, lease_generation: known.lease_generation, ...journal.receipts[known.operation_id].outcome });
223
+ }
224
+ return session;
225
+ }
226
+ if (session.phase === 'configuring') {
227
+ if (!session.site_id || !session.workspace_id) {
228
+ await heartbeat.pulse();
229
+ heartbeat.check();
230
+ session = heartbeat.latest(session);
231
+ await pause();
232
+ session = await client.poll();
233
+ continue;
234
+ }
235
+ const targetDigest = setupDigest({ workspace_id: session.workspace_id, site_id: session.site_id, api_url: client.identity.api_url });
236
+ if (!journal.plan || journal.plan.wire.choices_digest !== setupDigest(session.choices) || journal.target_digest !== targetDigest) {
237
+ if (Object.keys(journal.receipts).length)
238
+ throw new SetupError('setup_plan_changed', 'A completed local plan cannot be replaced without a new setup.');
239
+ journal.plan = validatePrepared(await setupAbortable(runtime.prepare(session), heartbeat.signal));
240
+ journal.target_digest = targetDigest;
241
+ journal.plan_request_id = randomUUID();
242
+ await saveJournal(path, journal);
243
+ }
244
+ try {
245
+ session = await client.plan({ ...scope, request_id: journal.plan_request_id, expected_revision: session.revision, plan: journal.plan.wire });
246
+ }
247
+ catch (error) {
248
+ if (!(error instanceof SetupError) || error.code !== 'revision_conflict')
249
+ throw error;
250
+ session = await client.poll();
251
+ }
252
+ continue;
253
+ }
254
+ if (!journal.plan || !session.local_plan || setupDigest(session.local_plan) !== setupDigest(journal.plan.wire))
255
+ throw new SetupError('setup_plan_missing', 'The browser-approved local plan is unavailable on this computer. No effects were applied.');
256
+ if (session.phase !== 'applying') {
257
+ await heartbeat.pulse();
258
+ heartbeat.check();
259
+ session = heartbeat.latest(session);
207
260
  await pause();
208
261
  session = await client.poll();
209
262
  continue;
210
263
  }
211
- const targetDigest = setupDigest({ workspace_id: session.workspace_id, site_id: session.site_id, api_url: client.identity.api_url });
212
- if (!journal.plan || journal.plan.wire.choices_digest !== setupDigest(session.choices) || journal.target_digest !== targetDigest) {
213
- if (Object.keys(journal.receipts).length)
214
- throw new SetupError('setup_plan_changed', 'A completed local plan cannot be replaced without a new setup.');
215
- journal.plan = validatePrepared(await runtime.prepare(session));
216
- journal.target_digest = targetDigest;
217
- journal.plan_request_id = randomUUID();
264
+ const unknown = session.commands.find(command => command.status === 'outcome_unknown' || command.status === 'running' || command.status === 'leased' && journal.receipts[command.operation_id]);
265
+ if (unknown) {
266
+ const operation = journal.plan.operations.find(item => setupDigest(item.effect) === unknown.input_digest);
267
+ if (!operation)
268
+ throw new SetupError('setup_command_mismatch', 'The command does not match the local reviewed plan.');
269
+ const receipt = journal.receipts[unknown.operation_id];
270
+ // A running command belongs to an interrupted process. Wait for the server
271
+ // to close its lease; never issue the side effect a second time.
272
+ if (unknown.status !== 'outcome_unknown') {
273
+ await pause();
274
+ session = await client.poll();
275
+ continue;
276
+ }
277
+ const stillVerified = await verified(directory, operation, runtime, receipt);
278
+ const outcome = receipt?.outcome && receipt.outcome.status !== 'succeeded' ? receipt.outcome : stillVerified
279
+ ? { status: 'succeeded', result: { code: 'already_present', verified: true } }
280
+ : { status: 'manual', result: { code: receipt?.outcome?.status === 'succeeded' ? 'review_required' : 'outcome_unknown' } };
281
+ const requestId = receipt?.request_id ?? randomUUID();
282
+ journal.receipts[unknown.operation_id] = { input_digest: unknown.input_digest, stage: 'settled', outcome, diagnostic: receipt?.diagnostic, outputs: receipt?.outputs, request_id: requestId };
218
283
  await saveJournal(path, journal);
284
+ session = await client.reconcile({ ...scope, request_id: requestId, project_fingerprint: client.identity.project_fingerprint, executor_generation: session.executor_generation, operation_id: unknown.operation_id, input_digest: unknown.input_digest, ...outcome });
285
+ continue;
219
286
  }
220
- session = await client.plan({ ...scope, request_id: journal.plan_request_id, expected_revision: session.revision, plan: journal.plan.wire });
221
- continue;
222
- }
223
- if (!journal.plan || !session.local_plan || setupDigest(session.local_plan) !== setupDigest(journal.plan.wire))
224
- throw new SetupError('setup_plan_missing', 'The browser-approved local plan is unavailable on this computer. No effects were applied.');
225
- if (session.phase !== 'applying') {
226
- session = await client.heartbeat({ ...scope, executor_generation: session.executor_generation, operation_id: null, lease_generation: null });
227
- await pause();
228
- session = await client.poll();
229
- continue;
230
- }
231
- const unknown = session.commands.find(command => command.status === 'outcome_unknown' || command.status === 'running' || command.status === 'leased' && journal.receipts[command.operation_id]);
232
- if (unknown) {
233
- const operation = journal.plan.operations.find(item => setupDigest(item.effect) === unknown.input_digest);
234
- if (!operation)
235
- throw new SetupError('setup_command_mismatch', 'The command does not match the local reviewed plan.');
236
- const receipt = journal.receipts[unknown.operation_id];
237
- // A running command belongs to an interrupted process. Wait for the server
238
- // to close its lease; never issue the side effect a second time.
239
- if (unknown.status !== 'outcome_unknown') {
287
+ const claimed = await client.claim({ ...scope, request_id: journal.claim_request_id, executor_generation: session.executor_generation });
288
+ session = claimed.session;
289
+ const command = claimed.command;
290
+ if (!command) {
240
291
  await pause();
241
292
  session = await client.poll();
242
293
  continue;
243
294
  }
244
- const stillVerified = await verified(directory, operation, runtime, receipt);
245
- const outcome = receipt?.outcome && receipt.outcome.status !== 'succeeded' ? receipt.outcome : stillVerified
246
- ? { status: 'succeeded', result: { code: 'already_present', verified: true } }
247
- : { status: 'manual', result: { code: receipt?.outcome?.status === 'succeeded' ? 'review_required' : 'outcome_unknown' } };
248
- const requestId = receipt?.request_id ?? randomUUID();
249
- journal.receipts[unknown.operation_id] = { input_digest: unknown.input_digest, stage: 'settled', outcome, outputs: receipt?.outputs, request_id: requestId };
295
+ if (command.status !== 'leased')
296
+ continue;
297
+ const operation = journal.plan.operations.find(item => item.effect.effect_id === command.input.effect_id);
298
+ if (!operation || setupDigest(operation.effect) !== command.input_digest || setupDigest(operation.effect) !== setupDigest(command.input))
299
+ throw new SetupError('setup_command_mismatch', 'The command differs from the approved local plan.');
300
+ const lease = { ...scope, operation_id: command.operation_id, executor_generation: command.executor_generation, lease_generation: command.lease_generation };
301
+ if (journal.receipts[command.operation_id])
302
+ throw new SetupError('setup_replay_refused', 'A local receipt already exists. Reconcile before attempting this command again.');
303
+ const receipt = { input_digest: command.input_digest, stage: 'claimed', request_id: randomUUID() };
304
+ journal.receipts[command.operation_id] = receipt;
250
305
  await saveJournal(path, journal);
251
- session = await client.reconcile({ ...scope, request_id: requestId, project_fingerprint: client.identity.project_fingerprint, executor_generation: session.executor_generation, operation_id: unknown.operation_id, input_digest: unknown.input_digest, ...outcome });
252
- continue;
253
- }
254
- const claimed = await client.claim({ ...scope, request_id: journal.claim_request_id, executor_generation: session.executor_generation });
255
- session = claimed.session;
256
- const command = claimed.command;
257
- if (!command) {
258
- await pause();
259
- session = await client.poll();
260
- continue;
261
- }
262
- if (command.status !== 'leased')
263
- continue;
264
- const operation = journal.plan.operations.find(item => item.effect.effect_id === command.input.effect_id);
265
- if (!operation || setupDigest(operation.effect) !== command.input_digest || setupDigest(operation.effect) !== setupDigest(command.input))
266
- throw new SetupError('setup_command_mismatch', 'The command differs from the approved local plan.');
267
- const lease = { ...scope, operation_id: command.operation_id, executor_generation: command.executor_generation, lease_generation: command.lease_generation };
268
- if (journal.receipts[command.operation_id])
269
- throw new SetupError('setup_replay_refused', 'A local receipt already exists. Reconcile before attempting this command again.');
270
- const receipt = { input_digest: command.input_digest, stage: 'claimed', request_id: randomUUID() };
271
- journal.receipts[command.operation_id] = receipt;
272
- await saveJournal(path, journal);
273
- session = await client.startCommand(lease);
274
- receipt.stage = 'running';
275
- await saveJournal(path, journal);
276
- const controller = new AbortController();
277
- const abort = () => controller.abort(options.signal?.reason);
278
- options.signal?.addEventListener('abort', abort, { once: true });
279
- if (options.signal?.aborted)
280
- abort();
281
- if (terminal.has(session.phase))
282
- controller.abort(new Error('Setup ended before local execution'));
283
- let heartbeatFailure, heartbeatBusy = false;
284
- let inFlightHeartbeat;
285
- const timer = setInterval(() => {
286
- if (heartbeatBusy)
287
- return;
288
- heartbeatBusy = true;
289
- inFlightHeartbeat = client.heartbeat(lease).then(value => { session = value; if (terminal.has(value.phase))
290
- controller.abort(new Error('Setup ended')); }, error => { heartbeatFailure = error; controller.abort(error); }).finally(() => { heartbeatBusy = false; });
291
- }, options.heartbeatMs ?? 10_000);
292
- try {
293
- controller.signal.throwIfAborted();
294
- if (!await verifySetupFiles(directory, operation.files, 'before'))
295
- throw new SetupError('setup_files_changed', 'Files changed after browser approval.');
296
- for (const guard of operation.guards ?? []) {
297
- const content = await readSetupFile(directory, guard.path);
298
- if ((content === null ? null : setupHash(content)) !== guard.before_hash)
299
- throw new SetupError('setup_files_changed', 'A package configuration changed after review.');
300
- }
301
- if (operation.effect.kind === 'install_packages')
302
- for (const file of operation.effect.managed_outputs ?? []) {
303
- const content = await readSetupFile(directory, file.path);
304
- if ((content === null ? null : setupHash(content)) !== file.before_hash)
305
- throw new SetupError('setup_files_changed', 'Package-manager outputs changed after approval.');
306
+ heartbeat.setLease(lease);
307
+ session = await client.startCommand(lease);
308
+ receipt.stage = 'running';
309
+ await saveJournal(path, journal);
310
+ const operationSignal = heartbeat.signal;
311
+ try {
312
+ operationSignal.throwIfAborted();
313
+ if (!await verifySetupFiles(directory, operation.files, 'before'))
314
+ throw new SetupError('setup_files_changed', 'Files changed after browser approval.');
315
+ for (const guard of operation.guards ?? []) {
316
+ const content = await readSetupFile(directory, guard.path);
317
+ if ((content === null ? null : setupHash(content)) !== guard.before_hash)
318
+ throw new SetupError('setup_files_changed', 'A package configuration changed after review.');
306
319
  }
307
- const code = await runtime.apply(operation, controller.signal);
308
- controller.signal.throwIfAborted();
309
- receipt.outputs = await outputs(directory, operation);
310
- if (!await verified(directory, operation, runtime, receipt))
311
- throw new SetupError('setup_verification_failed', 'The completed effect did not match the reviewed files.');
312
- receipt.outcome = { status: 'succeeded', result: { code, verified: true } };
313
- }
314
- catch (error) {
315
- receipt.outcome = { status: error instanceof SetupError && error.code === 'setup_files_changed' ? 'manual' : 'failed', result: { code: controller.signal.aborted || heartbeatFailure ? 'outcome_unknown' : error instanceof SetupError && error.code === 'setup_files_changed' ? 'review_required' : 'operation_failed' } };
316
- }
317
- finally {
318
- clearInterval(timer);
319
- await inFlightHeartbeat;
320
- options.signal?.removeEventListener('abort', abort);
320
+ if (operation.effect.kind === 'install_packages')
321
+ for (const file of operation.effect.managed_outputs ?? []) {
322
+ const content = await readSetupFile(directory, file.path);
323
+ if ((content === null ? null : setupHash(content)) !== file.before_hash)
324
+ throw new SetupError('setup_files_changed', 'Package-manager outputs changed after approval.');
325
+ }
326
+ const code = await runtime.apply(operation, operationSignal);
327
+ operationSignal.throwIfAborted();
328
+ receipt.outputs = await outputs(directory, operation);
329
+ if (!await verified(directory, operation, runtime, receipt))
330
+ throw new SetupError('setup_verification_failed', 'The completed effect did not match the reviewed files.');
331
+ receipt.outcome = { status: 'succeeded', result: { code, verified: true } };
332
+ }
333
+ catch (error) {
334
+ receipt.outcome = { status: error instanceof SetupError && error.code === 'setup_files_changed' ? 'manual' : 'failed', result: { code: operationSignal.aborted ? 'outcome_unknown' : error instanceof SetupError && error.code === 'setup_files_changed' ? 'review_required' : 'operation_failed' } };
335
+ receipt.diagnostic = sanitizeSetupDiagnostic(error instanceof Error ? error.message : 'Local operation failed without an error message.');
336
+ }
337
+ receipt.stage = 'settled';
338
+ journal.claim_request_id = randomUUID();
339
+ await saveJournal(path, journal);
340
+ showError(command.operation_id, receipt);
341
+ // A lost report is retried from its saved receipt on resume; the effect is never rerun.
342
+ session = await heartbeat.duringReport(() => client.report({ ...lease, ...receipt.outcome }));
321
343
  }
322
- receipt.stage = 'settled';
323
- journal.claim_request_id = randomUUID();
324
- await saveJournal(path, journal);
325
- // A lost report is retried from its saved receipt on resume; the effect is never rerun.
326
- session = await client.report({ ...lease, ...receipt.outcome });
327
- }
328
- });
344
+ });
345
+ }
346
+ finally {
347
+ if (!sharedHeartbeat)
348
+ await heartbeat.stop();
349
+ }
329
350
  }
@@ -0,0 +1,268 @@
1
+ import type { BrowserSetupClient } from './setup.js';
2
+ import type { SetupSessionView } from './setup-wire.js';
3
+ /** Stop waiting when this run ends, including user-supplied preparations. */
4
+ export declare function setupAbortable<T>(work: Promise<T>, signal?: AbortSignal): Promise<T>;
5
+ /** One lease-aware heartbeat for the entire run, independent of its current stage. */
6
+ export declare class SetupHeartbeat {
7
+ private client;
8
+ private options;
9
+ private session?;
10
+ private timer?;
11
+ private pending?;
12
+ private stopped;
13
+ private paused;
14
+ private failure?;
15
+ private lease?;
16
+ private readonly lifetime;
17
+ readonly signal: AbortSignal;
18
+ constructor(client: Pick<BrowserSetupClient, 'identity' | 'heartbeat'>, options?: {
19
+ signal?: AbortSignal;
20
+ heartbeatMs?: number;
21
+ });
22
+ observe(next: SetupSessionView): SetupSessionView;
23
+ get current(): ({
24
+ phase: "expired" | "cancelled" | "analyzing" | "configuring" | "awaiting_local_approval" | "applying" | "ready_for_recommendations" | "selecting_drafts" | "creating_drafts" | "agent_test" | "complete";
25
+ revision: string;
26
+ workspace_id: string | null;
27
+ site_id: string | null;
28
+ project_fingerprint: string;
29
+ choices: {
30
+ install_sdk: boolean;
31
+ install_skill: boolean;
32
+ footer_badge: boolean;
33
+ } & {};
34
+ capabilities: {
35
+ framework: string;
36
+ package_manager: string;
37
+ install_packages: boolean;
38
+ initialize_sdk: boolean;
39
+ install_skill: boolean;
40
+ insert_footer: boolean;
41
+ source_review: boolean;
42
+ sample_read: boolean;
43
+ } & {
44
+ onboarding_v2?: boolean | undefined;
45
+ project_name?: string | undefined;
46
+ launcher?: ({
47
+ wizard_version: string;
48
+ api_origin: string;
49
+ } & {
50
+ registry?: string | undefined;
51
+ }) | undefined;
52
+ terminal?: ({
53
+ platform: string;
54
+ app: string;
55
+ } & {}) | undefined;
56
+ };
57
+ local_plan: ({
58
+ project_fingerprint: string;
59
+ choices_digest: string;
60
+ effects: import("./setup-wire.js").SetupEffect[];
61
+ } & {
62
+ manual_steps?: ({
63
+ kind: "install_packages" | "initialize_sdk" | "install_skill" | "insert_footer";
64
+ code: string;
65
+ } & {})[] | undefined;
66
+ }) | null;
67
+ plan_digest: string | null;
68
+ commands: ({
69
+ operation_id: string;
70
+ sequence: number;
71
+ kind: "install_packages" | "initialize_sdk" | "install_skill" | "insert_footer";
72
+ expected_revision: string;
73
+ input: import("./setup-wire.js").SetupEffect;
74
+ input_digest: string;
75
+ approval_digest: string;
76
+ status: "approved" | "manual" | "cancelled" | "failed" | "outcome_unknown" | "prepared" | "leased" | "running" | "succeeded";
77
+ executor_generation: number | null;
78
+ lease_generation: number;
79
+ lease_expires_at: string | null;
80
+ result: ({
81
+ code: "cancelled" | "skipped" | "already_present" | "unsupported" | "operation_failed" | "verified" | "review_required" | "dependency_failed" | "outcome_unknown";
82
+ } & {
83
+ verified?: boolean | undefined;
84
+ }) | null;
85
+ result_digest: string | null;
86
+ created_at: string;
87
+ started_at: string | null;
88
+ finished_at: string | null;
89
+ updated_at: string;
90
+ } & {})[];
91
+ executor_generation: number;
92
+ connection: string;
93
+ heartbeat_at: string | null;
94
+ sample: {
95
+ status: string;
96
+ first_received_at: string | null;
97
+ latest_received_at: string | null;
98
+ latest_request_id: string | null;
99
+ } & {};
100
+ draft_outcomes: ({
101
+ candidate_id: string;
102
+ status: string;
103
+ prompt_id: string;
104
+ request_id: string;
105
+ revision: string | null;
106
+ code: "cancelled" | "skipped" | "already_present" | "unsupported" | "operation_failed" | "verified" | "review_required" | "dependency_failed" | "outcome_unknown" | null;
107
+ } & {})[];
108
+ bound_at: string;
109
+ last_activity_at: string;
110
+ idle_expires_at: string;
111
+ absolute_expires_at: string;
112
+ terminal_at: string | null;
113
+ schema_version: 1;
114
+ setup_id: string;
115
+ } & {
116
+ source?: ({
117
+ preparation_id: string;
118
+ status: string;
119
+ packet_digest: string;
120
+ provider_fingerprint: string;
121
+ authorized_at: string | null;
122
+ expires_at: string;
123
+ error_code: "operation_failed" | "planning_unavailable" | "planning_paused" | "planning_daily_budget" | "planning_allowance_exhausted" | "planning_provider_unavailable" | "planning_policy_changed" | "planning_consent_expired" | "planning_input_budget" | "planning_invalid_output" | "planning_timeout" | "source_changed" | "source_unavailable" | "recommendations_no_source" | null;
124
+ } & {
125
+ analysis_id?: string | undefined;
126
+ analysis_started_at?: string | undefined;
127
+ analysis_completed_at?: string | undefined;
128
+ deadline_at?: string | undefined;
129
+ timings?: ({} & {
130
+ extraction_ms?: number | undefined;
131
+ consolidation_ms?: number | undefined;
132
+ authoring_ms?: number | undefined;
133
+ total_ms?: number | undefined;
134
+ }) | undefined;
135
+ }) | null | undefined;
136
+ recommendation_digest?: string | null | undefined;
137
+ selected_candidates?: string[] | undefined;
138
+ }) | undefined;
139
+ setLease(lease: {
140
+ operation_id: string;
141
+ executor_generation: number;
142
+ lease_generation: number;
143
+ }): void;
144
+ latest(fallback: SetupSessionView): {
145
+ phase: "expired" | "cancelled" | "analyzing" | "configuring" | "awaiting_local_approval" | "applying" | "ready_for_recommendations" | "selecting_drafts" | "creating_drafts" | "agent_test" | "complete";
146
+ revision: string;
147
+ workspace_id: string | null;
148
+ site_id: string | null;
149
+ project_fingerprint: string;
150
+ choices: {
151
+ install_sdk: boolean;
152
+ install_skill: boolean;
153
+ footer_badge: boolean;
154
+ } & {};
155
+ capabilities: {
156
+ framework: string;
157
+ package_manager: string;
158
+ install_packages: boolean;
159
+ initialize_sdk: boolean;
160
+ install_skill: boolean;
161
+ insert_footer: boolean;
162
+ source_review: boolean;
163
+ sample_read: boolean;
164
+ } & {
165
+ onboarding_v2?: boolean | undefined;
166
+ project_name?: string | undefined;
167
+ launcher?: ({
168
+ wizard_version: string;
169
+ api_origin: string;
170
+ } & {
171
+ registry?: string | undefined;
172
+ }) | undefined;
173
+ terminal?: ({
174
+ platform: string;
175
+ app: string;
176
+ } & {}) | undefined;
177
+ };
178
+ local_plan: ({
179
+ project_fingerprint: string;
180
+ choices_digest: string;
181
+ effects: import("./setup-wire.js").SetupEffect[];
182
+ } & {
183
+ manual_steps?: ({
184
+ kind: "install_packages" | "initialize_sdk" | "install_skill" | "insert_footer";
185
+ code: string;
186
+ } & {})[] | undefined;
187
+ }) | null;
188
+ plan_digest: string | null;
189
+ commands: ({
190
+ operation_id: string;
191
+ sequence: number;
192
+ kind: "install_packages" | "initialize_sdk" | "install_skill" | "insert_footer";
193
+ expected_revision: string;
194
+ input: import("./setup-wire.js").SetupEffect;
195
+ input_digest: string;
196
+ approval_digest: string;
197
+ status: "approved" | "manual" | "cancelled" | "failed" | "outcome_unknown" | "prepared" | "leased" | "running" | "succeeded";
198
+ executor_generation: number | null;
199
+ lease_generation: number;
200
+ lease_expires_at: string | null;
201
+ result: ({
202
+ code: "cancelled" | "skipped" | "already_present" | "unsupported" | "operation_failed" | "verified" | "review_required" | "dependency_failed" | "outcome_unknown";
203
+ } & {
204
+ verified?: boolean | undefined;
205
+ }) | null;
206
+ result_digest: string | null;
207
+ created_at: string;
208
+ started_at: string | null;
209
+ finished_at: string | null;
210
+ updated_at: string;
211
+ } & {})[];
212
+ executor_generation: number;
213
+ connection: string;
214
+ heartbeat_at: string | null;
215
+ sample: {
216
+ status: string;
217
+ first_received_at: string | null;
218
+ latest_received_at: string | null;
219
+ latest_request_id: string | null;
220
+ } & {};
221
+ draft_outcomes: ({
222
+ candidate_id: string;
223
+ status: string;
224
+ prompt_id: string;
225
+ request_id: string;
226
+ revision: string | null;
227
+ code: "cancelled" | "skipped" | "already_present" | "unsupported" | "operation_failed" | "verified" | "review_required" | "dependency_failed" | "outcome_unknown" | null;
228
+ } & {})[];
229
+ bound_at: string;
230
+ last_activity_at: string;
231
+ idle_expires_at: string;
232
+ absolute_expires_at: string;
233
+ terminal_at: string | null;
234
+ schema_version: 1;
235
+ setup_id: string;
236
+ } & {
237
+ source?: ({
238
+ preparation_id: string;
239
+ status: string;
240
+ packet_digest: string;
241
+ provider_fingerprint: string;
242
+ authorized_at: string | null;
243
+ expires_at: string;
244
+ error_code: "operation_failed" | "planning_unavailable" | "planning_paused" | "planning_daily_budget" | "planning_allowance_exhausted" | "planning_provider_unavailable" | "planning_policy_changed" | "planning_consent_expired" | "planning_input_budget" | "planning_invalid_output" | "planning_timeout" | "source_changed" | "source_unavailable" | "recommendations_no_source" | null;
245
+ } & {
246
+ analysis_id?: string | undefined;
247
+ analysis_started_at?: string | undefined;
248
+ analysis_completed_at?: string | undefined;
249
+ deadline_at?: string | undefined;
250
+ timings?: ({} & {
251
+ extraction_ms?: number | undefined;
252
+ consolidation_ms?: number | undefined;
253
+ authoring_ms?: number | undefined;
254
+ total_ms?: number | undefined;
255
+ }) | undefined;
256
+ }) | null | undefined;
257
+ recommendation_digest?: string | null | undefined;
258
+ selected_candidates?: string[] | undefined;
259
+ };
260
+ check(): void;
261
+ private schedule;
262
+ pulse(): Promise<void>;
263
+ /** Finish the old lease's in-flight heartbeat before closing it in a report. */
264
+ duringReport<T>(operation: () => Promise<T>): Promise<T>;
265
+ stop(): Promise<void>;
266
+ }
267
+ /** Observe all responses; cancel outstanding reads/source work when the run ends. */
268
+ export declare function observeSetupClient<T extends object>(client: T, heartbeat: SetupHeartbeat, signal?: AbortSignal): T;