@indigoai-us/hq-cli 5.345.56 → 5.345.58

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,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.58] — 2026-10-09
6
+ - hq access now accepts `--as <member>` to view another member's access, read-only.
7
+
8
+ ## [5.345.57] — 2026-10-09
9
+ - 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.
10
+
11
+ - Add six-hour escalation for continuously blocked lanes, including durable cross-host inbox delivery and per-lane escalation history.
12
+ - 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.
13
+
5
14
  ## [5.345.56] — 2026-10-09
6
15
  - 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
16
  - Record privacy-limited `hq members invite` success and failure outcomes through the existing CLI feature telemetry event.
@@ -2871,6 +2871,10 @@ export declare const COMMAND_CATALOG: readonly [{
2871
2871
  readonly flags: "--company <slug>";
2872
2872
  readonly description: "Company slug";
2873
2873
  readonly hidden: false;
2874
+ }, {
2875
+ readonly flags: "--as <person-uid>";
2876
+ readonly description: "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)";
2877
+ readonly hidden: false;
2874
2878
  }, {
2875
2879
  readonly flags: "--json";
2876
2880
  readonly description: "Print a single JSON object";
@@ -11654,6 +11658,10 @@ export declare const COMMAND_CATALOG: readonly [{
11654
11658
  readonly flags: "--fresh-thread";
11655
11659
  readonly description: "Skip worker continuity and start a cold provider session";
11656
11660
  readonly hidden: false;
11661
+ }, {
11662
+ readonly flags: "--loop";
11663
+ readonly description: "Keep one worker lane registered to process queued phase envelopes";
11664
+ readonly hidden: false;
11657
11665
  }, {
11658
11666
  readonly flags: "--provider <name>";
11659
11667
  readonly description: "claude | codex | grok";
@@ -11734,6 +11742,52 @@ export declare const COMMAND_CATALOG: readonly [{
11734
11742
  readonly hidden: false;
11735
11743
  }];
11736
11744
  readonly subcommands: readonly [];
11745
+ }, {
11746
+ readonly name: "_loop-worker";
11747
+ readonly description: "";
11748
+ readonly aliases: readonly [];
11749
+ readonly hidden: true;
11750
+ readonly helpOption: true;
11751
+ readonly helpCommand: false;
11752
+ readonly usage: "[options] <lane-id>";
11753
+ readonly helpText: readonly [];
11754
+ readonly arguments: readonly [{
11755
+ readonly name: "lane-id";
11756
+ readonly required: true;
11757
+ readonly variadic: false;
11758
+ readonly description: "Loop lane id";
11759
+ }];
11760
+ readonly options: readonly [{
11761
+ readonly flags: "--json";
11762
+ readonly description: "JSON on stdout (always)";
11763
+ readonly hidden: false;
11764
+ }];
11765
+ readonly subcommands: readonly [];
11766
+ }, {
11767
+ readonly name: "enqueue";
11768
+ readonly description: "Validate and queue one phase envelope for a loop lane";
11769
+ readonly aliases: readonly [];
11770
+ readonly hidden: false;
11771
+ readonly helpOption: true;
11772
+ readonly helpCommand: false;
11773
+ readonly usage: "[options] <lane-id>";
11774
+ readonly helpText: readonly [];
11775
+ readonly arguments: readonly [{
11776
+ readonly name: "lane-id";
11777
+ readonly required: true;
11778
+ readonly variadic: false;
11779
+ readonly description: "Loop lane id";
11780
+ }];
11781
+ readonly options: readonly [{
11782
+ readonly flags: "--envelope <file>";
11783
+ readonly description: "Generic phase or hq-phase-envelope/v1 JSON file";
11784
+ readonly hidden: false;
11785
+ }, {
11786
+ readonly flags: "--json";
11787
+ readonly description: "JSON on stdout (always)";
11788
+ readonly hidden: false;
11789
+ }];
11790
+ readonly subcommands: readonly [];
11737
11791
  }, {
11738
11792
  readonly name: "reparent-target";
11739
11793
  readonly description: "";
@@ -12606,6 +12660,40 @@ export declare const COMMAND_CATALOG: readonly [{
12606
12660
  readonly hidden: false;
12607
12661
  }];
12608
12662
  readonly subcommands: readonly [];
12663
+ }, {
12664
+ readonly name: "_enqueue-blocked-escalation";
12665
+ readonly description: "";
12666
+ readonly aliases: readonly [];
12667
+ readonly hidden: true;
12668
+ readonly helpOption: true;
12669
+ readonly helpCommand: false;
12670
+ readonly usage: "[options] <recipient-kind> <recipient-id>";
12671
+ readonly helpText: readonly [];
12672
+ readonly arguments: readonly [{
12673
+ readonly name: "recipient-kind";
12674
+ readonly required: true;
12675
+ readonly variadic: false;
12676
+ readonly description: "";
12677
+ }, {
12678
+ readonly name: "recipient-id";
12679
+ readonly required: true;
12680
+ readonly variadic: false;
12681
+ readonly description: "";
12682
+ }];
12683
+ readonly options: readonly [{
12684
+ readonly flags: "--text <text>";
12685
+ readonly description: "Escalation notice";
12686
+ readonly hidden: false;
12687
+ }, {
12688
+ readonly flags: "--message-id <id>";
12689
+ readonly description: "Idempotency key";
12690
+ readonly hidden: false;
12691
+ }, {
12692
+ readonly flags: "--json";
12693
+ readonly description: "JSON on stdout (always)";
12694
+ readonly hidden: false;
12695
+ }];
12696
+ readonly subcommands: readonly [];
12609
12697
  }, {
12610
12698
  readonly name: "_spawn-envelope-repair";
12611
12699
  readonly description: "";
@@ -3462,6 +3462,11 @@ export const COMMAND_CATALOG = [
3462
3462
  "description": "Company slug",
3463
3463
  "hidden": false
3464
3464
  },
3465
+ {
3466
+ "flags": "--as <person-uid>",
3467
+ "description": "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)",
3468
+ "hidden": false
3469
+ },
3465
3470
  {
3466
3471
  "flags": "--json",
3467
3472
  "description": "Print a single JSON object",
@@ -14162,6 +14167,11 @@ export const COMMAND_CATALOG = [
14162
14167
  "description": "Skip worker continuity and start a cold provider session",
14163
14168
  "hidden": false
14164
14169
  },
14170
+ {
14171
+ "flags": "--loop",
14172
+ "description": "Keep one worker lane registered to process queued phase envelopes",
14173
+ "hidden": false
14174
+ },
14165
14175
  {
14166
14176
  "flags": "--provider <name>",
14167
14177
  "description": "claude | codex | grok",
@@ -14262,6 +14272,63 @@ export const COMMAND_CATALOG = [
14262
14272
  ],
14263
14273
  "subcommands": []
14264
14274
  },
14275
+ {
14276
+ "name": "_loop-worker",
14277
+ "description": "",
14278
+ "aliases": [],
14279
+ "hidden": true,
14280
+ "helpOption": true,
14281
+ "helpCommand": false,
14282
+ "usage": "[options] <lane-id>",
14283
+ "helpText": [],
14284
+ "arguments": [
14285
+ {
14286
+ "name": "lane-id",
14287
+ "required": true,
14288
+ "variadic": false,
14289
+ "description": "Loop lane id"
14290
+ }
14291
+ ],
14292
+ "options": [
14293
+ {
14294
+ "flags": "--json",
14295
+ "description": "JSON on stdout (always)",
14296
+ "hidden": false
14297
+ }
14298
+ ],
14299
+ "subcommands": []
14300
+ },
14301
+ {
14302
+ "name": "enqueue",
14303
+ "description": "Validate and queue one phase envelope for a loop lane",
14304
+ "aliases": [],
14305
+ "hidden": false,
14306
+ "helpOption": true,
14307
+ "helpCommand": false,
14308
+ "usage": "[options] <lane-id>",
14309
+ "helpText": [],
14310
+ "arguments": [
14311
+ {
14312
+ "name": "lane-id",
14313
+ "required": true,
14314
+ "variadic": false,
14315
+ "description": "Loop lane id"
14316
+ }
14317
+ ],
14318
+ "options": [
14319
+ {
14320
+ "flags": "--envelope <file>",
14321
+ "description": "Generic phase or hq-phase-envelope/v1 JSON file",
14322
+ "hidden": false
14323
+ },
14324
+ {
14325
+ "flags": "--json",
14326
+ "description": "JSON on stdout (always)",
14327
+ "hidden": false
14328
+ }
14329
+ ],
14330
+ "subcommands": []
14331
+ },
14265
14332
  {
14266
14333
  "name": "reparent-target",
14267
14334
  "description": "",
@@ -15334,6 +15401,48 @@ export const COMMAND_CATALOG = [
15334
15401
  ],
15335
15402
  "subcommands": []
15336
15403
  },
15404
+ {
15405
+ "name": "_enqueue-blocked-escalation",
15406
+ "description": "",
15407
+ "aliases": [],
15408
+ "hidden": true,
15409
+ "helpOption": true,
15410
+ "helpCommand": false,
15411
+ "usage": "[options] <recipient-kind> <recipient-id>",
15412
+ "helpText": [],
15413
+ "arguments": [
15414
+ {
15415
+ "name": "recipient-kind",
15416
+ "required": true,
15417
+ "variadic": false,
15418
+ "description": ""
15419
+ },
15420
+ {
15421
+ "name": "recipient-id",
15422
+ "required": true,
15423
+ "variadic": false,
15424
+ "description": ""
15425
+ }
15426
+ ],
15427
+ "options": [
15428
+ {
15429
+ "flags": "--text <text>",
15430
+ "description": "Escalation notice",
15431
+ "hidden": false
15432
+ },
15433
+ {
15434
+ "flags": "--message-id <id>",
15435
+ "description": "Idempotency key",
15436
+ "hidden": false
15437
+ },
15438
+ {
15439
+ "flags": "--json",
15440
+ "description": "JSON on stdout (always)",
15441
+ "hidden": false
15442
+ }
15443
+ ],
15444
+ "subcommands": []
15445
+ },
15337
15446
  {
15338
15447
  "name": "_spawn-envelope-repair",
15339
15448
  "description": "",
@@ -135,6 +135,15 @@ export interface RunAccessResult {
135
135
  alreadyAskedAt?: string;
136
136
  requestNote?: string;
137
137
  }
138
+ export interface AccessAsResult {
139
+ personUid: string;
140
+ company: string;
141
+ path: string;
142
+ permission: "read" | "write" | "admin" | null;
143
+ canRead: boolean;
144
+ roleBypass: boolean;
145
+ exitCode: 0 | 3;
146
+ }
138
147
  export declare function outcomeExitCode(outcome: AccessOutcome): 0 | 2 | 3 | 4;
139
148
  export declare function formatAccessOutcome(result: RunAccessResult): string;
140
149
  export declare function resolveGrantor(tree: AccessAclTree | null, members: AccessMember[]): AccessGrantor | null;
@@ -149,6 +158,11 @@ export declare function resolveAccessCompany(target: string, opts?: {
149
158
  company?: string;
150
159
  hqRoot?: string;
151
160
  }): string | undefined;
161
+ export declare function runAccessForPerson(rawPath: string, personUid: string, opts?: {
162
+ company?: string;
163
+ hqRoot?: string;
164
+ }): Promise<AccessAsResult>;
165
+ export declare function formatAccessForPerson(result: AccessAsResult): string;
152
166
  export declare function runAccessForPath(path: string, opts?: {
153
167
  company?: string;
154
168
  hqRoot?: string;
@@ -20,7 +20,7 @@ import * as readline from "node:readline";
20
20
  import { ListObjectsV2Command, } from "@aws-sdk/client-s3";
21
21
  import { DEFAULT_HQ_ROOT } from "../utils/cognito-session.js";
22
22
  import { getCompanyUid, vaultApiFetch, resolveCallerUid } from "../utils/vault-api.js";
23
- import { parseCompanySlugFromPath, resolveBrowseSession, makeCompanyPresignFactory, runGet, } from "./files-browse.js";
23
+ import { parseCompanySlugFromPath, classifyAclSource, resolveBrowseSession, makeCompanyPresignFactory, runGet, toBucketRelative, } from "./files-browse.js";
24
24
  import { listJournals, syncDoctor, } from "@indigoai-us/hq-cloud";
25
25
  import { listActiveMembers } from "./members.js";
26
26
  import { buildDmBody } from "./dm.js";
@@ -87,6 +87,10 @@ function normalizeTarget(raw, slug) {
87
87
  t = t.slice(anchor.length);
88
88
  return t;
89
89
  }
90
+ function fileGrantCoversPath(grantPath, targetPath) {
91
+ return classifyAclSource(targetPath, [grantPath]) !== "role-bypass";
92
+ }
93
+ const FILE_PERMISSION_RANK = { read: 1, write: 2, admin: 3 };
90
94
  function httpStatusOf(err) {
91
95
  if (!err || typeof err !== "object")
92
96
  return undefined;
@@ -637,6 +641,7 @@ export function registerAccessCommand(program) {
637
641
  .command("access <path-or-query>")
638
642
  .description("Tell whether a vault file exists and whether you can read it. Never a bare not-found. Exit codes: 0 local/not-synced, 2 never-existed, 3 no-access, 4 ambiguous.")
639
643
  .option("--company <slug>", "Company slug")
644
+ .option("--as <person-uid>", "Check a person's access (exact prs_ or agt_ UID; self or owner/admin)")
640
645
  .option("--json", "Print a single JSON object")
641
646
  .option("--no-fix", "Diagnose only: do not fetch, pin, or repair sync")
642
647
  .option("--yes", "Answer the one send-confirmation yes (hosts must pass this explicitly)")
@@ -650,6 +655,12 @@ Exit codes:
650
655
  `)
651
656
  .action(async (target, opts) => {
652
657
  try {
658
+ if (opts.as) {
659
+ const result = await runAccessForPerson(target, opts.as, opts);
660
+ console.log(opts.json ? JSON.stringify(result) : formatAccessForPerson(result));
661
+ process.exitCode = result.exitCode;
662
+ return;
663
+ }
653
664
  const result = await runAccessForPath(target, opts);
654
665
  process.exitCode = result.exitCode;
655
666
  }
@@ -669,6 +680,63 @@ export function resolveAccessCompany(target, opts) {
669
680
  const hqRoot = opts?.hqRoot ?? DEFAULT_HQ_ROOT;
670
681
  return opts?.company ?? slugFromTarget(target) ?? readActiveCompany(hqRoot);
671
682
  }
683
+ export async function runAccessForPerson(rawPath, personUid, opts) {
684
+ const uid = personUid.trim();
685
+ if (!/^(?:prs|agt)_[A-Za-z0-9_-]+$/.test(uid)) {
686
+ throw new Error("Pass the person's exact prs_ or agt_ UID to --as.");
687
+ }
688
+ const pathSlug = slugFromTarget(rawPath);
689
+ if (opts?.company && pathSlug && opts.company !== pathSlug) {
690
+ throw new Error("The --company value does not match the company in the path.");
691
+ }
692
+ const company = resolveAccessCompany(rawPath, opts);
693
+ if (!company)
694
+ throw new Error("Pass --company <slug> or an anchored company path.");
695
+ const relativePath = normalizeTarget(rawPath, company);
696
+ if (!relativePath || relativePath.split(/[\\/]/).includes("..")) {
697
+ throw new Error("Pass a company-relative path without parent-directory segments.");
698
+ }
699
+ const { token } = await resolveBrowseSession();
700
+ const companyUid = await getCompanyUid(token, company);
701
+ const response = await vaultApiFetch({
702
+ token,
703
+ path: `/files/${encodeURIComponent(companyUid)}/members/${encodeURIComponent(uid)}/access`,
704
+ method: "GET",
705
+ });
706
+ if (!response.ok) {
707
+ if (response.status === 403) {
708
+ throw new Error("Only the person themself or a company owner/admin can check this access.");
709
+ }
710
+ if (response.status === 404) {
711
+ throw new Error("That person is not an active member of this company.");
712
+ }
713
+ throw new Error(`Member access check failed (${response.status}).`);
714
+ }
715
+ const projection = (await response.json());
716
+ if (projection.personUid !== uid || !projection.files) {
717
+ throw new Error("The member access response did not match the requested person.");
718
+ }
719
+ const roleBypass = projection.files.roleBypass === true;
720
+ const grants = Array.isArray(projection.files.grants) ? projection.files.grants : [];
721
+ const matchingPermissions = grants
722
+ .filter((grant) => typeof grant.path === "string" && typeof grant.permission === "string" &&
723
+ grant.permission in FILE_PERMISSION_RANK &&
724
+ fileGrantCoversPath(toBucketRelative(grant.path, company), relativePath))
725
+ .map((grant) => grant.permission);
726
+ const permission = roleBypass ? "admin" : matchingPermissions.reduce((best, candidate) => best === null || FILE_PERMISSION_RANK[candidate] > FILE_PERMISSION_RANK[best] ? candidate : best, null);
727
+ return {
728
+ personUid: uid,
729
+ company,
730
+ path: `companies/${company}/${relativePath}`,
731
+ permission,
732
+ canRead: permission !== null,
733
+ roleBypass,
734
+ exitCode: permission === null ? 3 : 0,
735
+ };
736
+ }
737
+ export function formatAccessForPerson(result) {
738
+ return `${result.personUid} has ${result.permission ?? "no access"} access to ${result.path}.`;
739
+ }
672
740
  export async function runAccessForPath(path, opts) {
673
741
  const target = path;
674
742
  const hqRoot = opts?.hqRoot ?? DEFAULT_HQ_ROOT;
@@ -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