@indigoai-us/hq-cli 5.154.0 → 5.156.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.
@@ -59,14 +59,22 @@ export function formatCheckLines(threads, company, projectId) {
59
59
  const scope = projectId
60
60
  ? `${company.companySlug || company.companyUid}/${projectId}`
61
61
  : company.companySlug || company.companyUid;
62
- const lines = [`Work mesh: ${threads.length} active thread(s) for ${scope}`];
62
+ const lines = [`Work mesh: ${threads.length} thread(s) for ${scope}`];
63
63
  for (const thread of threads.slice(0, 8)) {
64
64
  const owner = thread.ownerUid ? ` owner=${thread.ownerUid}` : "";
65
65
  const summary = thread.progressSummary ||
66
66
  thread.sourceSignalSummary ||
67
67
  thread.blockedReason ||
68
68
  "";
69
- lines.push(`- ${thread.threadStatus ?? "unknown"} ${thread.threadId ?? "?"}${owner}${summary ? `: ${summary}` : ""}`);
69
+ const lane = thread.kind === "lane" ? thread.lane : undefined;
70
+ const laneDetails = lane
71
+ ? ` lane=${lane.laneId} laneStatus=${thread.laneStatus ?? "unknown"}` +
72
+ ` story=${lane.storyId ?? "unknown"} profile=${lane.workerProfile ?? "unknown"}` +
73
+ ` model=${lane.model ?? "unknown"} account=${lane.account ?? "unknown"}` +
74
+ `${lane.parentLaneId ? ` parent=${lane.parentLaneId}` : ""}`
75
+ : "";
76
+ const machine = thread.senderMachineUid ? ` machine=${thread.senderMachineUid}` : "";
77
+ lines.push(`- ${thread.threadStatus ?? "unknown"} ${thread.threadId ?? "?"}${owner}${machine}${laneDetails}${summary ? `: ${summary}` : ""}`);
70
78
  }
71
79
  return lines;
72
80
  }
@@ -320,6 +328,9 @@ function mergeProjectRepos(current, planned) {
320
328
  return [...planned, ...current.filter((repo) => !plannedPaths.has(repo.path))];
321
329
  }
