@indigoai-us/hq-cli 5.345.160 → 5.345.162

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 (73) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/assets/scaffold/core/scripts/session-journal.sh +116 -24
  3. package/dist/command-catalog.generated.d.ts +262 -5
  4. package/dist/command-catalog.generated.js +322 -5
  5. package/dist/command-registration-plan.d.ts +6 -0
  6. package/dist/command-registration-plan.js +1 -0
  7. package/dist/commands/agent-inbox.js +6 -2
  8. package/dist/commands/auth.js +6 -5
  9. package/dist/commands/core-session-helper-runtime.d.ts +1 -0
  10. package/dist/commands/core-session-journal-native.d.ts +1 -0
  11. package/dist/commands/core-session-journal-native.js +158 -34
  12. package/dist/commands/feedback.d.ts +12 -0
  13. package/dist/commands/feedback.js +52 -0
  14. package/dist/commands/files-browse.d.ts +8 -0
  15. package/dist/commands/files-browse.js +83 -1
  16. package/dist/commands/install-global.d.ts +2 -0
  17. package/dist/commands/install-global.js +24 -1
  18. package/dist/commands/integrations-connect.js +1 -0
  19. package/dist/commands/integrations-manage.d.ts +2 -0
  20. package/dist/commands/integrations-manage.js +6 -4
  21. package/dist/commands/lanes-link.js +5 -3
  22. package/dist/commands/lanes-loop.js +7 -7
  23. package/dist/commands/lanes-questions.js +54 -4
  24. package/dist/commands/lanes-session-start.d.ts +12 -0
  25. package/dist/commands/lanes-session-start.js +93 -12
  26. package/dist/commands/lanes.js +2 -1
  27. package/dist/commands/meetings.d.ts +11 -0
  28. package/dist/commands/meetings.js +41 -11
  29. package/dist/commands/sync-held.d.ts +87 -0
  30. package/dist/commands/sync-held.js +191 -0
  31. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3830 -2812
  32. package/dist/hq-cloud-sync-runner/package.json +1 -1
  33. package/dist/index.js +21 -0
  34. package/dist/lanes-session-start-argv.d.ts +7 -0
  35. package/dist/lanes-session-start-argv.js +9 -0
  36. package/dist/lib/agent-kit/inbox-state.d.ts +5 -4
  37. package/dist/lib/agent-kit/inbox-state.js +24 -3
  38. package/dist/lib/bot/log.d.ts +3 -2
  39. package/dist/lib/bot/progress.d.ts +23 -1
  40. package/dist/lib/bot/progress.js +43 -5
  41. package/dist/lib/bot/run.js +2 -0
  42. package/dist/lib/bot/runtime/claude.d.ts +12 -0
  43. package/dist/lib/bot/runtime/claude.js +20 -0
  44. package/dist/lib/bot/runtime/index.d.ts +10 -1
  45. package/dist/lib/bot/runtime/index.js +14 -2
  46. package/dist/lib/bot/runtime/types.d.ts +8 -0
  47. package/dist/lib/doctor/checks/ps1-shims.d.ts +21 -0
  48. package/dist/lib/doctor/checks/ps1-shims.js +82 -0
  49. package/dist/lib/doctor/fix/apply.d.ts +13 -1
  50. package/dist/lib/doctor/fix/apply.js +38 -3
  51. package/dist/lib/doctor/fix/remediation.d.ts +9 -1
  52. package/dist/lib/doctor/fix/remediation.js +22 -0
  53. package/dist/lib/doctor/registry.js +2 -0
  54. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  55. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  56. package/dist/lib/lanes/links.d.ts +2 -1
  57. package/dist/lib/lanes/links.js +235 -17
  58. package/dist/lib/lanes/loop.d.ts +17 -0
  59. package/dist/lib/lanes/loop.js +235 -5
  60. package/dist/lib/lanes/questions.d.ts +1 -0
  61. package/dist/lib/lanes/registry-flag.d.ts +24 -0
  62. package/dist/lib/lanes/registry-flag.js +72 -6
  63. package/dist/lib/lanes/store.d.ts +4 -2
  64. package/dist/lib/lanes/store.js +8 -8
  65. package/dist/lib/windows/ps1-shims.d.ts +60 -0
  66. package/dist/lib/windows/ps1-shims.js +152 -0
  67. package/dist/utils/self-update.d.ts +2 -0
  68. package/dist/utils/self-update.js +8 -0
  69. package/dist/utils/staged-npm-install.js +13 -2
  70. package/dist/utils/token-keychain.d.ts +11 -8
  71. package/dist/utils/token-keychain.js +36 -15
  72. package/package.json +8 -8
  73. package/scripts/install-hook-engine.mjs +12 -0
