@indigoai-us/hq-cli 5.345.56 → 5.345.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.57] — 2026-10-09
6
+ - Add `hq lanes create --loop` and `hq lanes enqueue` for persistent worker lanes that process FIFO phase envelopes, return generic results or section 8 handoffs, reuse sessions per story, retry one stall, and park a second stall for the senior. Loop behavior uses the default-on `lanes.conduct-loop` hq-flags kill switch. See `docs/lanes.md` for command output and exit codes.
7
+
8
+ - Add six-hour escalation for continuously blocked lanes, including durable cross-host inbox delivery and per-lane escalation history.
9
+ - Lane questions addressed to the current operator session record session-specific visibility on the owning host; unanswered questions cannot age past that session before it sees them, and reparenting to a different audience clears the marker. Visibility is separate from answer delivery so conflict-sidecar merging cannot suppress answer delivery. Outpost question show and answer lookups ignore malformed rows for unrelated question ids while still validating the requested record.
10
+
5
11
  ## [5.345.56] — 2026-10-09
6
12
  - Add `hq lanes reap` to report local lanes whose session senior is confirmed ended, clear stale pid markers on request, and keep process-tree stopping behind the `lanes.reap-stop` hq-flags key. SessionStart runs the report-only sweep detached and stores its report in host-specific user state.
7
13
  - Record privacy-limited `hq members invite` success and failure outcomes through the existing CLI feature telemetry event.
