@indigoai-us/hq-cli 5.345.161 → 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.
@@ -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;
@@ -1,5 +1,5 @@
1
1
  /** Durable envelope queue and runtime primitives for conduct worker lanes. */
2
- import { randomUUID } from "node:crypto";
2
+ import { createHash, randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
5
  import { providerSupportsSessionResume } from "./delivery.js";
@@ -14,6 +14,174 @@ export function loopAdmissionPath(state, hasPendingResume, initialAdmission) {
14
14
  return "deferred";
15
15
  return "resume";
16
16
  }
17
+ export const HANDOFF_HOLD_CHOICES = [
18
+ "Use the repaired handoff without running the phase again",
19
+ "Retry the phase; I understand external effects may repeat",
20
+ "Keep this phase held",
21
+ ];
22
+ function handoffHoldPath(runDir) { return path.join(runDir, "loop", "handoff-hold.json"); }
23
+ function readHandoffHold(runDir) {
24
+ const file = handoffHoldPath(runDir);
25
+ if (!fs.existsSync(file))
26
+ return undefined;
27
+ const hold = JSON.parse(new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(readHandoffBytes(file, LOOP_ENVELOPE_LIMIT * 8)));
28
+ if (!isRecord(hold) || hold.schema !== "hq-loop-handoff-hold.v1" || !nonEmpty(hold.envelope_id) || !nonEmpty(hold.question_id)) {
29
+ throw new Error("Invalid durable handoff hold; operator investigation required");
30
+ }
31
+ return hold;
32
+ }
33
+ async function notifyHandoffHold(options, hold, envelope) {
34
+ if (hold.notified || hold.resolution || !options.raiseQuestion)
35
+ return;
36
+ const record = envelope ? undefined : ["stalled", "active"].flatMap((dir) => envelopeFiles(path.join(options.runDir, "loop", dir)))
37
+ .map(fileEnvelope).find((entry) => entry.envelope_id === hold.envelope_id);
38
+ const heldEnvelope = envelope ?? record?.envelope;
39
+ if (!heldEnvelope)
40
+ throw new Error("Held handoff envelope is missing; cannot deliver its repair question");
41
+ await options.raiseQuestion({ envelope: heldEnvelope, envelopeId: hold.envelope_id, attempt: 1, error: hold.error, questionId: hold.question_id, reason: "handoff_needs_repair" });
42
+ const current = readHandoffHold(options.runDir);
43
+ if (current?.question_id === hold.question_id)
44
+ atomicJson(handoffHoldPath(options.runDir), { ...current, notified: true });
45
+ }
46
+ export function loopHandoffHoldIdentity(runDir) {
47
+ const hold = readHandoffHold(runDir);
48
+ return hold ? { envelope_id: hold.envelope_id, question_id: hold.question_id, unresolved: !hold.resolution } : undefined;
49
+ }
50
+ function readHandoffBytes(file, limit = LOOP_ENVELOPE_LIMIT) {
51
+ const before = fs.lstatSync(file);
52
+ if (!before.isFile())
53
+ throw new Error("Repaired handoff must be a bounded regular file");
54
+ if (!Number.isSafeInteger(before.ino) || before.ino <= 0 || !Number.isSafeInteger(before.dev))
55
+ throw new Error("Cannot establish handoff file identity on this filesystem");
56
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
57
+ try {
58
+ const stat = fs.fstatSync(fd);
59
+ const current = fs.lstatSync(file);
60
+ if (!current.isFile() || stat.dev !== before.dev || stat.ino !== before.ino || current.dev !== stat.dev || current.ino !== stat.ino)
61
+ throw new Error("Handoff file identity changed while opening it");
62
+ if (!stat.isFile() || stat.size > limit)
63
+ throw new Error("Repaired handoff must be a bounded regular file");
64
+ const bytes = Buffer.alloc(limit + 1);
65
+ let count = 0;
66
+ while (count < bytes.length) {
67
+ const read = fs.readSync(fd, bytes, count, bytes.length - count, null);
68
+ if (read === 0)
69
+ break;
70
+ count += read;
71
+ }
72
+ if (count > limit)
73
+ throw new Error("Repaired handoff exceeds the 1 MiB limit");
74
+ return bytes.subarray(0, count);
75
+ }
76
+ finally {
77
+ fs.closeSync(fd);
78
+ }
79
+ }
80
+ function validateAcceptedHandoff(output, envelope) {
81
+ if (Buffer.byteLength(output, "utf8") > LOOP_ENVELOPE_LIMIT)
82
+ throw new Error("Repaired handoff exceeds the 1 MiB limit");
83
+ if (envelope.schema !== PHASE_ENVELOPE_SCHEMA)
84
+ throw new Error("Repair hold requires a section 8 phase");
85
+ const phase = envelope;
86
+ const handoff = validateSection8Handoff(JSON.parse(output), phase);
87
+ if (handoff.worker_id !== phase.worker_id || handoff.status !== "passed") {
88
+ throw new Error("Repaired handoff must match the worker and have passed status");
89
+ }
90
+ const criteria = phase.acceptance_criteria;
91
+ if (!Array.isArray(criteria) || !criteria.every((item) => typeof item === "string"))
92
+ throw new Error("Held phase requires string acceptance criteria");
93
+ if (!Array.isArray(handoff.ac_evidence))
94
+ throw new Error("Repaired handoff requires acceptance-criterion evidence");
95
+ for (let index = 0; index < criteria.length; index += 1) {
96
+ const evidence = handoff.ac_evidence.filter((item) => isRecord(item) && item.index === index);
97
+ if (evidence.length !== 1)
98
+ throw new Error(`Repaired handoff requires exactly one evidence entry for criterion ${index}`);
99
+ const item = evidence[0];
100
+ if (item.criterion !== criteria[index] || item.met !== true || !nonEmpty(item.evidence)) {
101
+ throw new Error(`Repaired handoff must copy criterion ${index} verbatim with met=true and string evidence`);
102
+ }
103
+ }
104
+ for (const item of handoff.ac_evidence) {
105
+ if (!isRecord(item) || !Number.isInteger(item.index) || Number(item.index) < 0 || Number(item.index) >= criteria.length) {
106
+ throw new Error("Repaired handoff contains an unknown criterion index");
107
+ }
108
+ }
109
+ return handoff;
110
+ }
111
+ function applyHandoffHoldAnswer(input) {
112
+ const release = acquireEnqueueLock(input.runDir);
113
+ try {
114
+ const hold = readHandoffHold(input.runDir);
115
+ if (!hold || hold.question_id !== input.questionId)
116
+ throw new Error("Handoff hold identity changed");
117
+ if (!HANDOFF_HOLD_CHOICES.includes(input.choice))
118
+ throw new Error("Choose an explicit handoff repair disposition");
119
+ if (loopStatus(input.runDir, path.basename(input.runDir)).state === "stopped")
120
+ throw new Error("An intentionally stopped loop cannot resume from a handoff answer");
121
+ if (fs.existsSync(path.join(input.runDir, "loop", "runner.lock")))
122
+ throw new Error("Handoff controller has not released its lease yet; retry the same answer after it exits");
123
+ const resultPath = path.join(resultsDir(input.runDir), resultName(hold.envelope_id));
124
+ const result = fs.existsSync(resultPath) ? readJson(resultPath) : undefined;
125
+ if (input.choice === HANDOFF_HOLD_CHOICES[2]) {
126
+ if (hold.resolution)
127
+ throw new Error("A handoff disposition is already being committed");
128
+ if (isRecord(result))
129
+ atomicJson(resultPath, { ...result, decision: { question_id: input.questionId, choice: input.choice, at: input.at, ...(input.notes ? { notes: input.notes } : {}) } });
130
+ return { lane_id: path.basename(input.runDir), envelope_id: hold.envelope_id, action: "parked" };
131
+ }
132
+ if (hold.resolution && hold.resolution.choice !== input.choice)
133
+ throw new Error("A different handoff disposition is already recorded");
134
+ if (!hold.resolution) {
135
+ if (!isRecord(result) || result.hold_reason !== "handoff_needs_repair" || result.question_id !== input.questionId || result.envelope_id !== hold.envelope_id)
136
+ throw new Error("Handoff result identity changed");
137
+ const held = envelopeFiles(path.join(input.runDir, "loop", "stalled")).find((file) => fileEnvelope(file).envelope_id === hold.envelope_id);
138
+ if (!held)
139
+ throw new Error("Held handoff envelope is missing");
140
+ const record = fileEnvelope(held);
141
+ if (input.choice === HANDOFF_HOLD_CHOICES[0]) {
142
+ const externalPath = record.envelope.result_path;
143
+ if (typeof externalPath !== "string" || !externalPath.trim())
144
+ throw new Error("Held phase has no handoff path");
145
+ const output = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(readHandoffBytes(externalPath));
146
+ validateAcceptedHandoff(output, record.envelope);
147
+ delete record.handoff_retry;
148
+ record.accepted_handoff = { output, sha256: createHash("sha256").update(output).digest("hex") };
149
+ }
150
+ else {
151
+ delete record.accepted_handoff;
152
+ record.handoff_retry = true;
153
+ }
154
+ record.stall_attempts = 0;
155
+ hold.resolution = { choice: input.choice, at: input.at, ...(input.notes ? { notes: input.notes } : {}), pending_name: `0000000000000-answer-${path.basename(held)}`, record };
156
+ // Durable authorization precedes every queue/result move. The hold gates
157
+ // raw controllers until all those moves have completed, including retries.
158
+ atomicJson(handoffHoldPath(input.runDir), hold);
159
+ }
160
+ const resolution = hold.resolution;
161
+ const pending = path.join(pendingDir(input.runDir), resolution.pending_name);
162
+ if (path.basename(resolution.pending_name) !== resolution.pending_name || resolution.record.envelope_id !== hold.envelope_id)
163
+ throw new Error("Invalid handoff disposition record");
164
+ const held = envelopeFiles(path.join(input.runDir, "loop", "stalled")).find((file) => fileEnvelope(file).envelope_id === hold.envelope_id);
165
+ if (held) {
166
+ atomicJson(held, resolution.record);
167
+ fs.mkdirSync(pendingDir(input.runDir), { recursive: true });
168
+ fs.renameSync(held, pending);
169
+ }
170
+ else if (!fs.existsSync(pending) || JSON.stringify(fileEnvelope(pending)) !== JSON.stringify(resolution.record)) {
171
+ throw new Error("Disposition envelope changed before it could be resumed");
172
+ }
173
+ if (fs.existsSync(resultPath)) {
174
+ atomicJson(resultPath, { ...readJson(resultPath), decision: { question_id: input.questionId, choice: resolution.choice, at: resolution.at, ...(resolution.notes ? { notes: resolution.notes } : {}) } });
175
+ fs.renameSync(resultPath, asidePath(resultPath, "stalled"));
176
+ }
177
+ persistStatus(input.runDir, path.basename(input.runDir), { state: "waiting" });
178
+ fs.unlinkSync(handoffHoldPath(input.runDir));
179
+ return { lane_id: path.basename(input.runDir), envelope_id: hold.envelope_id, action: "requeued" };
180
+ }
181
+ finally {
182
+ release();
183
+ }
184
+ }
17
185
  function asidePath(file, marker) {
18
186
  const parsed = path.parse(file);
19
187
  let suffix = 1;
@@ -520,6 +688,11 @@ export function findLoopStallResult(runDir, questionId) {
520
688
  return resultPath ? readJson(resultPath) : undefined;
521
689
  }
522
690
  export function applyLoopStallAnswer(input) {
691
+ const hold = readHandoffHold(input.runDir);
692
+ if (hold?.question_id === input.questionId)
693
+ return applyHandoffHoldAnswer(input);
694
+ if (hold)
695
+ throw new Error("A different handoff hold must be resolved first");
523
696
  const result = findLoopStallResult(input.runDir, input.questionId);
524
697
  if (!result) {
525
698
  for (const name of stalledResultArchives(input.runDir)) {
@@ -586,12 +759,14 @@ export function loopStatus(runDir, laneId) {
586
759
  const stallCounts = isRecord(loop.stall_counts)
587
760
  ? Object.fromEntries(Object.entries(loop.stall_counts).filter((entry) => typeof entry[1] === "number"))
588
761
  : {};
762
+ const hold = readHandoffHold(runDir);
589
763
  const processState = loop.state === "running" || loop.state === "parked" || loop.state === "stopped" || loop.state === "failed" ? loop.state : "waiting";
590
764
  return {
591
- mode: "loop", lane_id: laneId, state: processState, queue_depth: queueDepth,
765
+ mode: "loop", lane_id: laneId, state: hold && processState !== "stopped" ? "parked" : processState, queue_depth: queueDepth,
592
766
  active_envelope_id: active?.envelope_id ?? null,
593
767
  active_phase: active ? phaseName(active.envelope, active.envelope_id) : null,
594
768
  stall_counts: stallCounts,
769
+ ...(hold ? { handoff_hold: { envelope_id: hold.envelope_id, question_id: hold.question_id } } : {}),
595
770
  ...(typeof loop.pid === "number" ? { pid: loop.pid } : {}),
596
771
  updated_at: typeof loop.updated_at === "string" ? loop.updated_at : new Date(0).toISOString(),
597
772
  };
@@ -746,6 +921,13 @@ function abortAfter(ms, parent) {
746
921
  /** Run queued phases serially. Sessions are isolated by story and provider. */
747
922
  export async function runLoopLane(options) {
748
923
  const now = options.now ?? (() => new Date());
924
+ const held = readHandoffHold(options.runDir);
925
+ if (held) {
926
+ if (loopStatus(options.runDir, options.lane.lane_id).state === "stopped")
927
+ return { state: "stopped", completed: 0 };
928
+ await notifyHandoffHold(options, held);
929
+ return { state: "parked", completed: 0 };
930
+ }
749
931
  const defaultDeadlineSeconds = options.defaultDeadlineSeconds ?? 3600;
750
932
  const sessions = new Map();
751
933
  try {
@@ -766,6 +948,10 @@ export async function runLoopLane(options) {
766
948
  let completed = 0;
767
949
  fs.mkdirSync(options.runDir, { recursive: true });
768
950
  let releaseRunner = acquireRunnerLease(options.runDir);
951
+ if (readHandoffHold(options.runDir)) {
952
+ releaseRunner();
953
+ return { state: loopStatus(options.runDir, options.lane.lane_id).state === "stopped" ? "stopped" : "parked", completed: 0 };
954
+ }
769
955
  persistStatus(options.runDir, options.lane.lane_id, { pid: process.pid });
770
956
  try {
771
957
  const previous = readJson(loopPath(options.runDir));
@@ -821,7 +1007,7 @@ export async function runLoopLane(options) {
821
1007
  const provider = envelopeProvider(envelope, options.lane.provider);
822
1008
  const story = envelopeStory(envelope);
823
1009
  const sessionKey = `${story}\0${provider}`;
824
- const fresh = envelope.fresh === true || envelope.fresh_call === true;
1010
+ const fresh = record.handoff_retry === true || envelope.fresh === true || envelope.fresh_call === true;
825
1011
  const previousSession = sessions.get(sessionKey);
826
1012
  const startedAt = now().toISOString();
827
1013
  const startedAtMs = Date.parse(startedAt);
@@ -835,14 +1021,29 @@ export async function runLoopLane(options) {
835
1021
  atomicJson(file, record);
836
1022
  persistStatus(options.runDir, options.lane.lane_id, { state: "running", queue_depth: listLoopQueue(options.runDir).length, active_envelope_id: record.envelope_id, active_phase: phaseName(envelope, record.envelope_id) });
837
1023
  const timer = abortAfter(budget * 1000, options.signal);
1024
+ let malformedHandoff;
838
1025
  try {
839
1026
  let result;
840
1027
  let fallback = false;
841
1028
  try {
842
1029
  const resumable = options.canResume?.(provider) ?? providerSupportsSessionResume(provider);
843
- result = await options.execute({ lane: options.lane, envelope, envelopeId: record.envelope_id, signal: timer.signal, fresh: fresh || previousSession === undefined || !resumable, ...(fresh || previousSession === undefined || !resumable ? {} : { sessionId: previousSession }) });
1030
+ if (record.accepted_handoff) {
1031
+ malformedHandoff = "Accepted handoff changed before continuation";
1032
+ const externalPath = envelope.result_path;
1033
+ if (typeof externalPath !== "string" || createHash("sha256").update(readHandoffBytes(externalPath)).digest("hex") !== record.accepted_handoff.sha256)
1034
+ throw new Error(malformedHandoff);
1035
+ if (createHash("sha256").update(record.accepted_handoff.output).digest("hex") !== record.accepted_handoff.sha256)
1036
+ throw new Error("Accepted handoff hash changed");
1037
+ validateAcceptedHandoff(record.accepted_handoff.output, envelope);
1038
+ result = { output: record.accepted_handoff.output };
1039
+ malformedHandoff = undefined;
1040
+ }
1041
+ else
1042
+ result = await options.execute({ lane: options.lane, envelope, envelopeId: record.envelope_id, signal: timer.signal, fresh: fresh || previousSession === undefined || !resumable, ...(fresh || previousSession === undefined || !resumable ? {} : { sessionId: previousSession }) });
844
1043
  }
845
1044
  catch (error) {
1045
+ if (record.accepted_handoff)
1046
+ throw error;
846
1047
  const resumable = options.canResume?.(provider) ?? providerSupportsSessionResume(provider);
847
1048
  if (!fresh && previousSession && resumable && !timer.signal.aborted) {
848
1049
  fallback = true;
@@ -875,6 +1076,7 @@ export async function runLoopLane(options) {
875
1076
  }
876
1077
  }
877
1078
  catch (validationError) {
1079
+ malformedHandoff = validationError instanceof Error ? validationError.message : String(validationError);
878
1080
  if (options.repairInvalidHandoff !== false && !timer.signal.aborted) {
879
1081
  const repairError = validationError instanceof Error ? validationError.message : String(validationError);
880
1082
  const repaired = await options.execute({
@@ -899,6 +1101,7 @@ export async function runLoopLane(options) {
899
1101
  if (repairedValidated.worker_id !== envelope.worker_id) {
900
1102
  throw new Error(`handoff worker_id must match the queued phase envelope: ${repairError}`);
901
1103
  }
1104
+ malformedHandoff = undefined;
902
1105
  result = repaired;
903
1106
  success = {
904
1107
  ...success,
@@ -932,6 +1135,33 @@ export async function runLoopLane(options) {
932
1135
  persistStatus(options.runDir, options.lane.lane_id, { state: "waiting", queue_depth: listLoopQueue(options.runDir).length, active_envelope_id: null, active_phase: null });
933
1136
  return { state: "aborted", completed };
934
1137
  }
1138
+ if (malformedHandoff !== undefined) {
1139
+ const message = error instanceof Error ? error.message : String(error);
1140
+ const questionId = randomUUID();
1141
+ const hold = { schema: "hq-loop-handoff-hold.v1", envelope_id: record.envelope_id, question_id: questionId, error: `${malformedHandoff}; repair failed: ${message}` };
1142
+ try {
1143
+ atomicJson(handoffHoldPath(options.runDir), hold);
1144
+ const externalPath = envelope.result_path;
1145
+ if (typeof externalPath === "string" && fs.existsSync(externalPath)) {
1146
+ const stat = fs.lstatSync(externalPath);
1147
+ if (stat.isFile() && stat.size <= LOOP_ENVELOPE_LIMIT) {
1148
+ const backup = path.join(options.runDir, "loop", "handoff-original", `${resultName(record.envelope_id)}.${questionId}`);
1149
+ fs.mkdirSync(path.dirname(backup), { recursive: true });
1150
+ fs.writeFileSync(backup, readHandoffBytes(externalPath), { flag: "wx", mode: 0o600 });
1151
+ hold.original_copy = backup;
1152
+ atomicJson(handoffHoldPath(options.runDir), hold);
1153
+ }
1154
+ }
1155
+ writeResultAt(options.runDir, file, record, { schema_version: LOOP_SCHEMA, envelope_id: record.envelope_id, status: "stalled", hold_reason: "handoff_needs_repair", question_id: questionId, started_at: startedAt, finished_at: now().toISOString(), error: hold.error }, options.repairInvalidHandoff !== false);
1156
+ persistStatus(options.runDir, options.lane.lane_id, { state: "parked", queue_depth: listLoopQueue(options.runDir).length, active_envelope_id: null, active_phase: null });
1157
+ }
1158
+ finally {
1159
+ releaseRunner();
1160
+ clearLoopControllerPid(options.runDir, options.lane.lane_id, process.pid);
1161
+ }
1162
+ await notifyHandoffHold(options, hold, envelope);
1163
+ return { state: "parked", completed };
1164
+ }
935
1165
  if (!timer.signal.aborted) {
936
1166
  const failure = error instanceof Error ? error.message : String(error);
937
1167
  writeResultAt(options.runDir, file, record, { schema_version: LOOP_SCHEMA, envelope_id: record.envelope_id, status: "failed", started_at: startedAt, finished_at: now().toISOString(), error: failure }, options.repairInvalidHandoff !== false);
@@ -1035,7 +1265,7 @@ function writeResultAt(runDir, activeFile, record, result, includeHandoffValidat
1035
1265
  schema: PHASE_HANDOFF_SCHEMA,
1036
1266
  story_id: String(envelope.story_id), phase: String(envelope.phase),
1037
1267
  worker_id: String(envelope.worker_id), status: stalled ? "blocked" : "failed",
1038
- summary: stalled ? "Phase stalled twice and needs operator direction." : includeHandoffValidationError ? `Phase execution failed: ${result.error ?? "unknown error"}` : "Phase execution failed.",
1268
+ summary: result.hold_reason === "handoff_needs_repair" ? `Handoff needs repair: ${result.error ?? "invalid handoff"}` : stalled ? "Phase stalled twice and needs operator direction." : includeHandoffValidationError ? `Phase execution failed: ${result.error ?? "unknown error"}` : "Phase execution failed.",
1039
1269
  files_changed: [], commits: [],
1040
1270
  back_pressure: { tests: "skip", lint: "skip", typecheck: "skip", build: "skip" },
1041
1271
  context_for_next: [result.error ?? result.status, ...(result.question_id ? [`Decision question: ${result.question_id}`] : [])].join("\n"),
@@ -9,6 +9,7 @@ export interface QuestionBody {
9
9
  lane_id: string;
10
10
  envelope_id: string;
11
11
  attempt: number;
12
+ reason?: "handoff_needs_repair";
12
13
  };
13
14
  }
14
15
  export declare function assertQuestionBodySecretSafe(body: QuestionBody): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.161",
3
+ "version": "5.345.162",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -52,7 +52,7 @@
52
52
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
53
53
  "@aws-sdk/client-s3": "^3.1049.0",
54
54
  "@aws-sdk/dsql-signer": "^3.1146.0",
55
- "@indigoai-us/hq-cloud": "6.18.99",
55
+ "@indigoai-us/hq-cloud": "6.18.100",
56
56
  "@indigoai-us/hq-flags-client": "^0.1.2",
57
57
  "@modelcontextprotocol/sdk": "1.29.0",
58
58
  "@sentry/node": "^10.49.0",
@@ -114,11 +114,11 @@
114
114
  "*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
115
115
  },
116
116
  "optionalDependencies": {
117
- "@indigoai-us/hq-hook-win32-x64": "5.345.161",
118
- "@indigoai-us/hq-hook-win32-arm64": "5.345.161",
119
- "@indigoai-us/hq-hook-darwin-x64": "5.345.161",
120
- "@indigoai-us/hq-hook-darwin-arm64": "5.345.161",
121
- "@indigoai-us/hq-hook-linux-x64": "5.345.161",
122
- "@indigoai-us/hq-hook-linux-arm64": "5.345.161"
117
+ "@indigoai-us/hq-hook-win32-x64": "5.345.162",
118
+ "@indigoai-us/hq-hook-win32-arm64": "5.345.162",
119
+ "@indigoai-us/hq-hook-darwin-x64": "5.345.162",
120
+ "@indigoai-us/hq-hook-darwin-arm64": "5.345.162",
121
+ "@indigoai-us/hq-hook-linux-x64": "5.345.162",
122
+ "@indigoai-us/hq-hook-linux-arm64": "5.345.162"
123
123
  }
124
124
  }