@indigoai-us/hq-cli 5.345.54 → 5.345.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/assets/scaffold/core/scripts/derive-trigger-facts.sh +10 -1
  3. package/assets/scaffold/core/scripts/jobs-validate.sh +2 -1
  4. package/assets/scaffold/core/scripts/session-title.sh +18 -15
  5. package/dist/command-catalog.generated.d.ts +143 -2
  6. package/dist/command-catalog.generated.js +175 -2
  7. package/dist/command-registration-plan.d.ts +6 -1
  8. package/dist/command-registration-plan.js +2 -1
  9. package/dist/commands/cloud.d.ts +7 -0
  10. package/dist/commands/cloud.js +92 -16
  11. package/dist/commands/core-delegate-w1b.js +18 -5
  12. package/dist/commands/core-derive-trigger-facts-command.js +14 -0
  13. package/dist/commands/core-derive-trigger-facts-native.d.ts +1 -0
  14. package/dist/commands/core-derive-trigger-facts-native.js +20 -1
  15. package/dist/commands/daemon.js +16 -0
  16. package/dist/commands/doctor.d.ts +1 -0
  17. package/dist/commands/doctor.js +15 -2
  18. package/dist/commands/import-scan.d.ts +24 -0
  19. package/dist/commands/import-scan.js +75 -0
  20. package/dist/commands/lanes-checks.d.ts +3 -0
  21. package/dist/commands/lanes-checks.js +34 -0
  22. package/dist/commands/lanes-session-start.d.ts +17 -0
  23. package/dist/commands/lanes-session-start.js +195 -0
  24. package/dist/commands/lanes-shared.d.ts +2 -1
  25. package/dist/commands/lanes-watch.d.ts +2 -0
  26. package/dist/commands/lanes-watch.js +2 -1
  27. package/dist/commands/lanes.d.ts +3 -1
  28. package/dist/commands/lanes.js +121 -18
  29. package/dist/commands/members.d.ts +4 -0
  30. package/dist/commands/members.js +23 -2
  31. package/dist/lanes-drain-hook.js +6 -2
  32. package/dist/lib/daemon/conflict-notices.d.ts +4 -0
  33. package/dist/lib/daemon/conflict-notices.js +80 -0
  34. package/dist/lib/doctor/checks/sync-health.d.ts +2 -0
  35. package/dist/lib/doctor/checks/sync-health.js +47 -23
  36. package/dist/lib/import-scan/events.d.ts +135 -0
  37. package/dist/lib/import-scan/events.js +71 -0
  38. package/dist/lib/import-scan/run.d.ts +84 -0
  39. package/dist/lib/import-scan/run.js +308 -0
  40. package/dist/lib/import-scan/scan-event.schema.json +127 -0
  41. package/dist/lib/import-scan/validate.d.ts +31 -0
  42. package/dist/lib/import-scan/validate.js +111 -0
  43. package/dist/lib/lanes/dropbox.d.ts +4 -2
  44. package/dist/lib/lanes/dropbox.js +54 -9
  45. package/dist/lib/lanes/flag-keys.d.ts +10 -0
  46. package/dist/lib/lanes/flag-keys.js +10 -0
  47. package/dist/lib/lanes/hooks-install.d.ts +14 -11
  48. package/dist/lib/lanes/hooks-install.js +70 -50
  49. package/dist/lib/lanes/monitor.js +1 -1
  50. package/dist/lib/lanes/reaper.d.ts +58 -0
  51. package/dist/lib/lanes/reaper.js +403 -0
  52. package/dist/lib/lanes/registry-flag.d.ts +2 -0
  53. package/dist/lib/lanes/registry-flag.js +1 -1
  54. package/dist/lib/lanes/spawn.d.ts +2 -0
  55. package/dist/lib/lanes/spawn.js +32 -2
  56. package/dist/lib/lanes/status-fields.d.ts +12 -0
  57. package/dist/lib/lanes/status-fields.js +189 -0
  58. package/dist/lib/lanes/types.js +1 -1
  59. package/dist/lib/lanes/worker-continuity.d.ts +18 -0
  60. package/dist/lib/lanes/worker-continuity.js +81 -0
  61. package/dist/lib/lanes/worker-process.d.ts +4 -0
  62. package/dist/lib/lanes/worker-process.js +77 -0
  63. package/dist/main.js +9 -3
  64. package/dist/utils/cli-telemetry.d.ts +4 -1
  65. package/dist/utils/cli-telemetry.js +8 -0
  66. package/dist/utils/jq-windows-compat.d.ts +13 -0
  67. package/dist/utils/jq-windows-compat.js +48 -0
  68. package/dist/utils/run-bundled-script.d.ts +3 -0
  69. package/dist/utils/run-bundled-script.js +9 -80
  70. package/dist/utils/windows-bash.d.ts +5 -0
  71. package/dist/utils/windows-bash.js +75 -0
  72. package/package.json +1 -1