@@ -11654,6 +11654,10 @@ export declare const COMMAND_CATALOG: readonly [{
11654
11654
  readonly flags: "--fresh-thread";
11655
11655
  readonly description: "Skip worker continuity and start a cold provider session";
11656
11656
  readonly hidden: false;
11657
+ }, {
11658
+ readonly flags: "--loop";
11659
+ readonly description: "Keep one worker lane registered to process queued phase envelopes";
11660
+ readonly hidden: false;
11657
11661
  }, {
11658
11662
  readonly flags: "--provider <name>";
11659
11663
  readonly description: "claude | codex | grok";
@@ -11734,6 +11738,52 @@ export declare const COMMAND_CATALOG: readonly [{
11734
11738
  readonly hidden: false;
11735
11739
  }];
11736
11740
  readonly subcommands: readonly [];
11741
+ }, {
11742
+ readonly name: "_loop-worker";
11743
+ readonly description: "";
11744
+ readonly aliases: readonly [];
11745
+ readonly hidden: true;
11746
+ readonly helpOption: true;
11747
+ readonly helpCommand: false;
11748
+ readonly usage: "[options] <lane-id>";
11749
+ readonly helpText: readonly [];
11750
+ readonly arguments: readonly [{
11751
+ readonly name: "lane-id";
11752
+ readonly required: true;
11753
+ readonly variadic: false;
11754
+ readonly description: "Loop lane id";
11755
+ }];
11756
+ readonly options: readonly [{
11757
+ readonly flags: "--json";
11758
+ readonly description: "JSON on stdout (always)";
11759
+ readonly hidden: false;
11760
+ }];
11761
+ readonly subcommands: readonly [];
11762
+ }, {
11763
+ readonly name: "enqueue";
11764
+ readonly description: "Validate and queue one phase envelope for a loop lane";
11765
+ readonly aliases: readonly [];
11766
+ readonly hidden: false;
11767
+ readonly helpOption: true;
11768
+ readonly helpCommand: false;
11769
+ readonly usage: "[options] <lane-id>";
11770
+ readonly helpText: readonly [];
11771
+ readonly arguments: readonly [{
11772
+ readonly name: "lane-id";
11773
+ readonly required: true;
11774
+ readonly variadic: false;
11775
+ readonly description: "Loop lane id";
11776
+ }];
11777
+ readonly options: readonly [{
11778
+ readonly flags: "--envelope <file>";
11779
+ readonly description: "Generic phase or hq-phase-envelope/v1 JSON file";
11780
+ readonly hidden: false;
11781
+ }, {
11782
+ readonly flags: "--json";
11783
+ readonly description: "JSON on stdout (always)";
11784
+ readonly hidden: false;
11785
+ }];
11786
+ readonly subcommands: readonly [];
11737
11787
  }, {
11738
11788
  readonly name: "reparent-target";
11739
11789
  readonly description: "";
@@ -12606,6 +12656,40 @@ export declare const COMMAND_CATALOG: readonly [{
12606
12656
  readonly hidden: false;
12607
12657
  }];
12608
12658
  readonly subcommands: readonly [];
12659
+ }, {
12660
+ readonly name: "_enqueue-blocked-escalation";
12661
+ readonly description: "";
12662
+ readonly aliases: readonly [];
12663
+ readonly hidden: true;
12664
+ readonly helpOption: true;
12665
+ readonly helpCommand: false;
12666
+ readonly usage: "[options] <recipient-kind> <recipient-id>";
12667
+ readonly helpText: readonly [];
12668
+ readonly arguments: readonly [{
12669
+ readonly name: "recipient-kind";
12670
+ readonly required: true;
12671
+ readonly variadic: false;
12672
+ readonly description: "";
12673
+ }, {
12674
+ readonly name: "recipient-id";
12675
+ readonly required: true;
12676
+ readonly variadic: false;
12677
+ readonly description: "";
12678
+ }];
12679
+ readonly options: readonly [{
12680
+ readonly flags: "--text <text>";
12681
+ readonly description: "Escalation notice";
12682
+ readonly hidden: false;
12683
+ }, {
12684
+ readonly flags: "--message-id <id>";
12685
+ readonly description: "Idempotency key";
12686
+ readonly hidden: false;
12687
+ }, {
12688
+ readonly flags: "--json";
12689
+ readonly description: "JSON on stdout (always)";
12690
+ readonly hidden: false;
12691
+ }];
12692
+ readonly subcommands: readonly [];
12609
12693
  }, {
12610
12694
  readonly name: "_spawn-envelope-repair";
12611
12695
  readonly description: "";
@@ -14162,6 +14162,11 @@ export const COMMAND_CATALOG = [
14162
14162
  "description": "Skip worker continuity and start a cold provider session",
14163
14163
  "hidden": false
14164
14164
  },
14165
+ {
14166
+ "flags": "--loop",
14167
+ "description": "Keep one worker lane registered to process queued phase envelopes",
14168
+ "hidden": false
14169
+ },
14165
14170
  {
14166
14171
  "flags": "--provider <name>",
14167
14172
  "description": "claude | codex | grok",
@@ -14262,6 +14267,63 @@ export const COMMAND_CATALOG = [
14262
14267
  ],
14263
14268
  "subcommands": []
14264
14269
  },
14270
+ {
14271
+ "name": "_loop-worker",
14272
+ "description": "",
14273
+ "aliases": [],
14274
+ "hidden": true,
14275
+ "helpOption": true,
14276
+ "helpCommand": false,
14277
+ "usage": "[options] <lane-id>",
14278
+ "helpText": [],
14279
+ "arguments": [
14280
+ {
14281
+ "name": "lane-id",
14282
+ "required": true,
14283
+ "variadic": false,
14284
+ "description": "Loop lane id"
14285
+ }
14286
+ ],
14287
+ "options": [
14288
+ {
14289
+ "flags": "--json",
14290
+ "description": "JSON on stdout (always)",
14291
+ "hidden": false
14292
+ }
14293
+ ],
14294
+ "subcommands": []
14295
+ },
14296
+ {
14297
+ "name": "enqueue",
14298
+ "description": "Validate and queue one phase envelope for a loop lane",
14299
+ "aliases": [],
14300
+ "hidden": false,
14301
+ "helpOption": true,
14302
+ "helpCommand": false,
14303
+ "usage": "[options] <lane-id>",
14304
+ "helpText": [],
14305
+ "arguments": [
14306
+ {
14307
+ "name": "lane-id",
14308
+ "required": true,
14309
+ "variadic": false,
14310
+ "description": "Loop lane id"
14311
+ }
14312
+ ],
14313
+ "options": [
14314
+ {
14315
+ "flags": "--envelope <file>",
14316
+ "description": "Generic phase or hq-phase-envelope/v1 JSON file",
14317
+ "hidden": false
14318
+ },
14319
+ {
14320
+ "flags": "--json",
14321
+ "description": "JSON on stdout (always)",
14322
+ "hidden": false
14323
+ }
14324
+ ],
14325
+ "subcommands": []
14326
+ },
14265
14327
  {
14266
14328
  "name": "reparent-target",
14267
14329
  "description": "",
@@ -15334,6 +15396,48 @@ export const COMMAND_CATALOG = [
15334
15396
  ],
15335
15397
  "subcommands": []
15336
15398
  },
15399
+ {
15400
+ "name": "_enqueue-blocked-escalation",
15401
+ "description": "",
15402
+ "aliases": [],
15403
+ "hidden": true,
15404
+ "helpOption": true,
15405
+ "helpCommand": false,
15406
+ "usage": "[options] <recipient-kind> <recipient-id>",
15407
+ "helpText": [],
15408
+ "arguments": [
15409
+ {
15410
+ "name": "recipient-kind",
15411
+ "required": true,
15412
+ "variadic": false,
15413
+ "description": ""
15414
+ },
15415
+ {
15416
+ "name": "recipient-id",
15417
+ "required": true,
15418
+ "variadic": false,
15419
+ "description": ""
15420
+ }
15421
+ ],
15422
+ "options": [
15423
+ {
15424
+ "flags": "--text <text>",
15425
+ "description": "Escalation notice",
15426
+ "hidden": false
15427
+ },
15428
+ {
15429
+ "flags": "--message-id <id>",
15430
+ "description": "Idempotency key",
15431
+ "hidden": false
15432
+ },
15433
+ {
15434
+ "flags": "--json",
15435
+ "description": "JSON on stdout (always)",
15436
+ "hidden": false
15437
+ }
15438
+ ],
15439
+ "subcommands": []
15440
+ },
15337
15441
  {
15338
15442
  "name": "_spawn-envelope-repair",
15339
15443
  "description": "",
@@ -0,0 +1,12 @@
1
+ import type { Ctx, LaneResumeOptions } from "./lanes-shared.js";
2
+ import type { LaneRecord } from "../lib/lanes/types.js";
3
+ export interface LoopControllerDeps {
4
+ resumeLane(laneId: string, options: LaneResumeOptions): Promise<unknown>;
5
+ interruptLane(laneId: string, reason: string, options: {
6
+ preserveLoopQueue: boolean;
7
+ }): Promise<void>;
8
+ emitTerminal(lane: LaneRecord): Promise<void>;
9
+ }
10
+ export declare function startLaneLoopController(ctx: Ctx, laneId: string, initialAdmission?: boolean): number;
11
+ export declare function runLaneLoopController(ctx: Ctx, laneId: string, deps: LoopControllerDeps): Promise<void>;
12
+ //# sourceMappingURL=lanes-loop.d.ts.map
@@ -0,0 +1,214 @@
1
+ import { spawn } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { LanesError } from "../lib/lanes/errors.js";
5
+ import { readLane, retryUpdateLane } from "../lib/lanes/store.js";
6
+ import { readLatestEnvelopeRecord } from "../lib/lanes/watcher.js";
7
+ import { spawnLaneWorker } from "../lib/lanes/spawn.js";
8
+ import { raiseLaneQuestion } from "../lib/lanes/questions.js";
9
+ import { isTerminalLaneState } from "../lib/lanes/types.js";
10
+ import { clearLoopControllerPid, loopAdmissionPath, runLoopLane, setLoopControllerPid } from "../lib/lanes/loop.js";
11
+ import { laneStories } from "../lib/lanes/stories.js";
12
+ const LOOP_RUNS_REL = path.join("workspace", "lanes-runs");
13
+ function loopRunDir(hqRoot, laneId) {
14
+ return path.join(hqRoot, LOOP_RUNS_REL, laneId);
15
+ }
16
+ export function startLaneLoopController(ctx, laneId, initialAdmission = false) {
17
+ const entry = process.argv[1];
18
+ if (!entry)
19
+ throw new LanesError("loop_controller_entry_missing", "Cannot resolve the hq CLI entry point for the loop controller", { lane_id: laneId });
20
+ const child = spawn(process.execPath, [entry, "lanes", "_loop-worker", laneId], {
21
+ cwd: ctx.hqRoot(),
22
+ detached: true,
23
+ stdio: "ignore",
24
+ env: { ...ctx.env, HQ_LANE_LOOP_CONTROLLER: "1", ...(initialAdmission ? { HQ_LANE_LOOP_INITIAL: "1" } : {}) },
25
+ });
26
+ child.unref();
27
+ if (child.pid === undefined)
28
+ throw new LanesError("loop_controller_spawn_failed", "Loop controller did not start", { lane_id: laneId });
29
+ setLoopControllerPid(loopRunDir(ctx.hqRoot(), laneId), laneId, child.pid);
30
+ return child.pid;
31
+ }
32
+ export async function runLaneLoopController(ctx, laneId, deps) {
33
+ const hqRoot = ctx.hqRoot();
34
+ const lane = readLane(hqRoot, laneId);
35
+ if (lane.loop_mode !== true)
36
+ throw new LanesError("lane_not_loop_mode", `Lane ${laneId} is not a loop lane`, { lane_id: laneId });
37
+ const runDir = loopRunDir(hqRoot, laneId);
38
+ let initialAdmission = ctx.env.HQ_LANE_LOOP_INITIAL === "1";
39
+ const stop = new AbortController();
40
+ const onSignal = () => stop.abort();
41
+ process.once("SIGTERM", onSignal);
42
+ process.once("SIGINT", onSignal);
43
+ try {
44
+ const result = await runLoopLane({
45
+ lane,
46
+ runDir,
47
+ signal: stop.signal,
48
+ idlePollMs: 250,
49
+ execute: async ({ envelope, signal, fresh, sessionId }) => {
50
+ let current = readLane(hqRoot, laneId);
51
+ const data = envelope;
52
+ const storyId = typeof data.story_id === "string" && data.story_id.trim() ? data.story_id.trim() : current.story_id;
53
+ if (storyId !== current.story_id) {
54
+ current = retryUpdateLane(hqRoot, laneId, "command", (record) => {
55
+ const stories = laneStories(record);
56
+ if (!stories.some((story) => story.project_id === record.project_id && story.story_id === storyId)) {
57
+ stories.push({ project_id: record.project_id, story_id: storyId, status: "in_progress", added_at: ctx.now().toISOString(), updated_at: ctx.now().toISOString() });
58
+ }
59
+ return { ...record, story_id: storyId, stories };
60
+ });
61
+ }
62
+ const worktree = typeof data.worktree === "string" ? path.resolve(data.worktree) : hqRoot;
63
+ if (typeof data.worktree === "string") {
64
+ let isDirectory = false;
65
+ try {
66
+ isDirectory = fs.statSync(worktree).isDirectory();
67
+ }
68
+ catch (error) {
69
+ if (error.code !== "ENOENT" && error.code !== "ENOTDIR")
70
+ throw error;
71
+ }
72
+ if (!isDirectory)
73
+ throw new Error(`phase worktree is not an accessible directory: ${worktree}`);
74
+ }
75
+ const phaseContext = data.schema === "hq-phase-envelope/v1"
76
+ ? [
77
+ `Story id: ${storyId}`,
78
+ `Story title: ${String(data.story_title ?? "")}`,
79
+ `Story description: ${String(data.story_description ?? "")}`,
80
+ `Phase: ${String(data.phase)}`,
81
+ `Working directory (worktree): ${worktree}`,
82
+ `Incoming handoff: ${String(data.incoming_handoff ?? "None: this is the first phase")}`,
83
+ "Acceptance criteria:",
84
+ ...(Array.isArray(data.acceptance_criteria) ? data.acceptance_criteria.map((item, index) => `${index}. ${String(item)}`) : []),
85
+ "Constraints:",
86
+ ...(Array.isArray(data.constraints) ? data.constraints.map((item) => `- ${String(item)}`) : []),
87
+ ].join("\n")
88
+ : `Execute the ${String(data.phase ?? "requested")} phase for story ${storyId}.`;
89
+ const prompt = typeof data.prompt === "string" ? data.prompt :
90
+ `${phaseContext}\n\nPhase envelope:\n${JSON.stringify(envelope, null, 2)}`;
91
+ const briefPath = path.join(runDir, "brief.md");
92
+ fs.writeFileSync(briefPath, [
93
+ "Follow the lane worker instructions and complete this single bounded phase.",
94
+ "Return the standard hq-lane-envelope.v1 envelope when the phase is complete. For a section 8 phase envelope, put a complete hq-phase-handoff/v1 object in artifacts; it must identify the requested story_id and phase and include worker_id, status, summary, files_changed, commits, back_pressure (tests/lint/typecheck/build), and context_for_next.",
95
+ "",
96
+ prompt,
97
+ ].join("\n"));
98
+ const admissionPath = loopAdmissionPath(current.state, current.pending_resume !== undefined, initialAdmission);
99
+ if (admissionPath === "initial") {
100
+ initialAdmission = false;
101
+ if (current.state === "running") {
102
+ retryUpdateLane(hqRoot, laneId, "command", (record) => ({
103
+ ...record,
104
+ state: "queued",
105
+ queued_reason: undefined,
106
+ queued_details: undefined,
107
+ admission_reserved_at: undefined,
108
+ }));
109
+ }
110
+ await spawnLaneWorker(hqRoot, current, {
111
+ env: ctx.env,
112
+ modelDeps: { readFileSync: ctx.readFileSync, probe: ctx.probeProvider, env: ctx.env },
113
+ existsSync: ctx.existsSync,
114
+ spawnProcess: ctx.spawnProcess,
115
+ proveDetached: ctx.proveDetached,
116
+ killProcess: ctx.killProcess,
117
+ now: ctx.now,
118
+ resolveMachineIdentity: ctx.resolveMachineIdentity,
119
+ runCompaction: ctx.runCompaction,
120
+ locateTranscript: ctx.locateTranscript,
121
+ workingDirectory: worktree,
122
+ });
123
+ }
124
+ else if (admissionPath === "deferred") {
125
+ retryUpdateLane(hqRoot, laneId, "command", (record) => ({
126
+ ...record,
127
+ pending_resume: record.pending_resume ? {
128
+ ...record.pending_resume,
129
+ ...(fresh ? { session_id: undefined, fresh_thread: true } : sessionId ? { session_id: sessionId, fresh_thread: undefined } : {}),
130
+ working_directory: worktree,
131
+ } : undefined,
132
+ }));
133
+ }
134
+ else {
135
+ await deps.resumeLane(laneId, {
136
+ text: `Read ${briefPath} and follow it as the complete instruction for this phase.`,
137
+ project: current.project_id,
138
+ story: storyId,
139
+ freshThread: fresh,
140
+ ...(sessionId ? { loopSessionId: sessionId } : {}),
141
+ workingDirectory: worktree,
142
+ });
143
+ }
144
+ let interruptPromise;
145
+ const abortPhase = () => {
146
+ interruptPromise = deps.interruptLane(laneId, "loop phase deadline exceeded", { preserveLoopQueue: true }).catch((error) => {
147
+ process.stderr.write(`hq lanes: could not interrupt stalled loop phase ${laneId}: ${error instanceof Error ? error.message : String(error)}\n`);
148
+ });
149
+ };
150
+ signal.addEventListener("abort", abortPhase, { once: true });
151
+ try {
152
+ for (;;) {
153
+ if (signal.aborted) {
154
+ await interruptPromise;
155
+ throw signal.reason ?? new Error("phase deadline exceeded");
156
+ }
157
+ const latest = readLane(hqRoot, laneId);
158
+ if (latest.state === "done" || latest.state === "blocked" || latest.state === "awaiting_input" || isTerminalLaneState(latest.state)) {
159
+ const envelopeRecord = readLatestEnvelopeRecord(hqRoot, laneId);
160
+ if (latest.state !== "done" && latest.state !== "blocked" && latest.state !== "awaiting_input") {
161
+ throw new Error(`lane phase ended in ${latest.state}${latest.terminal_reason ? ` (${latest.terminal_reason})` : ""}`);
162
+ }
163
+ if (!envelopeRecord)
164
+ throw new Error(`lane ${laneId} ended without writing a phase envelope`);
165
+ const handoff = envelopeRecord.envelope.artifacts.find((artifact) => typeof artifact === "object" && artifact !== null &&
166
+ artifact.schema === "hq-phase-handoff/v1");
167
+ if (data.schema === "hq-phase-envelope/v1" && !handoff) {
168
+ throw new Error("section 8 phase requires an hq-phase-handoff/v1 object in the lane envelope artifacts");
169
+ }
170
+ return { output: JSON.stringify(handoff ?? envelopeRecord.envelope), ...(latest.provider_session_id ? { sessionId: latest.provider_session_id } : {}) };
171
+ }
172
+ await new Promise((resolve) => setTimeout(resolve, 250));
173
+ }
174
+ }
175
+ finally {
176
+ signal.removeEventListener("abort", abortPhase);
177
+ }
178
+ },
179
+ raiseQuestion: ({ envelope, envelopeId, attempt, error }) => {
180
+ const current = readLane(hqRoot, laneId);
181
+ const data = envelope;
182
+ raiseLaneQuestion({
183
+ hqRoot,
184
+ lane: current,
185
+ raisedVia: "cli",
186
+ at: ctx.now().toISOString(),
187
+ body: {
188
+ question: `Loop phase ${String(data.phase ?? envelopeId)} stalled twice. How should it proceed?`,
189
+ context: `${error}\nThe phase is parked; later envelopes remain queued. Stall attempt: ${attempt}.`,
190
+ options: ["Retry with a longer deadline", "Reroute to another worker", "Drop this phase and block the story"],
191
+ recommended: "Retry with a longer deadline",
192
+ },
193
+ });
194
+ },
195
+ });
196
+ if (result.state === "stopped") {
197
+ const stopped = retryUpdateLane(hqRoot, laneId, "command", (current) => ({
198
+ ...current,
199
+ state: "done",
200
+ queued_reason: undefined,
201
+ queued_details: undefined,
202
+ admission_reserved_at: undefined,
203
+ terminal_reason: "loop_stopped",
204
+ }));
205
+ await deps.emitTerminal(stopped);
206
+ }
207
+ }
208
+ finally {
209
+ process.removeListener("SIGTERM", onSignal);
210
+ process.removeListener("SIGINT", onSignal);
211
+ clearLoopControllerPid(runDir, laneId, process.pid);
212
+ }
213
+ }
214
+ //# sourceMappingURL=lanes-loop.js.map
@@ -291,7 +291,7 @@ export function createLaneQuestionsHandlers(deps) {
291
291
  const body = parseOutpostQuestionBody(response.body, outpostId, question.question_id);
292
292
  return { question, audience, body };
293
293
  }
294
- async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
294
+ async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter, questionId, visibilityFilter) {
295
295
  const hostedLanes = loadAllLanes(hqRoot).filter((lane) => lane.host.kind === "outpost" &&
296
296
  !laneIsHostedHere(ctx, lane) &&
297
297
  (!companyFilter ||
@@ -356,7 +356,7 @@ export function createLaneQuestionsHandlers(deps) {
356
356
  };
357
357
  }
358
358
  try {
359
- const remoteQuestions = await readOutpostQuestionRecords(ctx, outpostId, group.company);
359
+ const remoteQuestions = await readOutpostQuestionRecords(ctx, outpostId, group.company, questionId, visibilityFilter);
360
360
  const localLanesById = new Map(group.lanes.map((lane) => [lane.lane_id, lane]));
361
361
  const groupQuestions = [];
362
362
  const seenQuestionIds = new Set();
@@ -595,8 +595,32 @@ export function createLaneQuestionsHandlers(deps) {
595
595
  }
596
596
  return true;
597
597
  });
598
+ const deliveredLocalQuestions = new Map();
599
+ const currentSessionId = sessionIdFromEnv(ctx.env);
600
+ if (currentSessionId) {
601
+ const deliveredAt = ctx.now().toISOString();
602
+ for (const question of localQuestions) {
603
+ if (question.status !== "pending")
604
+ continue;
605
+ const audience = resolveAudience(hqRoot, question, ctx);
606
+ if (audience.kind !== "session" || audience.id !== currentSessionId)
607
+ continue;
608
+ const delivered = casUpdateQuestion(hqRoot, question.question_id, (current) => current.status !== "pending" ||
609
+ (current.seen_at && current.seen_session_id === currentSessionId)
610
+ ? current
611
+ : { ...current, seen_at: deliveredAt, seen_session_id: currentSessionId });
612
+ deliveredLocalQuestions.set(question.question_id, delivered);
613
+ }
614
+ }
598
615
  const outpostLoad = opts.allHosts
599
- ? await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company)
616
+ ? await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company, undefined, {
617
+ sessionId: currentSessionId ?? undefined,
618
+ for: opts.for,
619
+ from: opts.from,
620
+ project: opts.project,
621
+ story: opts.story,
622
+ status: opts.status,
623
+ })
600
624
  : { questions: [], errors: [] };
601
625
  for (const owner of outpostLoad.questions) {
602
626
  assertQuestionCompanyAccess(owner.question, owner.lane, company, hqRoot);
@@ -607,12 +631,13 @@ export function createLaneQuestionsHandlers(deps) {
607
631
  ...localQuestions
608
632
  .filter((q) => !remoteQuestionIds.has(q.question_id))
609
633
  .map((q) => {
634
+ const visibleQuestion = deliveredLocalQuestions.get(q.question_id) ?? q;
610
635
  const row = {
611
- ...q,
612
- priority: q.priority ?? null,
613
- priority_display: q.priority ?? "-",
614
- audience: resolveAudience(hqRoot, q, ctx),
615
- ...(q.question_kind === "continue" ? { flag: "continue-loop" } : {}),
636
+ ...visibleQuestion,
637
+ priority: visibleQuestion.priority ?? null,
638
+ priority_display: visibleQuestion.priority ?? "-",
639
+ audience: resolveAudience(hqRoot, visibleQuestion, ctx),
640
+ ...(visibleQuestion.question_kind === "continue" ? { flag: "continue-loop" } : {}),
616
641
  };
617
642
  if (!opts.allHosts)
618
643
  return row;
@@ -662,7 +687,7 @@ export function createLaneQuestionsHandlers(deps) {
662
687
  catch (err) {
663
688
  if (!isLanesError(err) || err.error !== "question_not_found")
664
689
  throw err;
665
- const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
690
+ const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, undefined, questionId);
666
691
  if (lookup.errors.length > 0) {
667
692
  throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
668
693
  question_id: questionId,
@@ -717,7 +742,7 @@ export function createLaneQuestionsHandlers(deps) {
717
742
  if (!isLanesError(err) || err.error !== "question_not_found")
718
743
  throw err;
719
744
  const company = explicitCompany ?? await resolveActiveQuestionCompany(ctx, "Cannot look up this lane question because the active HQ company could not be resolved; re-run with --company <slug>");
720
- const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company);
745
+ const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company, questionId);
721
746
  const matches = lookup.questions.filter((entry) => entry.question.question_id === questionId);
722
747
  if (matches.length === 0 && lookup.errors.length > 0) {
723
748
  throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
@@ -726,7 +751,7 @@ export function createLaneQuestionsHandlers(deps) {
726
751
  });
727
752
  }
728
753
  if (matches.length === 0) {
729
- const allCompanyLookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
754
+ const allCompanyLookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, undefined, questionId);
730
755
  const otherCompanyMatch = allCompanyLookup.questions.find((entry) => entry.question.question_id === questionId);
731
756
  if (otherCompanyMatch) {
732
757
  assertQuestionCompanyAccess(otherCompanyMatch.question, otherCompanyMatch.lane, company, hqRoot);
@@ -818,6 +843,7 @@ export function createLaneQuestionsHandlers(deps) {
818
843
  company: recordedCompany,
819
844
  body: nextBody,
820
845
  status: "answered",
846
+ delivered_at: null,
821
847
  answer: {
822
848
  choice,
823
849
  answer_kind: freeform ? "freeform" : "option",
@@ -91,6 +91,9 @@ export function createLaneResumeHandlers(deps) {
91
91
  const hqRoot = ctx.hqRoot();
92
92
  const resolution = await deps.resolveCommandLane(ctx, laneId);
93
93
  let lane = resolution.lane;
94
+ if (opts.freshThread === true && lane.loop_mode !== true) {
95
+ throw new LanesError("fresh_thread_loop_only", "--fresh-thread is reserved for loop-lane phases", { lane_id: laneId });
96
+ }
94
97
  const failedResumeReason = opts.reason?.trim();
95
98
  if (opts.reason !== undefined && opts.fromFailed !== true) {
96
99
  throw new LanesError("failed_resume_reason_requires_flag", "--reason on hq lanes resume requires --from-failed", { lane_id: laneId });
@@ -153,6 +156,9 @@ export function createLaneResumeHandlers(deps) {
153
156
  throw new LanesError("effort_resume_unsupported", `Provider ${lane.provider} cannot change effort on a resumed session`, { lane_id: laneId, provider: lane.provider });
154
157
  }
155
158
  lane = laneWithRecoveredProviderSession(hqRoot, lane);
159
+ if (opts.loopSessionId !== undefined && opts.loopSessionId !== lane.provider_session_id) {
160
+ lane = casUpdateLane(hqRoot, laneId, (current) => ({ ...current, provider_session_id: opts.loopSessionId }));
161
+ }
156
162
  if (opts.model !== undefined && opts.account === undefined) {
157
163
  throw new LanesError("model_requires_account", "--model on hq lanes resume is only accepted together with --account", {});
158
164
  }
@@ -233,7 +239,7 @@ export function createLaneResumeHandlers(deps) {
233
239
  }
234
240
  const refusal = queuedProfileRecovery || lane.state === "awaiting_input"
235
241
  ? null
236
- : resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true || isFailedQuotaKeepAliveLane(lane) });
242
+ : resumeCommandRefusal(lane, { allowFailed: opts.fromFailed === true || isFailedQuotaKeepAliveLane(lane), allowFresh: opts.freshThread === true });
237
243
  if (refusal !== null) {
238
244
  throw new LanesError(refusal.error, refusal.message, {
239
245
  lane_id: laneId,
@@ -435,6 +441,8 @@ export function createLaneResumeHandlers(deps) {
435
441
  ...(deliveryText !== undefined ? { text: deliveryText } : {}),
436
442
  ...(messageId !== undefined ? { message_id: messageId } : {}),
437
443
  compact: opts.compact !== false,
444
+ ...(opts.freshThread === true ? { fresh_thread: true } : {}),
445
+ ...(opts.workingDirectory ? { working_directory: opts.workingDirectory } : {}),
438
446
  },
439
447
  });
440
448
  lane = move.lane;
@@ -469,6 +477,8 @@ export function createLaneResumeHandlers(deps) {
469
477
  ...(deliveryText !== undefined ? { text: deliveryText } : {}),
470
478
  ...(messageId !== undefined ? { message_id: messageId } : {}),
471
479
  compact: opts.compact !== false,
480
+ ...(opts.freshThread === true ? { fresh_thread: true } : {}),
481
+ ...(opts.workingDirectory ? { working_directory: opts.workingDirectory } : {}),
472
482
  },
473
483
  accountSelection: selection,
474
484
  });
@@ -496,6 +506,8 @@ export function createLaneResumeHandlers(deps) {
496
506
  ...(deliveryText !== undefined ? { text: deliveryText } : {}),
497
507
  ...(messageId !== undefined ? { message_id: messageId } : {}),
498
508
  compact: opts.compact !== false,
509
+ ...(opts.freshThread === true ? { fresh_thread: true } : {}),
510
+ ...(opts.workingDirectory ? { working_directory: opts.workingDirectory } : {}),
499
511
  },
500
512
  });
501
513
  lane = move.lane;
@@ -535,6 +547,8 @@ export function createLaneResumeHandlers(deps) {
535
547
  ...(deliveryText !== undefined ? { text: deliveryText } : {}),
536
548
  ...(messageId !== undefined ? { message_id: messageId } : {}),
537
549
  compact: opts.compact !== false,
550
+ ...(opts.freshThread === true ? { fresh_thread: true } : {}),
551
+ ...(opts.workingDirectory ? { working_directory: opts.workingDirectory } : {}),
538
552
  at: quotaHoldAt,
539
553
  };
540
554
  resumed = markSpawnFailure(hqRoot, laneId, "account_quota_exhausted", {
@@ -590,6 +604,8 @@ export function createLaneResumeHandlers(deps) {
590
604
  ...(deliveryText !== undefined ? { text: deliveryText } : {}),
591
605
  ...(messageId !== undefined ? { message_id: messageId } : {}),
592
606
  compact: opts.compact !== false,
607
+ ...(opts.freshThread === true ? { fresh_thread: true } : {}),
608
+ ...(opts.workingDirectory ? { working_directory: opts.workingDirectory } : {}),
593
609
  at,
594
610
  },
595
611
  };
@@ -610,10 +626,12 @@ export function createLaneResumeHandlers(deps) {
610
626
  resolveMachineIdentity: ctx.resolveMachineIdentity,
611
627
  runCompaction: ctx.runCompaction,
612
628
  locateTranscript: ctx.locateTranscript,
629
+ ...(opts.workingDirectory ? { workingDirectory: opts.workingDirectory } : {}),
613
630
  }, {
614
631
  trigger: {
615
632
  kind: "resume",
616
633
  from: resumedFrom,
634
+ ...(opts.freshThread === true ? { fresh: true } : {}),
617
635
  ...(resumedFrom === "awaiting_input" && pendingQuestions.length === 0 ? { allowAwaitingInput: true } : {}),
618
636
  ...(opts.fromFailed === true ? { fromFailed: true } : {}),
619
637
  ...(failedResumeExpectedSenior !== undefined