322
330
  async function runProjectSet(projectId, opts) {
331
+ if (opts.channel !== undefined && !opts.channel.trim()) {
332
+ fail("PROJECT_CHANNEL_INVALID: --channel must be a non-empty channel id.");
333
+ }
323
334
  if (opts.storiesFile && opts.storiesJson) {
324
335
  fail("Pass only one of --stories-file or --stories-json.");
325
336
  }
@@ -358,6 +369,7 @@ async function runProjectSet(projectId, opts) {
358
369
  const principalUid = await resolveMeshPrincipalUid(token, loadCachedTokens());
359
370
  const PROJECT_CHANGED = "The Board changed while you were editing. Re-run to pick up the latest.";
360
371
  let view;
372
+ let registration;
361
373
  if (!current) {
362
374
  const ensured = await ensureProjectBoard({
363
375
  token,
@@ -367,10 +379,12 @@ async function runProjectSet(projectId, opts) {
367
379
  description: opts.description !== undefined ? opts.description : source?.description ?? "",
368
380
  stories: source?.stories ?? [],
369
381
  repos: explicitRepos ?? source?.repos ?? [],
382
+ channelId: opts.channel,
370
383
  });
371
384
  view = ensured.view;
385
+ registration = ensured.registration;
372
386
  }
373
- else {
387
+ else if (opts.name !== undefined || opts.description !== undefined || source !== undefined || explicitRepos !== undefined) {
374
388
  try {
375
389
  view = await putProjectView(token, company.companyUid, projectId, {
376
390
  name: opts.name?.trim() || source?.name || current.name,
@@ -395,18 +409,30 @@ async function runProjectSet(projectId, opts) {
395
409
  throw err;
396
410
  }
397
411
  }
412
+ else {
413
+ view = current;
414
+ }
415
+ if (current && opts.channel !== undefined) {
416
+ registration = await registerMeshProject(token, company.companyUid, projectId, opts.channel);
417
+ }
398
418
  if (!current && opts.create) {
399
419
  noteCallingSessionBound(view.projectId, company);
400
420
  }
401
421
  if (opts.json) {
402
- console.log(JSON.stringify({ ok: true, action: "project.set", principalUid, ...view }, null, 2));
422
+ console.log(JSON.stringify({
423
+ ok: true,
424
+ action: "project.set",
425
+ principalUid,
426
+ ...view,
427
+ ...(registration ? { registration } : {}),
428
+ }, null, 2));
403
429
  return;
404
430
  }
405
431
  console.log(`Work mesh: project ${view.projectId} set (${view.name}; ${view.stories.length} stories)`);
406
432
  }
407
433
  async function runProjectRegister(projectId, opts) {
408
- if (opts.channel?.trim()) {
409
- fail("--channel is not supported by the current Work Mesh registration API; omit it to let the server create or bind the project channel.");
434
+ if (opts.channel !== undefined && !opts.channel.trim()) {
435
+ fail("PROJECT_CHANNEL_INVALID: --channel must be a non-empty channel id.");
410
436
  }
411
437
  const { token, company } = await withCompany(opts);
412
438
  const principalUid = await resolveMeshPrincipalUid(token, loadCachedTokens());
@@ -425,7 +451,7 @@ async function runProjectRegister(projectId, opts) {
425
451
  }
426
452
  let registration;
427
453
  try {
428
- registration = await registerMeshProject(token, company.companyUid, projectId);
454
+ registration = await registerMeshProject(token, company.companyUid, projectId, opts.channel);
429
455
  }
430
456
  catch (err) {
431
457
  const failure = err;
@@ -1787,6 +1813,7 @@ export function registerMeshCommand(program) {
1787
1813
  .option("--stories-json <json>", "Story array or JSON object with userStories")
1788
1814
  .option("--repos-json <json>", "Repository array, such as [{path,branch}]")
1789
1815
  .option("--repo <path[@branch]>", "Repository binding (repeatable)", collectRepeatable)
1816
+ .option("--channel <channelId>", "Bind the project to an existing company or project channel")
1790
1817
  .option("--create", "Create the Board view when the GET is a 404")
1791
1818
  .option("--json", "Print machine-readable JSON")
1792
1819
  .action((projectId, opts) => wrap(() => runProjectSet(projectId, opts))());
@@ -1801,7 +1828,7 @@ export function registerMeshCommand(program) {
1801
1828
  .command("register <projectId>")
1802
1829
  .description("Register an existing Board and bind its project channel; use project ensure to create or repair a Board")
1803
1830
  .requiredOption("--company <slug|uid>", "Company slug or cloud uid")
1804
- .option("--channel <channelId>", "Reserved: the current server chooses the project channel")
1831
+ .option("--channel <channelId>", "Bind the project to an existing company or project channel")
1805
1832
  .option("--json", "Print machine-readable JSON")
1806
1833
  .action((projectId, opts) => wrap(() => runProjectRegister(projectId, opts))());
1807
1834
  mesh
@@ -4,8 +4,8 @@
4
4
  * Naming an account and omitting one mean different things: an explicit
5
5
  * disabled id is refused; omitting picks the least-loaded eligible account.
6
6
  * Status reports `configured` and `logged_in` separately. logged_in is
7
- * derived from whether a provider credential file EXISTS — this module
8
- * never reads, prints, copies, or hashes that file.
7
+ * derived from credential-file existence or a provider status command; this
8
+ * module never reads, prints, copies, or hashes credential files.
9
9
  */
10
10
  import { type LaneRecord, type LaneProvider } from "./types.js";
11
11
  import type { LanesConfig } from "./types.js";
@@ -19,13 +19,22 @@ export declare const CLAUDE_CREDENTIALS_NAME = ".credentials.json";
19
19
  * file here at all.
20
20
  */
21
21
  export declare const PROVIDER_CREDENTIAL_FILES: Record<LaneProvider, readonly string[]>;
22
- export type AuthState = "file" | "absent" | "unknown";
22
+ export type AuthState = "file" | "status" | "absent" | "unknown";
23
+ export interface AccountStatusProbeResult {
24
+ status: number | null;
25
+ stdout: string;
26
+ stderr: string;
27
+ }
23
28
  export interface AccountDeps {
24
29
  existsSync: (p: string) => boolean;
30
+ /** Provider status output is parsed for a boolean only; it is never printed. */
31
+ probe?: (command: string, args: string[], env: NodeJS.ProcessEnv) => AccountStatusProbeResult;
32
+ env?: NodeJS.ProcessEnv;
25
33
  }
26
34
  export interface AccountResolveContext {
27
35
  hqRoot?: string;
28
36
  existsSync?: (p: string) => boolean;
37
+ accountDeps?: Omit<AccountDeps, "existsSync">;
29
38
  loadLanes?: (hqRoot: string) => LaneRecord[];
30
39
  /** Already-resolved model for this lane. Beats cliModel and account defaults. */
31
40
  model?: string;
@@ -38,6 +47,8 @@ export interface AccountResolveContext {
38
47
  * set is never selected, so no refusal has to be decided under the lock.
39
48
  */
40
49
  allowedAccounts?: readonly string[];
50
+ /** Ordered preferences; callers pass these only for a new lane's first assignment. */
51
+ preferredAccounts?: readonly string[];
41
52
  }
42
53
  export interface ResolvedAccount {
43
54
  provider: LaneProvider;
@@ -54,13 +65,7 @@ export interface AccountStatus {
54
65
  disabled_reason?: string;
55
66
  /** Present in personal/lanes/config.json. */
56
67
  configured: boolean;
57
- /**
58
- * True when a provider credential file exists. False only when every
59
- * candidate is absent AND absence is conclusive for that provider
60
- * (Codex, Grok). Null for Claude when no candidate exists: Claude can
61
- * authenticate via keychain, so a missing file is not logged-out.
62
- * Never inspects file contents.
63
- */
68
+ /** True/false comes from file existence or the provider status command. */
64
69
  logged_in: boolean | null;
65
70
  auth_state: AuthState;
66
71
  envVar: string;
@@ -79,7 +84,7 @@ export declare function authStateFor(provider: LaneProvider, present: boolean):
79
84
  export declare function loggedInFromAuthState(state: AuthState): boolean | null;
80
85
  /** Admission and listing share this: refuse only a conclusive logged-out. */
81
86
  export declare function isLoggedOut(status: Pick<AccountStatus, "logged_in">): boolean;
82
- /** Auto-select requires a proven credential file, not Claude-unknown. */
87
+ /** Auto-select requires a positive file or provider-status login result. */
83
88
  export declare function isCredentialProven(status: Pick<AccountStatus, "logged_in">): boolean;
84
89
  /**
85
90
  * Occupying states hold a slot: running / awaiting_input / blocked have a
@@ -4,8 +4,8 @@
4
4
  * Naming an account and omitting one mean different things: an explicit
5
5
  * disabled id is refused; omitting picks the least-loaded eligible account.
6
6
  * Status reports `configured` and `logged_in` separately. logged_in is
7
- * derived from whether a provider credential file EXISTS — this module
8
- * never reads, prints, copies, or hashes that file.
7
+ * derived from credential-file existence or a provider status command; this
8
+ * module never reads, prints, copies, or hashes credential files.
9
9
  */
10
10
  import * as fs from "node:fs";
11
11
  import * as path from "node:path";
@@ -76,7 +76,7 @@ export function loggedInFromAuthState(state) {
76
76
  export function isLoggedOut(status) {
77
77
  return status.logged_in === false;
78
78
  }
79
- /** Auto-select requires a proven credential file, not Claude-unknown. */
79
+ /** Auto-select requires a positive file or provider-status login result. */
80
80
  export function isCredentialProven(status) {
81
81
  return status.logged_in === true;
82
82
  }
@@ -205,8 +205,12 @@ export function selectBalancedAccount(provider, config, ctx = {}) {
205
205
  }
206
206
  const proven = [];
207
207
  for (const id of enabled) {
208
- const account = providerCfg.accounts[id];
209
- if (hasCredentialFile(provider, account.home, existsSync))
208
+ const status = accountStatus(provider, id, config, {
209
+ existsSync,
210
+ ...(ctx.accountDeps?.probe ? { probe: ctx.accountDeps.probe } : {}),
211
+ ...(ctx.accountDeps?.env ? { env: ctx.accountDeps.env } : {}),
212
+ });
213
+ if (status.logged_in === true)
210
214
  proven.push(id);
211
215
  }
212
216
  if (proven.length === 0) {
@@ -235,6 +239,9 @@ export function selectBalancedAccount(provider, config, ctx = {}) {
235
239
  const cap = accountMaxRunning(providerCfg, id);
236
240
  return runningLaneCount(lanes, provider, id) < cap;
237
241
  });
242
+ const preferred = underCap.length > 0
243
+ ? ctx.preferredAccounts?.find((id) => underCap.includes(id))
244
+ : undefined;
238
245
  const pool = underCap.length > 0 ? underCap : runnable;
239
246
  const ranked = pool
240
247
  .map((id) => ({
@@ -243,7 +250,7 @@ export function selectBalancedAccount(provider, config, ctx = {}) {
243
250
  lastStarted: lastStartedAt(lanes, provider, id),
244
251
  }))
245
252
  .sort(compareEligible);
246
- const chosen = ranked[0].id;
253
+ const chosen = preferred ?? ranked[0].id;
247
254
  return boundAccount(provider, chosen, providerCfg.accounts[chosen]);
248
255
  }
249
256
  export function resolveAccount(provider, requestedId, config, ctx = {}) {
@@ -293,14 +300,28 @@ export function accountStatus(provider, accountId, config, deps = defaultAccount
293
300
  };
294
301
  }
295
302
  const present = hasCredentialFile(provider, account.home, deps.existsSync);
296
- const auth_state = authStateFor(provider, present);
303
+ let auth_state = authStateFor(provider, present);
304
+ let logged_in = loggedInFromAuthState(auth_state);
305
+ if (!present && provider === "claude" && deps.probe !== undefined) {
306
+ try {
307
+ const result = deps.probe("claude", ["auth", "status"], { ...(deps.env ?? process.env), CLAUDE_CONFIG_DIR: account.home });
308
+ const providerStatus = claudeLoginStatus(result);
309
+ if (providerStatus !== null) {
310
+ auth_state = "status";
311
+ logged_in = providerStatus;
312
+ }
313
+ }
314
+ catch {
315
+ process.stderr.write(`hq lanes: Claude auth status probe failed for account ${accountId}\n`);
316
+ }
317
+ }
297
318
  const status = {
298
319
  provider,
299
320
  id: accountId,
300
321
  home: account.home,
301
322
  enabled: account.enabled,
302
323
  configured: true,
303
- logged_in: loggedInFromAuthState(auth_state),
324
+ logged_in,
304
325
  auth_state,
305
326
  envVar,
306
327
  };
@@ -312,6 +333,27 @@ export function accountStatus(provider, accountId, config, deps = defaultAccount
312
333
  export function listAccountStatuses(provider, config, deps = defaultAccountDeps()) {
313
334
  return Object.keys(config.providers[provider].accounts).map((id) => accountStatus(provider, id, config, deps));
314
335
  }
336
+ function claudeLoginStatus(result) {
337
+ const text = result.stdout.trim();
338
+ if (text.startsWith("{")) {
339
+ try {
340
+ const parsed = JSON.parse(text);
341
+ if (parsed !== null &&
342
+ typeof parsed === "object" &&
343
+ !Array.isArray(parsed) &&
344
+ typeof parsed.loggedIn === "boolean") {
345
+ return parsed.loggedIn;
346
+ }
347
+ }
348
+ catch (err) {
349
+ process.stderr.write(`hq lanes: Claude auth status returned invalid JSON: ${err instanceof Error ? err.message : String(err)}\n`);
350
+ return null;
351
+ }
352
+ }
353
+ if (/\bnot logged in\b/i.test(`${result.stdout}\n${result.stderr}`))
354
+ return false;
355
+ return null;
356
+ }
315
357
  /** Environment assignment the worker process should receive. */
316
358
  export function accountEnv(resolved) {
317
359
  return { [resolved.envVar]: resolved.home };
@@ -0,0 +1,19 @@
1
+ import type { AdmissionHost } from "./admission.js";
2
+ export declare const OUTPOST_ID_PATH = "/etc/outpost/outpost-id";
3
+ export declare const OUTPOST_DIR = "/etc/outpost";
4
+ export type OutpostIdentity = {
5
+ kind: "id";
6
+ id: string;
7
+ } | {
8
+ kind: "unknown";
9
+ } | {
10
+ kind: "not_outpost";
11
+ };
12
+ export declare function readOutpostIdentity(existsSync?: (p: string) => boolean, readFileSync?: (p: string) => string): OutpostIdentity;
13
+ export declare function resolveAdmissionHost(deps?: {
14
+ hostname?: () => string;
15
+ readOutpostIdentity?: () => OutpostIdentity;
16
+ existsSync?: (p: string) => boolean;
17
+ readFileSync?: (p: string) => string;
18
+ }): AdmissionHost;
19
+ //# sourceMappingURL=admission-host.d.ts.map
@@ -0,0 +1,33 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ export const OUTPOST_ID_PATH = "/etc/outpost/outpost-id";
4
+ export const OUTPOST_DIR = "/etc/outpost";
5
+ export function readOutpostIdentity(existsSync = (p) => fs.existsSync(p), readFileSync = (p) => fs.readFileSync(p, "utf8")) {
6
+ const hasMarker = existsSync(OUTPOST_ID_PATH);
7
+ const hasDir = existsSync(OUTPOST_DIR);
8
+ if (hasMarker) {
9
+ let text;
10
+ try {
11
+ text = readFileSync(OUTPOST_ID_PATH);
12
+ }
13
+ catch (err) {
14
+ process.stderr.write(`hq lanes: failed to read ${OUTPOST_ID_PATH}: ${err instanceof Error ? err.message : String(err)}\n`);
15
+ throw err;
16
+ }
17
+ const id = text.trim();
18
+ if (!id)
19
+ return hasDir ? { kind: "unknown" } : { kind: "not_outpost" };
20
+ return { kind: "id", id };
21
+ }
22
+ if (hasDir)
23
+ return { kind: "unknown" };
24
+ return { kind: "not_outpost" };
25
+ }
26
+ export function resolveAdmissionHost(deps = {}) {
27
+ const identity = deps.readOutpostIdentity?.() ?? readOutpostIdentity(deps.existsSync, deps.readFileSync);
28
+ return {
29
+ hostname: (deps.hostname ?? os.hostname)(),
30
+ outpostId: identity.kind === "id" ? identity.id : null,
31
+ };
32
+ }
33
+ //# sourceMappingURL=admission-host.js.map
@@ -173,6 +173,29 @@ function parseFastModeModels(value, field) {
173
173
  }
174
174
  return value.map((model) => model.trim());
175
175
  }
176
+ function parsePreferredAccounts(provider, value, accounts) {
177
+ const field = `providers.${provider}.preferred_accounts`;
178
+ if (value === undefined)
179
+ return [];
180
+ if (!Array.isArray(value) ||
181
+ value.some((id) => typeof id !== "string" ||
182
+ id.trim().length === 0 ||
183
+ id.trim() !== id)) {
184
+ throw new LanesConfigError("invalid_preferred_accounts", `${field} must be an array of non-empty account ids without surrounding whitespace`, { provider, field });
185
+ }
186
+ const preferred = value;
187
+ const seen = new Set();
188
+ for (const id of preferred) {
189
+ if (seen.has(id)) {
190
+ throw new LanesConfigError("invalid_preferred_accounts", `${field} contains duplicate account ${JSON.stringify(id)}`, { provider, field, account: id });
191
+ }
192
+ if (accounts[id] === undefined) {
193
+ throw new LanesConfigError("preferred_account_unconfigured", `${field} names unconfigured account ${JSON.stringify(id)}`, { provider, field, account: id, accounts: Object.keys(accounts) });
194
+ }
195
+ seen.add(id);
196
+ }
197
+ return [...preferred];
198
+ }
176
199
  function rejectDuplicateHomes(provider, accounts) {
177
200
  const seen = new Map();
178
201
  for (const [id, account] of Object.entries(accounts)) {
@@ -232,6 +255,7 @@ function parseProvider(provider, raw, deps) {
232
255
  max_running: positiveInt(raw.max_running, `providers.${provider}.max_running`, DEFAULT_PROVIDER_MAX_RUNNING[provider]),
233
256
  max_queued: positiveInt(raw.max_queued, `providers.${provider}.max_queued`, DEFAULT_MAX_QUEUED),
234
257
  accounts,
258
+ preferred_accounts: parsePreferredAccounts(provider, raw.preferred_accounts, accounts),
235
259
  };
236
260
  if (provider === "codex") {
237
261
  parsed.fast_mode_models = parseFastModeModels(raw.fast_mode_models, `providers.${provider}.fast_mode_models`);
@@ -301,6 +325,7 @@ export function documentedDefaultConfig(deps = defaultDeps()) {
301
325
  max_running: DEFAULT_PROVIDER_MAX_RUNNING[provider],
302
326
  max_queued: DEFAULT_MAX_QUEUED,
303
327
  accounts: defaultAccountMap(provider, deps),
328
+ preferred_accounts: [],
304
329
  };
305
330
  if (provider === "codex")
306
331
  providerConfig.fast_mode_models = [];
@@ -42,11 +42,11 @@
42
42
  * of opening a browser nobody is watching, and the watcher records
43
43
  * `mesh_event_failed` and moves on. No credential file is read here.
44
44
  */
45
- import type { LaneCompanyRef, LaneEnvelope, LaneRecord } from "./types.js";
46
- export declare const LANE_MESH_EVENTS: readonly ["lane_started", "lane_progress", "lane_question", "lane_question_at_user", "lane_ask", "lane_blocked", "lane_done"];
45
+ import type { LaneCompanyRef, LaneEnvelope, LaneRecord, TerminalLaneState } from "./types.js";
46
+ export declare const LANE_MESH_EVENTS: readonly ["lane_started", "lane_progress", "lane_question", "lane_question_at_user", "lane_ask", "lane_blocked", "lane_done", "lane_failed", "lane_interrupted", "lane_cancelled", "lane_killed"];
47
47
  export type LaneMeshEvent = (typeof LANE_MESH_EVENTS)[number];
48
48
  /** Roster kinds this module emits. Subset of the server's `validKinds`. */
49
- export declare const WIRE_EVENT_KINDS: readonly ["run_started", "run_progress", "run_complete", "question", "note"];
49
+ export declare const WIRE_EVENT_KINDS: readonly ["run_started", "run_progress", "run_complete", "blocked", "question", "note"];
50
50
  export type WireEventKind = (typeof WIRE_EVENT_KINDS)[number];
51
51
  /** Server caps: activity title/summary 2000, question text 500, note text 500. */
52
52
  export declare const WIRE_ACTIVITY_TEXT_MAX = 2000;
@@ -70,6 +70,7 @@ export declare function toWireThreadEvent(event: LaneMeshEvent, payload: Record<
70
70
  */
71
71
  export interface LaneMesh {
72
72
  ensureProjectThread: (company: LaneCompanyRef, projectId: string) => Promise<string>;
73
+ ensureLaneThread?: (company: LaneCompanyRef, lane: LaneRecord, laneStatus?: string, signal?: AbortSignal) => Promise<string>;
73
74
  appendThreadEvent: (company: LaneCompanyRef, threadId: string, event: LaneMeshEvent, payload: Record<string, unknown>, signal?: AbortSignal) => Promise<void>;
74
75
  patchStoryStatus: (company: LaneCompanyRef, projectId: string, storyId: string, status: string) => Promise<void>;
75
76
  }
@@ -92,6 +93,7 @@ export declare function createLaneMesh(opts?: CreateLaneMeshOptions): LaneMesh;
92
93
  export declare function laneMeshCompany(lane: Pick<LaneRecord, "company" | "mesh_company">): LaneCompanyRef | undefined;
93
94
  /** Company attribution + thread present. Unscoped personal lanes emit nothing. */
94
95
  export declare function laneCanEmit(lane: Pick<LaneRecord, "company" | "mesh_company" | "thread_id">): boolean;
96
+ export declare function laneMeshEventIdentity(lane: LaneRecord): Record<string, unknown>;
95
97
  /**
96
98
  * Spec §8 terminal-envelope events. `done` → lane_done and story → review;
97
99
  * `blocked` → lane_blocked; `ask` → lane_ask with the queue ids. The story is
@@ -101,6 +103,8 @@ export declare function emitLaneDecisionEvents(mesh: LaneMesh, lane: LaneRecord,
101
103
  questionIds?: string[];
102
104
  signal?: AbortSignal;
103
105
  }): Promise<void>;
106
+ /** Emit an abnormal terminal state even when the worker produced no envelope. */
107
+ export declare function emitLaneTerminalStateEvent(mesh: LaneMesh, lane: LaneRecord, state: Exclude<TerminalLaneState, "done">, summary?: string, signal?: AbortSignal): Promise<void>;
104
108
  export declare function emitLaneProgressEvent(mesh: LaneMesh, lane: LaneRecord, detail: string, signal?: AbortSignal): Promise<void>;
105
109
  /**
106
110
  * Throttle for lane_progress (spec §8 progress row). True when no progress