@@ -0,0 +1,195 @@
1
+ import fs from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
7
+ import { readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
8
+ import { LANE_CONDUCT_SESSION_START_FLAG } from "../lib/lanes/flag-keys.js";
9
+ function enabledFromSettings(root) {
10
+ const personal = path.join(root, "personal", "settings", "orchestrator.yaml");
11
+ const settings = fs.existsSync(personal) ? personal : path.join(root, "core", "settings", "orchestrator.yaml");
12
+ if (!fs.existsSync(settings))
13
+ return false;
14
+ const text = fs.readFileSync(settings, "utf8");
15
+ const block = /^conduct:\s*(?:#.*)?\r?\n((?:[ \t]+.*\r?\n?)*)/m.exec(text)?.[1] ?? "";
16
+ const value = /^\s+default_enabled:\s*["']?(true|false|yes|no|on|off|1|0)["']?\s*(?:#.*)?$/mi.exec(block)?.[1]?.toLowerCase();
17
+ return value === "true" || value === "yes" || value === "on" || value === "1";
18
+ }
19
+ function sessionWasDisabled(root, env, payloadSessionId) {
20
+ const sessionId = payloadSessionId || env.HQ_SESSION_ID;
21
+ if (!sessionId || !/^[A-Za-z0-9_-]+$/.test(sessionId))
22
+ return false;
23
+ const meta = path.join(root, "workspace", "sessions", sessionId, "meta.yaml");
24
+ if (!fs.existsSync(meta))
25
+ return false;
26
+ const text = fs.readFileSync(meta, "utf8");
27
+ return /^\s*(?:conduct_mode|conduct_enabled|conduct_default):\s*(?:off|false|no|0)\s*(?:#.*)?$/mi.test(text) ||
28
+ /^\s*conduct_engine:\s*(?:""|''|#.*)?\s*$/mi.test(text);
29
+ }
30
+ function extractConductorBlock(core) {
31
+ const start = core.indexOf("<!-- inject:start -->");
32
+ const end = core.indexOf("<!-- inject:end -->");
33
+ if (start < 0 || end < 0 || end <= start)
34
+ return null;
35
+ return core.slice(start + "<!-- inject:start -->".length, end).replace(/^\r?\n/, "").replace(/\r?\n$/, "");
36
+ }
37
+ const ORPHAN_SWEEP_LOCK_TTL_MS = 15 * 60_000;
38
+ /** Starts the CL-4 read-only orphan report once per host, without holding SessionStart open. */
39
+ export function startSessionStartOrphanSweep(root, env = process.env, spawnDetached = spawn) {
40
+ const stateRoot = env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
41
+ const host = os.hostname().replace(/[^A-Za-z0-9._-]/g, "_") || "unknown-host";
42
+ const stateDir = path.join(stateRoot, "hq-cli", "lanes-session-start", host);
43
+ const lockPath = path.join(stateDir, "orphan-sweep.lock");
44
+ const reportDir = path.join(stateRoot, "hq-cli", "lanes-orphan-reports", host);
45
+ const ownerToken = randomUUID();
46
+ try {
47
+ fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
48
+ fs.mkdirSync(reportDir, { recursive: true, mode: 0o700 });
49
+ let fd;
50
+ try {
51
+ fd = fs.openSync(lockPath, "wx", 0o600);
52
+ }
53
+ catch (error) {
54
+ if (error.code !== "EEXIST")
55
+ return;
56
+ let previous = {};
57
+ try {
58
+ previous = JSON.parse(fs.readFileSync(lockPath, "utf8"));
59
+ }
60
+ catch { /* malformed lease can be reclaimed */ }
61
+ const age = Date.now() - Number(previous.started_at || 0);
62
+ let running = false;
63
+ if (Number.isInteger(previous.pid) && Number(previous.pid) > 0) {
64
+ try {
65
+ process.kill(Number(previous.pid), 0);
66
+ running = true;
67
+ }
68
+ catch {
69
+ running = false;
70
+ }
71
+ }
72
+ if (age < ORPHAN_SWEEP_LOCK_TTL_MS && running)
73
+ return;
74
+ fs.rmSync(lockPath, { force: true });
75
+ try {
76
+ fd = fs.openSync(lockPath, "wx", 0o600);
77
+ }
78
+ catch {
79
+ return;
80
+ }
81
+ }
82
+ const startedAt = Date.now();
83
+ fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, started_at: startedAt, owner_token: ownerToken }));
84
+ fs.closeSync(fd);
85
+ const cli = process.argv[1];
86
+ if (!cli) {
87
+ fs.rmSync(lockPath, { force: true });
88
+ return;
89
+ }
90
+ const wrapper = [
91
+ "const cp=require('node:child_process'),fs=require('node:fs'),path=require('node:path');",
92
+ "const [cli,lock,root,token,report]=process.argv.slice(1);",
93
+ "const cleanup=()=>{try{const lease=JSON.parse(fs.readFileSync(lock,'utf8'));if(lease.owner_token===token)fs.rmSync(lock,{force:true})}catch{}};",
94
+ "const start=()=>{let lease;try{lease=JSON.parse(fs.readFileSync(lock,'utf8'))}catch{}if(lease?.owner_token!==token){if(Date.now()<Number(lease?.started_at||0)+1000)return setTimeout(start,10);process.exit(0)}",
95
+ "try{fs.writeFileSync(lock,JSON.stringify({...lease,pid:process.pid}))}catch{}",
96
+ "const tmp=report+'.tmp';let output;try{output=fs.createWriteStream(tmp,{flags:'wx',mode:0o600})}catch{cleanup();process.exit(0)}",
97
+ "const child=cp.spawn(process.execPath,[cli,'lanes','_reap','--json'],{stdio:['ignore','pipe','ignore'],env:{...process.env,HQ_ROOT:root,HQ_NO_UPDATE_CHECK:'1'}});",
98
+ "child.stdout.pipe(output);child.on('error',()=>{output.destroy();try{fs.rmSync(tmp,{force:true})}catch{}cleanup()});",
99
+ "child.on('exit',(code)=>output.end(()=>{try{if(code===0)fs.renameSync(tmp,report);else fs.rmSync(tmp,{force:true})}catch{}cleanup()}));};start();",
100
+ ].join("");
101
+ const reportPath = path.join(reportDir, `orphans-${startedAt}-${ownerToken}.json`);
102
+ const launcher = spawnDetached(process.execPath, ["-e", wrapper, cli, lockPath, root, ownerToken, reportPath], {
103
+ detached: true,
104
+ stdio: "ignore",
105
+ env: { ...env, HQ_ROOT: root, HQ_NO_UPDATE_CHECK: "1" },
106
+ });
107
+ if (launcher.pid) {
108
+ try {
109
+ fs.writeFileSync(lockPath, JSON.stringify({ pid: launcher.pid, started_at: startedAt, owner_token: ownerToken }));
110
+ }
111
+ catch { /* TTL lease still bounds an orphaned lock */ }
112
+ }
113
+ launcher.on("error", () => {
114
+ try {
115
+ const lease = JSON.parse(fs.readFileSync(lockPath, "utf8"));
116
+ if (lease.owner_token === ownerToken)
117
+ fs.rmSync(lockPath, { force: true });
118
+ }
119
+ catch { /* The lease may already have been reclaimed. */ }
120
+ });
121
+ launcher.unref();
122
+ }
123
+ catch {
124
+ // SessionStart's orphan report is advisory and must never block startup.
125
+ }
126
+ }
127
+ /** SessionStart-compatible, fail-open conduct bootstrap. */
128
+ export async function runLaneSessionStart(options = {}) {
129
+ const env = options.env ?? process.env;
130
+ const stdin = options.stdin ?? process.stdin;
131
+ const stdout = options.stdout ?? process.stdout;
132
+ const stderr = options.stderr ?? process.stderr;
133
+ let debugLineWritten = false;
134
+ const debug = (message) => {
135
+ if (env.HQ_LANES_DEBUG === "1" && !debugLineWritten) {
136
+ debugLineWritten = true;
137
+ stderr.write(`${message}\n`);
138
+ }
139
+ };
140
+ try {
141
+ const chunks = [];
142
+ for await (const chunk of stdin)
143
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
144
+ const input = Buffer.concat(chunks).toString("utf8");
145
+ let source = "startup";
146
+ let payloadSessionId;
147
+ try {
148
+ const payload = JSON.parse(input);
149
+ source = payload.source || "startup";
150
+ payloadSessionId = payload.session_id;
151
+ }
152
+ catch { /* malformed hook input uses startup default */ }
153
+ if (source !== "startup")
154
+ return;
155
+ const disabledHooks = `,${env.HQ_DISABLED_HOOKS ?? ""},`.replace(/\s/g, "");
156
+ if (/(^|,)auto-conduct(,|$)/.test(disabledHooks))
157
+ return;
158
+ const root = (options.resolveRoot ?? ((e, cwd) => resolveLanesHqRoot({ env: e, cwd })))(env, options.cwd);
159
+ const flagEnabled = await readLanesRegistryFlagFailOpen(LANE_CONDUCT_SESSION_START_FLAG, {
160
+ env,
161
+ defaultValue: true,
162
+ reportFailure: (error) => {
163
+ const kind = error instanceof Error ? error.name : "Error";
164
+ debug(`hq lanes session-start: flag read failed (${kind})`);
165
+ },
166
+ });
167
+ if (!flagEnabled)
168
+ return;
169
+ (options.startSweep ?? ((r, e) => startSessionStartOrphanSweep(r, e, spawn)))(root, env);
170
+ if (sessionWasDisabled(root, env, payloadSessionId))
171
+ return;
172
+ let enabled = enabledFromSettings(root);
173
+ if (/^(1|true|on|yes)$/i.test(env.HQ_AUTO_CONDUCT ?? ""))
174
+ enabled = true;
175
+ else if (/^(0|false|off|no)$/i.test(env.HQ_AUTO_CONDUCT ?? ""))
176
+ enabled = false;
177
+ if (!enabled)
178
+ return;
179
+ const core = path.join(root, ".claude", "skills", "conduct", "conductor-core.md");
180
+ if (!fs.existsSync(core))
181
+ return;
182
+ const block = extractConductorBlock(fs.readFileSync(core, "utf8"));
183
+ if (block)
184
+ stdout.write(`${block}\n`);
185
+ }
186
+ catch {
187
+ debug("hq lanes session-start: bootstrap failed");
188
+ }
189
+ }
190
+ export function registerLaneSessionStartCommand(lanes) {
191
+ lanes.command("session-start", { hidden: true })
192
+ .description("Internal SessionStart bootstrap")
193
+ .action(async () => runLaneSessionStart());
194
+ }
195
+ //# sourceMappingURL=lanes-session-start.js.map
@@ -1,4 +1,4 @@
1
- import type { LaneCompanyRef, LanePlacementPolicy, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LaneAccountSelection, PendingResume, QueuedReason, ResolvedLaneWorker, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneHireLocks } from "../lib/lanes/index.js";
1
+ import type { LaneCompanyRef, LanePlacementPolicy, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LaneAccountSelection, PendingResume, QueuedReason, ResolvedLaneWorker, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneAdmissionLock, withLaneHireLocks } from "../lib/lanes/index.js";
2
2
  import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
3
3
  import type { OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
4
4
  import type { CpuBusySource } from "../lib/lanes/throttle.js";
@@ -313,6 +313,7 @@ export interface Ctx {
313
313
  storyLedgerEnabled: () => boolean;
314
314
  isSsmOwnerDispatch: () => boolean;
315
315
  withGrantHireLocks: typeof withLaneHireLocks;
316
+ withAdmissionLock: typeof withLaneAdmissionLock;
316
317
  mergeHoldEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
317
318
  mergeReviewEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
318
319
  }
@@ -1,4 +1,6 @@
1
+ import { type MonitorEngine } from "../lib/lanes/index.js";
1
2
  import type { Ctx, MonitorCheckOptions, LaneWaitCommandOptions, LaneWatchHandlerDeps, LaneWatchOptions } from "./lanes-shared.js";
3
+ export declare function resolveMonitorEngine(requested: string | undefined, env: NodeJS.ProcessEnv): MonitorEngine;
2
4
  export declare function createLaneWatchHandlers(deps: LaneWatchHandlerDeps): {
3
5
  runWatch: (ctx: Ctx, laneIds: string[], opts: LaneWatchOptions) => Promise<number>;
4
6
  runMonitorCheck: (ctx: Ctx, opts: MonitorCheckOptions) => Promise<number>;
@@ -10,8 +10,9 @@ const MONITOR_ACTIVE_LANE_STATES = new Set([
10
10
  "awaiting_input",
11
11
  "blocked",
12
12
  ]);
13
- function resolveMonitorEngine(requested, env) {
13
+ export function resolveMonitorEngine(requested, env) {
14
14
  const value = requested?.trim().toLowerCase() ||
15
+ env.HQ_LANE_ENGINE?.trim().toLowerCase() ||
15
16
  env.HQ_CONDUCT_ENGINE?.trim().toLowerCase() ||
16
17
  "claude";
17
18
  if (!MONITOR_ENGINES.includes(value)) {
@@ -17,7 +17,7 @@ export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/
17
17
  export type { OutpostIdentity } from "../lib/lanes/admission-host.js";
18
18
  import { type OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
19
19
  import { emitLaneDecisionEvents } from "../lib/lanes/mesh-events.js";
20
- import { MAX_LANE_DEPTH, withLaneHireLocks, type LaneCompanyRef, type QuestionBody, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
20
+ import { MAX_LANE_DEPTH, withLaneAdmissionLock, withLaneHireLocks, type LaneCompanyRef, type QuestionBody, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
21
21
  import { questionBodyAbsPath } from "../lib/lanes/questions.js";
22
22
  import { type RunCompactionFn } from "../lib/lanes/compaction.js";
23
23
  import type { WatcherLoopDeps } from "../lib/lanes/watcher.js";
@@ -106,6 +106,8 @@ export interface LanesCommandDeps {
106
106
  isSsmOwnerDispatch?: () => boolean;
107
107
  /** Test seam for changing a lane between pre-lock and locked grant reads. */
108
108
  withGrantHireLocks?: typeof withLaneHireLocks;
109
+ /** Test seam proving create admission encloses the worker busy check. */
110
+ withAdmissionLock?: typeof withLaneAdmissionLock;
109
111
  /** Test seam for the review-before-merge registry flag. */
110
112
  mergeReviewEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
111
113
  /** Test seam for the repository-merge-hold registry flag. */
@@ -33,13 +33,16 @@ import { registerLaneReviewCommand } from "./lanes-review.js";
33
33
  import { ensureCognitoToken } from "../utils/cognito-session.js";
34
34
  import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
35
35
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
36
+ import { registerLaneSessionStartCommand } from "./lanes-session-start.js";
36
37
  import { laneDepthCap } from "../lib/lanes/depth-cap.js";
37
38
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
38
39
  import { runResidentCommandWithPackageLease } from "../utils/resident-package-preload.js";
39
40
  import { readLanesFlagLocalOverride, readLanesRegistryFlag, readLanesRegistryFlagFailOff, readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
41
+ import { readLaneStatusRowFields } from "../lib/lanes/status-fields.js";
40
42
  import { appendProjectFocusOverride, readProjectStoryFocusData, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
41
43
  import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } from "../lib/lanes/focus-guard.js";
42
- import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, } from "../lib/lanes/flag-keys.js";
44
+ import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_STATUS_ROW_FIELDS_FLAG, LANE_REAP_STOP_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, LANE_WORKER_CONTINUITY_FLAG, } from "../lib/lanes/flag-keys.js";
45
+ import { findWorkerContinuityLane, sameLaneHost, workerContinuityPrompt } from "../lib/lanes/worker-continuity.js";
43
46
  import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
44
47
  import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
45
48
  import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
@@ -461,6 +464,7 @@ function resolveDeps(deps = {}) {
461
464
  focusGuardEnabled: deps.focusGuardEnabled,
462
465
  isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
463
466
  withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
467
+ withAdmissionLock: deps.withAdmissionLock ?? withLaneAdmissionLock,
464
468
  mergeReviewEnabled: deps.mergeReviewEnabled,
465
469
  mergeHoldEnabled: deps.mergeHoldEnabled,
466
470
  };
@@ -3364,6 +3368,16 @@ async function runCreate(ctx, opts) {
3364
3368
  const provider = explicitProvider ??
3365
3369
  profile?.meta.provider ??
3366
3370
  parseLaneProvider("codex");
3371
+ const continuityEnabled = opts.worker !== undefined && await readLanesRegistryFlagFailOpen(LANE_WORKER_CONTINUITY_FLAG, { company: company ?? meshCompany, env: ctx.env });
3372
+ let workerContinuity = continuityEnabled
3373
+ ? findWorkerContinuityLane(loadAllLanes(hqRoot), {
3374
+ senior,
3375
+ worker: resolvedWorker?.id ?? opts.worker,
3376
+ provider,
3377
+ company: company ?? meshCompany,
3378
+ freshThread: opts.freshThread,
3379
+ })
3380
+ : { resumable: false };
3367
3381
  const profileDefaultsApply = profile !== undefined && profile.meta.provider === provider;
3368
3382
  const cliModel = parseCliModel(opts.model);
3369
3383
  // Validated here because `claude --effort <unknown>` only warns and runs.
@@ -3738,6 +3752,24 @@ async function runCreate(ctx, opts) {
3738
3752
  // The lane id is created in `persist`; its Work Mesh thread is ensured
3739
3753
  // immediately afterward from that stable identity.
3740
3754
  const persist = async () => {
3755
+ const host = placementHostOverride ?? (outpostId !== undefined
3756
+ ? { kind: "outpost", id: outpostId }
3757
+ : { kind: "local", id: ctx.hostname() });
3758
+ // Repeat the busy check while holding provider admission serialization so
3759
+ // two concurrent creates cannot both claim one worker's provider thread.
3760
+ if (continuityEnabled) {
3761
+ workerContinuity = findWorkerContinuityLane(loadAllLanes(hqRoot), {
3762
+ senior,
3763
+ worker: resolvedWorker?.id ?? opts.worker,
3764
+ provider,
3765
+ company: company ?? meshCompany,
3766
+ freshThread: opts.freshThread,
3767
+ });
3768
+ if (workerContinuity.resumable && workerContinuity.previous &&
3769
+ !sameLaneHost(workerContinuity.previous.host, host)) {
3770
+ workerContinuity = { previous: workerContinuity.previous, resumable: false, fallbackReason: "host_mismatch" };
3771
+ }
3772
+ }
3741
3773
  let admission;
3742
3774
  let validation;
3743
3775
  if (!outpostId && provider === "claude" && explicitBalanceAccount === undefined &&
@@ -3841,9 +3873,6 @@ async function runCreate(ctx, opts) {
3841
3873
  throw admission.error;
3842
3874
  }
3843
3875
  }
3844
- const host = placementHostOverride ?? (outpostId !== undefined
3845
- ? { kind: "outpost", id: outpostId }
3846
- : { kind: "local", id: ctx.hostname() });
3847
3876
  const queuedReason = admission.action === "queue" ? admission.reason : undefined;
3848
3877
  const admissionReservedAt = admission.action === "start" ? ctx.now().toISOString() : undefined;
3849
3878
  const laneInit = {
@@ -3887,6 +3916,18 @@ async function runCreate(ctx, opts) {
3887
3916
  keep_alive: keepAlive,
3888
3917
  ...(keepAlive ? { keep_alive_opted_in_at: createdAt } : {}),
3889
3918
  provider,
3919
+ ...(workerContinuity.resumable && workerContinuity.previous
3920
+ ? {
3921
+ provider_session_id: workerContinuity.previous.provider_session_id,
3922
+ last_message_at: workerContinuity.previous.last_message_at ?? workerContinuity.previous.created_at,
3923
+ pending_resume: {
3924
+ session_id: workerContinuity.previous.provider_session_id,
3925
+ at: createdAt,
3926
+ trigger: "resume",
3927
+ resumed_from: workerContinuity.previous.state,
3928
+ },
3929
+ }
3930
+ : {}),
3890
3931
  provider_source: explicitProvider !== undefined
3891
3932
  ? "cli"
3892
3933
  : profile?.meta.provider !== undefined
@@ -3956,12 +3997,7 @@ async function runCreate(ctx, opts) {
3956
3997
  return { lane, admission, validation };
3957
3998
  };
3958
3999
  let persisted;
3959
- if (outpostId) {
3960
- persisted = await persist();
3961
- }
3962
- else {
3963
- persisted = await withLaneAdmissionLock(hqRoot, provider, persist, ctx.sleep);
3964
- }
4000
+ persisted = await ctx.withAdmissionLock(hqRoot, provider, persist, ctx.sleep);
3965
4001
  let lane = persisted.lane;
3966
4002
  const { admission, validation } = persisted;
3967
4003
  if (focusAssessment.enabled && opts.focusOverride !== undefined) {
@@ -4051,10 +4087,25 @@ async function runCreate(ctx, opts) {
4051
4087
  ensureDir(path.join(dest, "dropbox"));
4052
4088
  ensureDir(path.join(dest, "questions"));
4053
4089
  const taskBrief = fs.readFileSync(briefAbs, "utf8");
4054
- const briefText = resolvedWorker
4090
+ const previousResult = workerContinuity.previous
4091
+ ? readLatestEnvelopeRecord(hqRoot, workerContinuity.previous.lane_id)
4092
+ : undefined;
4093
+ const briefText = `${workerContinuity.resumable && workerContinuity.previous
4094
+ ? workerContinuityPrompt(workerContinuity.previous, previousResult?.envelope.summary, previousResult?.ref)
4095
+ : ""}${resolvedWorker
4055
4096
  ? composeWorkerBrief(resolvedWorker, taskBrief)
4056
- : composeGeneralBrief(taskBrief);
4097
+ : composeGeneralBrief(taskBrief)}`;
4057
4098
  fs.writeFileSync(path.join(dest, "brief.md"), briefText);
4099
+ if (workerContinuity.resumable && lane.pending_resume) {
4100
+ const newBriefPath = path.join(dest, "brief.md");
4101
+ lane = casUpdateLane(hqRoot, lane.lane_id, (current) => ({
4102
+ ...current,
4103
+ pending_resume: current.pending_resume ? {
4104
+ ...current.pending_resume,
4105
+ text: `The new brief at ${newBriefPath} is the instruction for this turn. Read and follow it.`,
4106
+ } : undefined,
4107
+ }));
4108
+ }
4058
4109
  if (timeoutSecs !== undefined) {
4059
4110
  writeJsonFile(timeoutPath(hqRoot, lane.lane_id), {
4060
4111
  timeout_secs: timeoutSecs,
@@ -4121,6 +4172,16 @@ async function runCreate(ctx, opts) {
4121
4172
  stories: laneStories(started),
4122
4173
  story_warning: combinedStoryWarning(started, createStoryWarnings),
4123
4174
  thread_id: started.thread_id ?? null,
4175
+ ...(continuityEnabled ? {
4176
+ worker_continuity: workerContinuity.resumable
4177
+ ? { action: "resume", previous_lane_id: workerContinuity.previous?.lane_id }
4178
+ : {
4179
+ action: "fresh",
4180
+ ...(workerContinuity.previous ? { previous_lane_id: workerContinuity.previous.lane_id } : {}),
4181
+ ...(opts.freshThread ? { reason: "fresh_thread_requested" } : {}),
4182
+ ...(workerContinuity.fallbackReason ? { reason: workerContinuity.fallbackReason } : {}),
4183
+ },
4184
+ } : {}),
4124
4185
  senior: started.senior,
4125
4186
  depth: started.depth,
4126
4187
  timeout_secs: timeoutSecs ?? null,
@@ -5342,6 +5403,24 @@ async function runPickup(ctx, opts) {
5342
5403
  lane: started,
5343
5404
  });
5344
5405
  }
5406
+ async function laneStatusFieldsForCommand(ctx, hqRoot, lane, now) {
5407
+ const resolved = resolveLaneCompany(hqRoot, lane).company;
5408
+ let enabled = true;
5409
+ try {
5410
+ enabled = await readLanesRegistryFlagFailOpen(LANE_STATUS_ROW_FIELDS_FLAG, {
5411
+ ...(resolved ? { company: resolved } : {}),
5412
+ env: ctx.env,
5413
+ });
5414
+ }
5415
+ catch (err) {
5416
+ process.stderr.write(`hq lanes: status row fields flag read failed open: ${err instanceof Error ? err.message : String(err)}\n`);
5417
+ }
5418
+ if (!enabled)
5419
+ return {};
5420
+ const host = reconcileHostFromCtx(ctx);
5421
+ const local = laneRunsHere(lane, host);
5422
+ return { ...readLaneStatusRowFields(hqRoot, lane, now.getTime(), local) };
5423
+ }
5345
5424
  async function runList(ctx, opts) {
5346
5425
  if (opts.json && opts.human) {
5347
5426
  throw new LanesError("conflicting_output_format", "Use either --json or --human for hq lanes list", { json: true, human: true });
@@ -5364,7 +5443,7 @@ async function runList(ctx, opts) {
5364
5443
  new Set(opts.hierarchy).size !== opts.hierarchy.length)) {
5365
5444
  throw new LanesError("lane_list_hierarchy_invalid", "--hierarchy must be repeated with distinct, non-empty lane ids without surrounding whitespace", {});
5366
5445
  }
5367
- if (opts.snapshot && (opts.human || opts.company || opts.project || opts.story || opts.state || opts.senior)) {
5446
+ if (opts.snapshot && (opts.human || opts.company || opts.project || opts.story || opts.state || opts.senior || opts.worker)) {
5368
5447
  throw new LanesError("lane_list_snapshot_invalid", "--snapshot only supports owner lane filters and JSON output", {});
5369
5448
  }
5370
5449
  if (opts.hierarchy !== undefined && !opts.snapshot) {
@@ -5413,21 +5492,29 @@ async function runList(ctx, opts) {
5413
5492
  }
5414
5493
  if (senior && seniorKey(lane.senior) !== seniorKey(senior))
5415
5494
  return false;
5495
+ if (opts.worker && lane.worker !== opts.worker)
5496
+ return false;
5416
5497
  return true;
5417
5498
  };
5418
5499
  const selectLanes = () => laneSource
5419
5500
  .filter((lane) => matchesLaneFilters(lane, true))
5420
5501
  .sort(compareLaneQueueOrder);
5421
5502
  let selectedLanes = selectLanes();
5503
+ const statusFieldsByLane = new Map();
5504
+ const statusNow = ctx.now();
5505
+ await Promise.all(selectedLanes.map(async (lane) => {
5506
+ statusFieldsByLane.set(lane.lane_id, await laneStatusFieldsForCommand(ctx, hqRoot, lane, statusNow));
5507
+ }));
5422
5508
  if (opts.snapshot) {
5423
5509
  printJson({
5424
5510
  ok: true,
5425
5511
  action: "list",
5426
5512
  lanes: selectedLanes.map((lane) => {
5513
+ const fields = statusFieldsByLane.get(lane.lane_id) ?? {};
5427
5514
  if (!isTerminalLaneState(lane.state))
5428
- return lane;
5515
+ return { ...lane, ...fields };
5429
5516
  const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
5430
- return envelope?.summary ? { ...lane, envelope_summary: envelope.summary } : lane;
5517
+ return { ...lane, ...fields, ...(envelope?.summary ? { envelope_summary: envelope.summary } : {}) };
5431
5518
  }),
5432
5519
  });
5433
5520
  return;
@@ -5531,6 +5618,7 @@ async function runList(ctx, opts) {
5531
5618
  !laneRunsHere(lane, host);
5532
5619
  return {
5533
5620
  ...lane,
5621
+ ...statusFieldsByLane.get(lane.lane_id),
5534
5622
  ...hireByLane.get(lane.lane_id),
5535
5623
  ...(remoteRefresh.unverifiedRemote.has(lane.lane_id) ? { unverified_remote: true } : {}),
5536
5624
  stories: laneStories(lane),
@@ -5900,7 +5988,10 @@ async function runShow(ctx, laneId, hasPendingQuestionForLane, resolveLaneTarget
5900
5988
  })
5901
5989
  .slice(0, 20)
5902
5990
  .map(({ repo, pr, head_sha, recorded_at }) => ({ repo, pr, head_sha, recorded_at }));
5903
- const laneForShow = { ...lane };
5991
+ const laneForShow = {
5992
+ ...lane,
5993
+ ...await laneStatusFieldsForCommand(ctx, hqRoot, storedLane, ctx.now()),
5994
+ };
5904
5995
  delete laneForShow.resume_history;
5905
5996
  printJson({
5906
5997
  ok: true,
@@ -9234,8 +9325,9 @@ export function registerLanesCommand(parent, deps = {}) {
9234
9325
  timeoutPath,
9235
9326
  });
9236
9327
  const lanes = parent
9237
- .command("lanes", { hidden: true })
9328
+ .command("lanes")
9238
9329
  .description("Create, supervise, and steer worker lanes; routed decision queue");
9330
+ registerLaneSessionStartCommand(lanes);
9239
9331
  registerLaneChecksCommand(lanes, {
9240
9332
  hqRoot: ctx.hqRoot,
9241
9333
  resolveCompany: ctx.resolveCompany,
@@ -9246,6 +9338,15 @@ export function registerLanesCommand(parent, deps = {}) {
9246
9338
  presenceConnected: ctx.presenceConnected ?? false,
9247
9339
  deps: ctx.livenessDeps,
9248
9340
  }),
9341
+ stopEnabled: (lane) => {
9342
+ const resolution = resolveLaneCompany(ctx.hqRoot(), lane);
9343
+ const company = resolution.company ?? undefined;
9344
+ return readLanesRegistryFlag(LANE_REAP_STOP_FLAG, {
9345
+ company,
9346
+ ...(company ? { companyUidSource: laneCompanyUidSource(resolution.source) } : {}),
9347
+ env: ctx.env,
9348
+ });
9349
+ },
9249
9350
  }, wrap);
9250
9351
  registerLaneDropboxRepairCommand(lanes, {
9251
9352
  hqRoot: ctx.hqRoot,
@@ -9311,6 +9412,7 @@ export function registerLanesCommand(parent, deps = {}) {
9311
9412
  .option("--additional-story <id>", "Additional story carried by this lane; repeatable and discouraged when active", collectRepeatable)
9312
9413
  .requiredOption("--brief-file <path>", "Path to the lane brief")
9313
9414
  .option("--worker <name>", "HQ worker or reusable profile id")
9415
+ .option("--fresh-thread", "Skip worker continuity and start a cold provider session")
9314
9416
  .option("--provider <name>", "claude | codex | grok")
9315
9417
  .option("--account <id>", "Provider account id")
9316
9418
  .option("--model <id>", "Model id for the provider CLI")
@@ -9410,6 +9512,7 @@ export function registerLanesCommand(parent, deps = {}) {
9410
9512
  .option("--story <id>", "Filter by story")
9411
9513
  .option("--state <state>", `Filter by state (${LANE_STATES.join("|")})`)
9412
9514
  .option("--senior <ref>", "Filter by senior ref")
9515
+ .option("--worker <id>", "Filter by worker id")
9413
9516
  .option("--lane <id>", "Filter by lane id (repeatable)", (value, previous = []) => [...previous, value], [])
9414
9517
  .addOption(new Option("--lane-ulid <ulid>", "Filter by lane ULID for internal owner lookup").hideHelp()
9415
9518
  .argParser((value, previous = []) => [...previous, value]))
@@ -9506,7 +9609,7 @@ export function registerLanesCommand(parent, deps = {}) {
9506
9609
  .command("monitor-check")
9507
9610
  .description("Check that active Claude senior lanes have live Monitor watch tasks")
9508
9611
  .option("--session <id>", "Senior session id")
9509
- .option("--engine <engine>", "claude | codex | grok; defaults to HQ_CONDUCT_ENGINE or claude")
9612
+ .option("--engine <engine>", "claude | codex | grok; defaults to HQ_LANE_ENGINE (HQ_CONDUCT_ENGINE is deprecated) or claude")
9510
9613
  .option("--reminder", "Print the senior Monitor reminder text")
9511
9614
  .option("--json", "JSON on stdout")
9512
9615
  .action(wrapText(async (opts) => watchHandlers.runMonitorCheck(ctx, opts)));
@@ -48,6 +48,8 @@ export interface InviteOptions {
48
48
  companyUid: string;
49
49
  callerUid: string;
50
50
  token: string;
51
+ /** Reports only whether the request returned a 2xx response. */
52
+ onRequestResult?: (ok: boolean) => void;
51
53
  }
52
54
  /**
53
55
  * Outcome of `hq members invite`. Two shapes depending on server schema:
@@ -96,6 +98,8 @@ export interface ResendInviteOptions {
96
98
  companyUid: string;
97
99
  callerUid: string;
98
100
  token: string;
101
+ /** Reports only whether the request returned a 2xx response. */
102
+ onRequestResult?: (ok: boolean) => void;
99
103
  }
100
104
  /**
101
105
  * Outcome of `hq members invite --resend`. No new membership row was
@@ -3,6 +3,7 @@ import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
3
3
  import { gateApiKeyCapabilities } from "../utils/api-key-command-gate.js";
4
4
  import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
5
5
  import { isPlanGateError } from "../utils/plan-gate-error.js";
6
+ import { queueCliFeatureUsed } from "../utils/cli-telemetry.js";
6
7
  const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
7
8
  const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
8
9
  const AGENT_UID_PATTERN = /^agt_[A-Za-z0-9_-]+$/i;
@@ -107,6 +108,7 @@ export async function inviteMember(options) {
107
108
  body,
108
109
  });
109
110
  if (!res.ok) {
111
+ options.onRequestResult?.(false);
110
112
  const err = (await res.json().catch(() => ({})));
111
113
  throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
112
114
  }
@@ -116,6 +118,7 @@ export async function inviteMember(options) {
116
118
  throw new Error(`Invite endpoint returned 2xx with no membership row (response keys: ${keys}). ` +
117
119
  "This is a server-side regression — file an issue.");
118
120
  }
121
+ options.onRequestResult?.(true);
119
122
  // Two server schemas in the wild:
120
123
  // - Legacy (schemaVersion ≤ 1): response carries a random `inviteToken`
121
124
  // the invitee redeems via `hq://accept/{token}`.
@@ -177,6 +180,7 @@ export async function resendInvite(options) {
177
180
  },
178
181
  });
179
182
  if (!res.ok) {
183
+ options.onRequestResult?.(false);
180
184
  const err = (await res.json().catch(() => ({})));
181
185
  throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
182
186
  }
@@ -184,6 +188,7 @@ export async function resendInvite(options) {
184
188
  if (data.resent !== true) {
185
189
  throw new Error("Resend endpoint returned 2xx without `resent: true` — the connected hq-pro likely doesn't support `resend: true` yet. Upgrade hq-pro or re-invite without --resend.");
186
190
  }
191
+ options.onRequestResult?.(true);
187
192
  return {
188
193
  resent: true,
189
194
  emailSent: data.emailSent === true,
@@ -436,6 +441,7 @@ export function registerMembersCommand(program) {
436
441
  let token;
437
442
  let companyUid;
438
443
  let companySlug;
444
+ let outcome = "failure";
439
445
  try {
440
446
  token = (await resolveVaultCredential()).token;
441
447
  companySlug = members.opts().company;
@@ -448,6 +454,9 @@ export function registerMembersCommand(program) {
448
454
  companyUid,
449
455
  callerUid,
450
456
  token,
457
+ onRequestResult: (ok) => {
458
+ outcome = ok ? "success" : "failure";
459
+ },
451
460
  });
452
461
  console.log(chalk.green(`Re-fired invite email for ${target.toLowerCase()} (existing pending row left untouched)`));
453
462
  console.log();
@@ -472,6 +481,9 @@ export function registerMembersCommand(program) {
472
481
  companyUid,
473
482
  callerUid,
474
483
  token,
484
+ onRequestResult: (ok) => {
485
+ outcome = ok ? "success" : "failure";
486
+ },
475
487
  });
476
488
  const agentTarget = detectTarget(target)?.isAgent === true;
477
489
  console.log(chalk.green(`Invited ${target} as ${result.membership.role} (status: ${result.membership.status})`));
@@ -612,10 +624,19 @@ export function registerMembersCommand(program) {
612
624
  }
613
625
  if (err instanceof InviteHttpError) {
614
626
  console.error(chalk.red(formatInviteHttpError(err.status, err.message, err.code)));
615
- process.exit(1);
627
+ process.exitCode = 1;
628
+ return;
616
629
  }
617
630
  console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
618
- process.exit(1);
631
+ process.exitCode = 1;
632
+ return;
633
+ }
634
+ finally {
635
+ queueCliFeatureUsed({
636
+ kind: "members_invite",
637
+ outcome,
638
+ ...(companyUid ? { companyUid } : {}),
639
+ });
619
640
  }
620
641
  });
621
642
  members