@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 +6 -0
- package/dist/command-catalog.generated.d.ts +84 -0
- package/dist/command-catalog.generated.js +104 -0
- package/dist/commands/lanes-loop.d.ts +12 -0
- package/dist/commands/lanes-loop.js +214 -0
- package/dist/commands/lanes-questions.js +37 -11
- package/dist/commands/lanes-resume.js +19 -1
- package/dist/commands/lanes-shared.d.ts +12 -1
- package/dist/commands/lanes.js +262 -22
- package/dist/lib/lanes/delivery.d.ts +3 -0
- package/dist/lib/lanes/delivery.js +5 -0
- package/dist/lib/lanes/dropbox.d.ts +3 -0
- package/dist/lib/lanes/dropbox.js +103 -1
- package/dist/lib/lanes/flag-keys.d.ts +4 -0
- package/dist/lib/lanes/flag-keys.js +4 -0
- package/dist/lib/lanes/loop.d.ts +158 -0
- package/dist/lib/lanes/loop.js +743 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +9 -1
- package/dist/lib/lanes/outpost-dispatch.js +21 -7
- package/dist/lib/lanes/reconcile.d.ts +19 -0
- package/dist/lib/lanes/reconcile.js +173 -6
- package/dist/lib/lanes/spawn.d.ts +2 -0
- package/dist/lib/lanes/spawn.js +10 -8
- package/dist/lib/lanes/store.js +49 -2
- package/dist/lib/lanes/types.d.ts +18 -0
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.js +3 -0
- package/package.json +1 -1
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
|
-
...
|
|
612
|
-
priority:
|
|
613
|
-
priority_display:
|
|
614
|
-
audience: resolveAudience(hqRoot,
|
|
615
|
-
...(
|
|
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
|