@@ -20,6 +20,8 @@ export interface LinkOptions {
20
20
  timeout?: number;
21
21
  event?: string;
22
22
  input?: string;
23
+ ackMode?: string;
24
+ claims?: string;
23
25
  now?: () => Date;
24
26
  sleep?: (milliseconds: number) => Promise<void>;
25
27
  }
@@ -31,7 +33,6 @@ export declare function resolveLinkSessionId(root: string, explicit: string | un
31
33
  /** Cheap hook guard used before remote flag lookup or queue access. */
32
34
  export declare function linkedSessionIdFromHookInput(input: string): string;
33
35
  export declare function isLinkedSession(root: string, sid: string): boolean;
34
- /** Claims pending files by renaming the whole queue, matching conduct-inbox's no-loss drain. */
35
36
  export declare function drainLinkQueue(inbox: string): string;
36
37
  export declare function runLaneLink(subcommand: string, options: LinkOptions): Promise<LinkResult>;
37
38
  /** Apply the default-on feature gate contract at the command boundary. */
@@ -2,6 +2,8 @@ import { randomBytes } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { appendDropboxMessageAtPath } from "./dropbox.js";
5
+ import { withLaneRecordFileLock } from "./store.js";
6
+ import { LanesError } from "./errors.js";
5
7
  export const LANE_SESSION_LINKS_FLAG = "lanes.session-links";
6
8
  const ID_PATTERN = /^[A-Za-z0-9._-]+$/;
7
9
  export class LaneLinkError extends Error {
@@ -132,6 +134,17 @@ function resolvePointer(root, sid, role, options) {
132
134
  function queueDir(root, link, child) {
133
135
  return path.join(rootDir(root), link, child ? "children" : "up", ...(child ? [child] : []), "inbox");
134
136
  }
137
+ function withLinkQueueLock(inbox, fn) {
138
+ try {
139
+ return withLaneRecordFileLock(path.join(inbox, "queue"), fn, { reclaimByAge: false });
140
+ }
141
+ catch (error) {
142
+ if (error instanceof LanesError && error.error === "store_lock_timeout") {
143
+ throw new LaneLinkError("queue_busy", "Conductor queue busy; instructions retained");
144
+ }
145
+ throw error;
146
+ }
147
+ }
135
148
  function sendQueue(root, link, child, text) {
136
149
  const inbox = queueDir(root, link, child);
137
150
  const pending = path.join(inbox, "pending");
@@ -139,10 +152,12 @@ function sendQueue(root, link, child, text) {
139
152
  // Reuse hq monitor's session-inbox append primitive; the pending directory is
140
153
  // still claimed as a whole below, keeping conduct-inbox's no-loss handoff.
141
154
  const cleanText = text.replaceAll("\0", "").replaceAll("\u000b", "").replaceAll("\u000c", "").replaceAll("\r", "");
142
- appendDropboxMessageAtPath(pending, { at: new Date().toISOString(), text: cleanText });
155
+ withLinkQueueLock(inbox, () => {
156
+ appendDropboxMessageAtPath(pending, { at: new Date().toISOString(), text: cleanText });
157
+ });
143
158
  }
144
159
  /** Claims pending files by renaming the whole queue, matching conduct-inbox's no-loss drain. */
145
- export function drainLinkQueue(inbox) {
160
+ function drainLinkQueueUnlocked(inbox) {
146
161
  const pending = path.join(inbox, "pending");
147
162
  const claimedRoot = path.join(inbox, "claimed");
148
163
  // Recover claims interrupted before the delivery result was committed. This is
@@ -190,16 +205,197 @@ export function drainLinkQueue(inbox) {
190
205
  fs.writeFileSync(path.join(claim, ".consumed"), "delivered\n", { mode: 0o600 });
191
206
  return messages;
192
207
  }
208
+ export function drainLinkQueue(inbox) {
209
+ return withLinkQueueLock(inbox, () => drainLinkQueueUnlocked(inbox));
210
+ }
211
+ /** Claims remain replayable until the recipient explicitly accepts instructions.
212
+ * This is at-least-once transport, not proof of a native turn or execution.
213
+ */
214
+ function claimLinkQueueUnlocked(inbox) {
215
+ const pending = path.join(inbox, "pending");
216
+ const claimed = path.join(inbox, "unaccepted");
217
+ fs.mkdirSync(claimed, { recursive: true });
218
+ if (fs.existsSync(path.join(pending, "inbox.jsonl"))) {
219
+ // Allocate while holding the queue lock. Wall time alone can repeat or
220
+ // move backward, and pid/random suffixes do not preserve claim order.
221
+ let serial = 0n;
222
+ for (const previous of fs.readdirSync(claimed)) {
223
+ const prefix = /^(\d{1,16})-/.exec(previous)?.[1];
224
+ if (prefix && BigInt(prefix) > serial)
225
+ serial = BigInt(prefix);
226
+ }
227
+ const wall = Date.now();
228
+ if (!Number.isSafeInteger(wall) || wall < 0)
229
+ throw new LaneLinkError("claim_clock_invalid", "Cannot allocate a safe receipt serial");
230
+ serial = BigInt(wall) > serial ? BigInt(wall) : serial + 1n;
231
+ if (serial > 9999999999999999n)
232
+ throw new LaneLinkError("claim_serial_exhausted", "Receipt serial exhausted; instructions retained");
233
+ const id = `${serial.toString().padStart(16, "0")}-${process.pid}-${randomBytes(8).toString("hex")}`;
234
+ const dir = path.join(claimed, id);
235
+ fs.mkdirSync(dir);
236
+ // The shared queue lock joins the producer's open/write/close before this
237
+ // immutable snapshot is read. Keep the producer directory stable too.
238
+ try {
239
+ fs.renameSync(path.join(pending, "inbox.jsonl"), path.join(dir, "inbox.jsonl"));
240
+ }
241
+ catch (error) {
242
+ if (error.code !== "ENOENT")
243
+ throw error;
244
+ }
245
+ }
246
+ const claims = [];
247
+ const messages = [];
248
+ const ordered = fs.readdirSync(claimed).sort((a, b) => {
249
+ const left = /^(\d{1,16})-/.exec(a)?.[1];
250
+ const right = /^(\d{1,16})-/.exec(b)?.[1];
251
+ if (left && right && BigInt(left) !== BigInt(right))
252
+ return BigInt(left) < BigInt(right) ? -1 : 1;
253
+ return a.localeCompare(b);
254
+ });
255
+ for (const id of ordered) {
256
+ if (!safeId(id))
257
+ continue;
258
+ const dir = path.join(claimed, id);
259
+ if (fs.existsSync(path.join(dir, ".consumed")))
260
+ continue;
261
+ let text;
262
+ try {
263
+ text = fs.readFileSync(path.join(dir, "inbox.jsonl"), "utf8");
264
+ }
265
+ catch (error) {
266
+ if (error.code === "ENOENT")
267
+ continue;
268
+ throw error;
269
+ }
270
+ const rows = [];
271
+ const invalidRows = [];
272
+ text.split(/\r?\n/).forEach((line, index) => {
273
+ if (!line)
274
+ return;
275
+ try {
276
+ const row = JSON.parse(line);
277
+ if (!row || typeof row !== "object" || typeof row.text !== "string")
278
+ throw new Error("invalid row");
279
+ rows.push(row.text);
280
+ }
281
+ catch {
282
+ invalidRows.push(index + 1);
283
+ }
284
+ });
285
+ if (invalidRows.length) {
286
+ // The immutable original is the quarantine evidence. Record positions,
287
+ // never malformed contents, and retain both even after valid rows' ACK.
288
+ const audit = path.join(dir, "invalid-rows.json");
289
+ const expectedAudit = JSON.stringify({ version: 1, rows: invalidRows }) + "\n";
290
+ let previousAudit;
291
+ try {
292
+ previousAudit = fs.readFileSync(audit, "utf8");
293
+ }
294
+ catch (error) {
295
+ if (error.code !== "ENOENT")
296
+ throw error;
297
+ }
298
+ if (previousAudit !== expectedAudit) {
299
+ const temporary = `${audit}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
300
+ try {
301
+ const fd = fs.openSync(temporary, "wx", 0o600);
302
+ try {
303
+ fs.writeFileSync(fd, expectedAudit);
304
+ fs.fsyncSync(fd);
305
+ }
306
+ finally {
307
+ fs.closeSync(fd);
308
+ }
309
+ fs.renameSync(temporary, audit);
310
+ }
311
+ finally {
312
+ fs.rmSync(temporary, { force: true });
313
+ }
314
+ }
315
+ // Retry all durability barriers even if a previous attempt wrote the
316
+ // audit but failed before publishing its directory entry durably.
317
+ for (const file of [path.join(dir, "inbox.jsonl"), audit]) {
318
+ const fd = fs.openSync(file, "r");
319
+ try {
320
+ fs.fsyncSync(fd);
321
+ }
322
+ finally {
323
+ fs.closeSync(fd);
324
+ }
325
+ }
326
+ for (const directory of [dir, claimed]) {
327
+ try {
328
+ const fd = fs.openSync(directory, "r");
329
+ try {
330
+ fs.fsyncSync(fd);
331
+ }
332
+ finally {
333
+ fs.closeSync(fd);
334
+ }
335
+ }
336
+ catch (error) {
337
+ // Same known unsupported directory-flush codes as lanes store.
338
+ // File flush errors and unknown directory errors remain fatal.
339
+ if (!["EISDIR", "EINVAL", "ENOTSUP"].includes(error.code ?? ""))
340
+ throw error;
341
+ }
342
+ }
343
+ }
344
+ if (!rows.length)
345
+ continue;
346
+ claims.push(id);
347
+ messages.push(...rows);
348
+ }
349
+ return { messages: messages.join("\n"), claims };
350
+ }
351
+ function claimLinkQueue(inbox) {
352
+ return withLinkQueueLock(inbox, () => claimLinkQueueUnlocked(inbox));
353
+ }
354
+ function acknowledgeLinkClaims(inbox, claims) {
355
+ return withLinkQueueLock(inbox, () => acknowledgeLinkClaimsUnlocked(inbox, claims));
356
+ }
357
+ function acknowledgeLinkClaimsUnlocked(inbox, claims) {
358
+ if (!Array.isArray(claims) || !claims.length || claims.some(id => typeof id !== "string" || !safeId(id))) {
359
+ throw new LaneLinkError("claim_invalid", "An exact nonempty claim receipt is required");
360
+ }
361
+ // Validate the entire receipt before changing any claim. Never resolve paths
362
+ // outside the current session's authoritative queue.
363
+ for (const id of claims) {
364
+ if (!fs.existsSync(path.join(inbox, "unaccepted", id, "inbox.jsonl"))) {
365
+ throw new LaneLinkError("claim_not_found", "Claim does not belong to this queue");
366
+ }
367
+ }
368
+ for (const id of claims) {
369
+ fs.writeFileSync(path.join(inbox, "unaccepted", id, ".consumed"), "recipient-accepted\n", { mode: 0o600 });
370
+ }
371
+ }
372
+ function countQuarantined(inbox) {
373
+ const claims = path.join(inbox, "unaccepted");
374
+ if (!fs.existsSync(claims))
375
+ return 0;
376
+ return fs.readdirSync(claims).filter(id => safeId(id) && fs.existsSync(path.join(claims, id, "invalid-rows.json"))).length;
377
+ }
193
378
  function countPending(inbox) {
194
- const payload = path.join(inbox, "pending", "inbox.jsonl");
195
- try {
196
- return fs.readFileSync(payload, "utf8").split(/\r?\n/).filter(Boolean).length;
379
+ const paths = [path.join(inbox, "pending", "inbox.jsonl")];
380
+ for (const name of ["claimed", "unaccepted"]) {
381
+ const claimed = path.join(inbox, name);
382
+ if (fs.existsSync(claimed))
383
+ for (const id of fs.readdirSync(claimed)) {
384
+ if (!fs.existsSync(path.join(claimed, id, ".consumed")))
385
+ paths.push(path.join(claimed, id, "inbox.jsonl"));
386
+ }
197
387
  }
198
- catch (error) {
199
- if (error.code === "ENOENT")
200
- return 0;
201
- throw error;
388
+ let count = 0;
389
+ for (const file of paths) {
390
+ try {
391
+ count += fs.readFileSync(file, "utf8").split(/\r?\n/).filter(Boolean).length;
392
+ }
393
+ catch (error) {
394
+ if (error.code !== "ENOENT")
395
+ throw error;
396
+ }
202
397
  }
398
+ return count;
203
399
  }
204
400
  function readText(options) {
205
401
  if (options.textFile) {
@@ -244,7 +440,7 @@ function listChildren(root, dir) {
244
440
  if (!fs.existsSync(metaPath))
245
441
  return [];
246
442
  const unread = countPending(path.join(cdir, "inbox"));
247
- return [{ ...readJson(metaPath), undelivered: unread }];
443
+ return [{ ...readJson(metaPath), undelivered: unread, quarantined_claims: countQuarantined(path.join(cdir, "inbox")) }];
248
444
  });
249
445
  }
250
446
  async function waitForReport(dir, timeoutSeconds, sleep) {
@@ -288,6 +484,7 @@ export async function runLaneLink(subcommand, options) {
288
484
  const root = options.hqRoot;
289
485
  let sid = "";
290
486
  let childContext = false;
487
+ let stopHookActive = false;
291
488
  if (subcommand === "_deliver") {
292
489
  let payload = {};
293
490
  try {
@@ -299,6 +496,7 @@ export async function runLaneLink(subcommand, options) {
299
496
  const value = payload.session_id ?? payload.sessionId;
300
497
  sid = typeof value === "string" && safeId(value) ? value : "";
301
498
  childContext = isChildHookPayload(payload);
499
+ stopHookActive = payload.stop_hook_active === true || payload.stopHookActive === true;
302
500
  }
303
501
  const requireSid = () => sid || (sid = resolveLinkSessionId(root, options.sessionId, env));
304
502
  const links = rootDir(root);
@@ -307,21 +505,41 @@ export async function runLaneLink(subcommand, options) {
307
505
  // drain the root's queue into the subagent, so leave the message queued.
308
506
  if (!sid || childContext || !["PostToolUse", "Stop"].includes(options.event ?? ""))
309
507
  return {};
310
- if (options.event === "Stop" && env.HQ_STOP_DECISION_DELIVERABLE === "0")
508
+ if (options.event === "Stop" && (stopHookActive || env.HQ_STOP_DECISION_DELIVERABLE === "0"))
311
509
  return {};
312
510
  const pointer = readPointer(root, sid);
313
511
  if (!pointer)
314
512
  return {};
315
513
  const target = hookMessage(root, pointer);
316
- const messages = drainLinkQueue(target.inbox);
514
+ if (options.ackMode !== undefined && options.ackMode !== "explicit")
515
+ throw new LaneLinkError("ack_mode_invalid", "Unsupported acknowledgement mode");
516
+ const claim = options.ackMode === "explicit" ? claimLinkQueue(target.inbox) : undefined;
517
+ const messages = claim ? claim.messages : drainLinkQueue(target.inbox);
317
518
  if (!messages)
318
519
  return {};
319
- const body = target.role === "conductor"
520
+ const receipt = claim ? { _hqLinkDelivery: { version: 1, sessionId: sid, link: pointer.link, child: pointer.name ?? null, claims: claim.claims } } : {};
521
+ const acceptance = claim ? `\n\n[conduct receipt] Stable instruction receipt IDs: ${claim.claims.join(",")}. Treat replayed IDs as the same instruction, not new execution authority. After accepting these instructions, acknowledge receipt once with: hq lanes link ack --session-id ${sid} --link ${pointer.link}${pointer.name ? ` --child ${pointer.name}` : ""} --claims ${claim.claims.join(",")}. Receipt acceptance does not approve actions, answer owner pickers, or mean work is completed.` : "";
522
+ const body = (target.role === "conductor"
320
523
  ? `[conduct] Sessions you are conducting reported while you were working.\nEach report names the session it came from. Relay what the operator needs to know, and answer a session with: hq lanes link send --child <name> --text '...'\n\n${messages}`
321
- : `[conduct] The session conducting you sent this while you were working.\nTreat it as a new instruction from the operator, taking precedence over your current task where the two conflict. When you have something to tell the conductor - a result, a blocker, a question - send it with: hq lanes link report --state <working|blocked|idle|done> --text '...'\n\n${messages}`;
524
+ : `[conduct] The session conducting you sent this while you were working.\nTreat it as a new instruction from the operator, taking precedence over your current task where the two conflict. When you have something to tell the conductor - a result, a blocker, a question - send it with: hq lanes link report --state <working|blocked|idle|done> --text '...'\n\n${messages}`) + acceptance;
322
525
  return options.event === "PostToolUse"
323
- ? { output: { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: body } } }
324
- : { output: { decision: "block", reason: body } };
526
+ ? { output: { ...receipt, hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: body } } }
527
+ : { output: { ...receipt, decision: "block", reason: body } };
528
+ }
529
+ if (subcommand === "capabilities") {
530
+ return { output: { protocol: "conduct-recipient-acceptance/1", explicitClaim: true, recipientAck: true, idleWake: false } };
531
+ }
532
+ if (subcommand === "ack") {
533
+ const currentSid = requireSid();
534
+ const pointer = readPointer(root, currentSid);
535
+ if (!pointer)
536
+ throw new LaneLinkError("session_unlinked", "Session is not linked");
537
+ if (!options.link || options.link !== pointer.link || (options.child ?? null) !== (pointer.name ?? null)) {
538
+ throw new LaneLinkError("claim_owner_invalid", "Receipt does not match the current link/child binding");
539
+ }
540
+ const claims = options.claims?.split(",");
541
+ acknowledgeLinkClaims(hookMessage(root, pointer).inbox, claims);
542
+ return { output: { acknowledged: true, acceptance: "recipient_instruction_only" } };
325
543
  }
326
544
  if (subcommand === "open") {
327
545
  const sessionId = requireSid();
@@ -438,7 +656,7 @@ export async function runLaneLink(subcommand, options) {
438
656
  const dir = linkPath(root, link);
439
657
  const meta = readJson(path.join(dir, "meta.json"));
440
658
  const unread = countPending(path.join(dir, "up", "inbox"));
441
- return { output: { ...meta, unread_reports: unread, children: listChildren(root, dir) } };
659
+ return { output: { ...meta, unread_reports: unread, quarantined_reports: countQuarantined(path.join(dir, "up", "inbox")), children: listChildren(root, dir) } };
442
660
  }
443
661
  if (subcommand === "leave") {
444
662
  const sessionId = requireSid();
@@ -48,6 +48,10 @@ export interface LoopStatus {
48
48
  stall_counts: Record<string, number>;
49
49
  pid?: number;
50
50
  updated_at: string;
51
+ handoff_hold?: {
52
+ envelope_id: string;
53
+ question_id: string;
54
+ };
51
55
  }
52
56
  export interface LoopResult {
53
57
  schema_version: typeof LOOP_SCHEMA;
@@ -61,6 +65,7 @@ export interface LoopResult {
61
65
  resumed?: boolean;
62
66
  fresh_fallback?: boolean;
63
67
  question_id?: string;
68
+ hold_reason?: "handoff_needs_repair";
64
69
  decision?: {
65
70
  question_id: string;
66
71
  choice: string;
@@ -112,6 +117,7 @@ export interface LoopRunnerOptions {
112
117
  attempt: number;
113
118
  error: string;
114
119
  questionId: string;
120
+ reason?: "handoff_needs_repair";
115
121
  }): Promise<void> | void;
116
122
  defaultDeadlineSeconds?: number;
117
123
  now?: () => Date;
@@ -131,8 +137,19 @@ interface StoredEnvelope {
131
137
  queued_at: string;
132
138
  deadline_budget_seconds?: number;
133
139
  stall_attempts?: number;
140
+ accepted_handoff?: {
141
+ output: string;
142
+ sha256: string;
143
+ };
144
+ handoff_retry?: boolean;
134
145
  envelope: LoopEnvelope;
135
146
  }
147
+ export declare const HANDOFF_HOLD_CHOICES: readonly ["Use the repaired handoff without running the phase again", "Retry the phase; I understand external effects may repeat", "Keep this phase held"];
148
+ export declare function loopHandoffHoldIdentity(runDir: string): {
149
+ envelope_id: string;
150
+ question_id: string;
151
+ unresolved: boolean;
152
+ } | undefined;
136
153
  /** Validate both hq-cli phase envelopes and the conduct protocol §8 shape. */
137
154
  export declare function validateLoopEnvelope(value: unknown): LoopEnvelope;
138
155
  export declare function validateSection8Handoff(value: unknown, envelope: Section8PhaseEnvelope): Section8Handoff;