@indigoai-us/hq-cli 5.254.0 → 5.255.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.255.0] — 2026-09-28
6
+
7
+ ### Changed
8
+
9
+ - Internal: moved watch, monitor-check, and wait handlers to src/commands/lanes-watch.ts; behavior is unchanged.
10
+
5
11
  ## [5.254.0] — 2026-09-28
6
12
 
7
13
  ### Added
@@ -14,6 +20,9 @@
14
20
  ### Fixed
15
21
 
16
22
  - The end-of-session work report is no longer dropped when the background
23
+ transcript watcher polls late or adopts a session after a restart. A
24
+ synthesized `turn_end` now carries the transcript time, capped at now, and
25
+ is never earlier than the last event sent for the session.
17
26
  - `hq db sql --remote` is hidden because remote SQL is not available in this
18
27
  CLI build. Existing scripts get a clear error, and local SQL remains
19
28
  available without `--remote`.
@@ -7,6 +7,7 @@ import type { WatcherLoopDeps } from "../lib/lanes/watcher.js";
7
7
  import type { RunCompactionFn } from "../lib/lanes/compaction.js";
8
8
  import type { LaneMesh, LaneMeshEvent } from "../lib/lanes/mesh-events.js";
9
9
  import type { BalanceHostGap, BalanceHostSnapshot } from "../lib/lanes/claude-balance.js";
10
+ import type { HostCapacitySnapshot } from "../lib/lanes/host-capacity.js";
10
11
  import { type ResolvedEffort } from "../lib/lanes/effort.js";
11
12
  import type { SessionCopyResult } from "../lib/lanes/move-account.js";
12
13
  import type { WorkerProfile } from "../lib/lanes/profile.js";
@@ -223,6 +224,32 @@ export interface Ctx {
223
224
  laneTrackerEnabled: (company: LaneCompanyRef) => boolean | Promise<boolean>;
224
225
  trackerActor: () => string | null;
225
226
  }
227
+ export interface LaneWaitCommandOptions {
228
+ any?: string[];
229
+ for?: string[];
230
+ timeout?: string;
231
+ json?: boolean;
232
+ }
233
+ export interface MonitorCheckOptions {
234
+ session?: string;
235
+ engine?: string;
236
+ json?: boolean;
237
+ reminder?: boolean;
238
+ }
239
+ export interface LaneWatchOptions {
240
+ intervalMs?: string;
241
+ json?: boolean;
242
+ }
243
+ export interface LaneWatchHandlerDeps {
244
+ laneHostFromCtx(ctx: Ctx): LaneRecord["host"];
245
+ loadAllLanes(hqRoot: string): LaneRecord[];
246
+ loadClaudeBalanceHostSnapshots(ctx: Ctx, hqRoot: string, config: LanesConfig): Promise<{
247
+ snapshots: BalanceHostSnapshot[];
248
+ unavailableHosts: BalanceHostGap[];
249
+ }>;
250
+ printJson(value: unknown): void;
251
+ readOutpostCapacity(ctx: Ctx, outpostId: string): Promise<HostCapacitySnapshot>;
252
+ }
226
253
  export interface PrimaryOwnedOutpostQuestion {
227
254
  question: QuestionRecord;
228
255
  lane: LaneRecord;
@@ -0,0 +1,7 @@
1
+ import type { Ctx, MonitorCheckOptions, LaneWaitCommandOptions, LaneWatchHandlerDeps, LaneWatchOptions } from "./lanes-shared.js";
2
+ export declare function createLaneWatchHandlers(deps: LaneWatchHandlerDeps): {
3
+ runWatch: (ctx: Ctx, laneIds: string[], opts: LaneWatchOptions) => Promise<number>;
4
+ runMonitorCheck: (ctx: Ctx, opts: MonitorCheckOptions) => Promise<number>;
5
+ runWait: (ctx: Ctx, opts: LaneWaitCommandOptions) => Promise<number>;
6
+ };
7
+ //# sourceMappingURL=lanes-watch.d.ts.map
@@ -0,0 +1,215 @@
1
+ import { accountStatus, coveredLaneIds, loadLanesConfig, MAX_WATCHED_LANES, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, monitorReminderText, runLaneWatch, } from "../lib/lanes/index.js";
2
+ import { LEAD_LANE_KEEP_ALIVE_FLAG } from "../lib/lanes/keep-alive.js";
3
+ import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
4
+ import { isLanesError, LanesError } from "../lib/lanes/errors.js";
5
+ import { LANE_WAIT_INVALID_INPUT_EXIT_CODE, LANE_WAIT_TIMEOUT_EXIT_CODE, runLaneWait, } from "../lib/lanes/wait.js";
6
+ const MONITOR_ACTIVE_LANE_STATES = new Set([
7
+ "running",
8
+ "queued",
9
+ "awaiting_input",
10
+ "blocked",
11
+ ]);
12
+ function resolveMonitorEngine(requested, env) {
13
+ const value = requested?.trim().toLowerCase() ||
14
+ env.HQ_CONDUCT_ENGINE?.trim().toLowerCase() ||
15
+ "claude";
16
+ if (!MONITOR_ENGINES.includes(value)) {
17
+ throw new LanesError("monitor_invalid_engine", `Unknown monitor-check engine ${JSON.stringify(value)}. Accepted: ${MONITOR_ENGINES.join(", ")}`, { accepted: [...MONITOR_ENGINES], value });
18
+ }
19
+ return value;
20
+ }
21
+ function monitorCallsForLaneIds(laneIds) {
22
+ const calls = [];
23
+ for (let offset = 0; offset < laneIds.length; offset += MAX_WATCHED_LANES) {
24
+ const chunk = laneIds.slice(offset, offset + MAX_WATCHED_LANES);
25
+ calls.push({
26
+ command: `hq lanes watch ${chunk.join(" ")}`,
27
+ timeout_ms: MONITOR_MAX_TIMEOUT_MS,
28
+ persistent: true,
29
+ });
30
+ }
31
+ return calls;
32
+ }
33
+ function laneWaitHumanLine(result) {
34
+ const details = Object.entries(result.change)
35
+ .filter(([key]) => key !== "type")
36
+ .map(([key, value]) => `${key}=${String(value)}`)
37
+ .join(" ");
38
+ return `lane=${result.lane_id} change=${result.change.type}${details ? ` ${details}` : ""} at=${result.at}`;
39
+ }
40
+ export function createLaneWatchHandlers(deps) {
41
+ const { laneHostFromCtx, loadAllLanes, loadClaudeBalanceHostSnapshots, printJson, readOutpostCapacity, } = deps;
42
+ async function runWatch(ctx, laneIds, opts) {
43
+ const rawInterval = opts.intervalMs === undefined ? undefined : Number(opts.intervalMs);
44
+ const hqRoot = ctx.hqRoot();
45
+ await runLaneWatch(hqRoot, laneIds, {
46
+ intervalMs: rawInterval,
47
+ sleep: ctx.sleep,
48
+ keepAliveEnabled: (lane) => readLanesRegistryFlag(LEAD_LANE_KEEP_ALIVE_FLAG, {
49
+ company: lane.company,
50
+ env: ctx.env,
51
+ }),
52
+ readAccountStatuses: async (lanes) => {
53
+ if (lanes.length === 0)
54
+ return [];
55
+ const config = loadLanesConfig(hqRoot);
56
+ const currentHost = laneHostFromCtx(ctx);
57
+ const currentHostname = ctx.hostname().trim();
58
+ const isCurrentHost = (host) => (host.kind === currentHost.kind && host.id === currentHost.id) ||
59
+ (host.kind === "local" && host.id === currentHostname);
60
+ const remoteOutpostIds = [...new Set(lanes
61
+ .filter((lane) => lane.host.kind === "outpost" && !isCurrentHost(lane.host))
62
+ .map((lane) => lane.host.id))];
63
+ const remoteOutposts = await Promise.all(remoteOutpostIds.map(async (id) => [id, await readOutpostCapacity(ctx, id)]));
64
+ const outpostSnapshots = new Map(remoteOutposts);
65
+ const remoteLocalLanes = lanes.filter((lane) => lane.host.kind === "local" && !isCurrentHost(lane.host));
66
+ const remoteLocalHosts = remoteLocalLanes.length > 0
67
+ ? await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config)
68
+ : undefined;
69
+ const localStatusCache = new Map();
70
+ return lanes.map((lane) => {
71
+ if (config.providers[lane.provider].accounts[lane.account] === undefined) {
72
+ throw new LanesError("watch_account_unconfigured", `Lane ${lane.lane_id} records unconfigured ${lane.provider} account ${lane.account}`, { lane_id: lane.lane_id, provider: lane.provider, account: lane.account, host: lane.host });
73
+ }
74
+ let loggedIn;
75
+ if (isCurrentHost(lane.host)) {
76
+ const key = JSON.stringify([lane.provider, lane.account, currentHost.kind, currentHost.id]);
77
+ if (!localStatusCache.has(key)) {
78
+ localStatusCache.set(key, accountStatus(lane.provider, lane.account, config, {
79
+ existsSync: ctx.existsSync,
80
+ probe: ctx.probeProvider,
81
+ env: ctx.env,
82
+ hostname: currentHostname,
83
+ }).logged_in);
84
+ }
85
+ loggedIn = localStatusCache.get(key);
86
+ }
87
+ else if (lane.host.kind === "outpost") {
88
+ loggedIn = outpostSnapshots.get(lane.host.id)?.providers[lane.provider].accounts[lane.account]?.logged_in;
89
+ }
90
+ else {
91
+ const snapshot = remoteLocalHosts?.snapshots.find((candidate) => candidate.capacity.host.kind === lane.host.kind &&
92
+ candidate.capacity.host.id === lane.host.id);
93
+ if (!snapshot) {
94
+ const gap = remoteLocalHosts?.unavailableHosts.find((candidate) => candidate.host.kind === lane.host.kind && candidate.host.id === lane.host.id);
95
+ throw new LanesError("watch_account_host_unavailable", `Cannot check ${lane.provider} account ${lane.account} on ${lane.host.kind}:${lane.host.id}${gap ? `: ${gap.reason}` : ""}`, { lane_id: lane.lane_id, provider: lane.provider, account: lane.account, host: lane.host });
96
+ }
97
+ loggedIn = snapshot.capacity.providers[lane.provider].accounts[lane.account]?.logged_in;
98
+ }
99
+ if (loggedIn === undefined) {
100
+ throw new LanesError("watch_account_unconfigured", `Lane ${lane.lane_id} records unconfigured ${lane.provider} account ${lane.account} on ${lane.host.kind}:${lane.host.id}`, { lane_id: lane.lane_id, provider: lane.provider, account: lane.account, host: lane.host });
101
+ }
102
+ return {
103
+ lane_id: lane.lane_id,
104
+ provider: lane.provider,
105
+ account: lane.account,
106
+ host: lane.host,
107
+ logged_in: loggedIn,
108
+ };
109
+ });
110
+ },
111
+ writeLine: (line) => console.log(opts.json ? JSON.stringify({ event: line }) : line),
112
+ writeAccountEvent: (event) => console.log(opts.json ? JSON.stringify(event) : `account_logged_out provider=${event.provider} account=${event.account} host=${event.host.kind}:${event.host.id} lane_ids=${event.lane_ids.join(",")}`),
113
+ writeError: (line) => process.stderr.write(`${line}\n`),
114
+ });
115
+ return 0;
116
+ }
117
+ async function runMonitorCheck(ctx, opts) {
118
+ if (opts.reminder) {
119
+ console.log(monitorReminderText());
120
+ return 0;
121
+ }
122
+ const sessionId = opts.session?.trim();
123
+ if (!sessionId) {
124
+ throw new LanesError("monitor_session_required", "hq lanes monitor-check requires --session <id> unless --reminder is used");
125
+ }
126
+ const engine = resolveMonitorEngine(opts.engine, ctx.env);
127
+ const activeLanes = loadAllLanes(ctx.hqRoot()).filter((lane) => lane.senior.kind === "session" &&
128
+ lane.senior.id === sessionId &&
129
+ MONITOR_ACTIVE_LANE_STATES.has(lane.state));
130
+ if (engine !== "claude") {
131
+ if (opts.json) {
132
+ printJson({
133
+ ok: true,
134
+ action: "monitor-check",
135
+ session_id: sessionId,
136
+ engine,
137
+ active_lane_ids: activeLanes.map((lane) => lane.lane_id),
138
+ covered_lane_ids: [],
139
+ uncovered_lane_ids: [],
140
+ skipped: "engine_has_no_claude_monitor",
141
+ });
142
+ }
143
+ return 0;
144
+ }
145
+ if (activeLanes.length === 0) {
146
+ if (opts.json) {
147
+ printJson({
148
+ ok: true,
149
+ action: "monitor-check",
150
+ session_id: sessionId,
151
+ engine,
152
+ active_lane_ids: [],
153
+ covered_lane_ids: [],
154
+ uncovered_lane_ids: [],
155
+ });
156
+ }
157
+ return 0;
158
+ }
159
+ const evidence = await ctx.monitorEvidence(sessionId);
160
+ if (evidence.sessionId !== sessionId) {
161
+ throw new LanesError("monitor_evidence_session_mismatch", `Monitor evidence was for ${JSON.stringify(evidence.sessionId)}, expected ${JSON.stringify(sessionId)}`);
162
+ }
163
+ const covered = coveredLaneIds(evidence, ctx.now());
164
+ const activeIds = activeLanes.map((lane) => lane.lane_id);
165
+ const uncovered = activeIds.filter((laneId) => !covered.has(laneId));
166
+ const monitorCalls = monitorCallsForLaneIds(uncovered);
167
+ if (opts.json) {
168
+ printJson({
169
+ ok: uncovered.length === 0,
170
+ action: "monitor-check",
171
+ session_id: sessionId,
172
+ engine,
173
+ active_lane_ids: activeIds,
174
+ covered_lane_ids: activeIds.filter((laneId) => covered.has(laneId)),
175
+ uncovered_lane_ids: uncovered,
176
+ monitor_call: monitorCalls.length === 1 ? monitorCalls[0] : null,
177
+ monitor_calls: monitorCalls,
178
+ });
179
+ }
180
+ else if (uncovered.length > 0) {
181
+ for (const laneId of uncovered)
182
+ console.log(`uncovered lane: ${laneId}`);
183
+ for (const monitorCall of monitorCalls) {
184
+ console.log(`Monitor(command=${JSON.stringify(monitorCall.command)}, timeout_ms=${monitorCall.timeout_ms}, persistent=true)`);
185
+ }
186
+ }
187
+ return uncovered.length > 0 ? MONITOR_UNCOVERED_EXIT_CODE : 0;
188
+ }
189
+ async function runWait(ctx, opts) {
190
+ const timeoutSeconds = opts.timeout === undefined ? Number.NaN : Number(opts.timeout);
191
+ try {
192
+ const result = await runLaneWait(ctx.hqRoot(), opts.any ?? [], opts.for ?? [], timeoutSeconds * 1000, { now: ctx.now, sleep: ctx.sleep });
193
+ if (result) {
194
+ console.log(opts.json ? JSON.stringify(result) : laneWaitHumanLine(result));
195
+ return 0;
196
+ }
197
+ console.log(opts.json
198
+ ? JSON.stringify({ ok: false, error: "wait_timeout" })
199
+ : "wait_timeout");
200
+ return LANE_WAIT_TIMEOUT_EXIT_CODE;
201
+ }
202
+ catch (err) {
203
+ if (!isLanesError(err) || err.error !== "wait_invalid_input")
204
+ throw err;
205
+ const output = { ok: false, error: err.error, message: err.message };
206
+ if (opts.json)
207
+ console.log(JSON.stringify(output));
208
+ else
209
+ process.stderr.write(`${err.message}\n`);
210
+ return LANE_WAIT_INVALID_INPUT_EXIT_CODE;
211
+ }
212
+ }
213
+ return { runWatch, runMonitorCheck, runWait };
214
+ }
215
+ //# sourceMappingURL=lanes-watch.js.map
@@ -15,6 +15,7 @@ import { createLaneAccountsHandlers } from "./lanes-accounts.js";
15
15
  import { createLaneMessageHandlers } from "./lanes-message.js";
16
16
  import { createLaneQuestionsHandlers } from "./lanes-questions.js";
17
17
  import { createLaneResumeHandlers } from "./lanes-resume.js";
18
+ import { createLaneWatchHandlers } from "./lanes-watch.js";
18
19
  export { resolveAnswerChoice } from "./lanes-questions.js";
19
20
  import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
20
21
  import { registerLaneChecksCommand } from "./lanes-checks.js";
@@ -45,16 +46,15 @@ import { assertNoSecretText } from "../lib/lanes/text-scan.js";
45
46
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
46
47
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
47
48
  import { effectiveAccountHome } from "../lib/lanes/account-home.js";
48
- import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
49
+ import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
49
50
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
50
51
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
51
- import { keepAliveExclusion, LEAD_LANE_KEEP_ALIVE_FLAG, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
52
+ import { keepAliveExclusion, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
52
53
  import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
53
54
  import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
54
55
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
55
56
  import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
56
57
  import { LanesError } from "../lib/lanes/errors.js";
57
- import { LANE_WAIT_INVALID_INPUT_EXIT_CODE, LANE_WAIT_TIMEOUT_EXIT_CODE, runLaneWait, } from "../lib/lanes/wait.js";
58
58
  import { loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
59
59
  import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
60
60
  import { managerStatusGateEnabledForLane, resolveManagerStatusGateForLane, } from "../lib/lanes/manager-status-gate.js";
@@ -1196,180 +1196,6 @@ function assertPlacementEnabled(ctx) {
1196
1196
  function loadAllQuestions(hqRoot) {
1197
1197
  return listQuestionIds(hqRoot).map((id) => readQuestion(hqRoot, id));
1198
1198
  }
1199
- const MONITOR_ACTIVE_LANE_STATES = new Set([
1200
- "running",
1201
- "queued",
1202
- "awaiting_input",
1203
- "blocked",
1204
- ]);
1205
- function resolveMonitorEngine(requested, env) {
1206
- const value = requested?.trim().toLowerCase() ||
1207
- env.HQ_CONDUCT_ENGINE?.trim().toLowerCase() ||
1208
- "claude";
1209
- if (!MONITOR_ENGINES.includes(value)) {
1210
- throw new LanesError("monitor_invalid_engine", `Unknown monitor-check engine ${JSON.stringify(value)}. Accepted: ${MONITOR_ENGINES.join(", ")}`, { accepted: [...MONITOR_ENGINES], value });
1211
- }
1212
- return value;
1213
- }
1214
- async function runWatch(ctx, laneIds, opts) {
1215
- const rawInterval = opts.intervalMs === undefined ? undefined : Number(opts.intervalMs);
1216
- const hqRoot = ctx.hqRoot();
1217
- await runLaneWatch(hqRoot, laneIds, {
1218
- intervalMs: rawInterval,
1219
- sleep: ctx.sleep,
1220
- keepAliveEnabled: (lane) => readLanesRegistryFlag(LEAD_LANE_KEEP_ALIVE_FLAG, {
1221
- company: lane.company,
1222
- env: ctx.env,
1223
- }),
1224
- readAccountStatuses: async (lanes) => {
1225
- if (lanes.length === 0)
1226
- return [];
1227
- const config = loadLanesConfig(hqRoot);
1228
- const currentHost = laneHostFromCtx(ctx);
1229
- const currentHostname = ctx.hostname().trim();
1230
- const isCurrentHost = (host) => (host.kind === currentHost.kind && host.id === currentHost.id) ||
1231
- (host.kind === "local" && host.id === currentHostname);
1232
- const remoteOutpostIds = [...new Set(lanes
1233
- .filter((lane) => lane.host.kind === "outpost" && !isCurrentHost(lane.host))
1234
- .map((lane) => lane.host.id))];
1235
- const remoteOutposts = await Promise.all(remoteOutpostIds.map(async (id) => [id, await readOutpostCapacity(ctx, id)]));
1236
- const outpostSnapshots = new Map(remoteOutposts);
1237
- const remoteLocalLanes = lanes.filter((lane) => lane.host.kind === "local" && !isCurrentHost(lane.host));
1238
- const remoteLocalHosts = remoteLocalLanes.length > 0
1239
- ? await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config)
1240
- : undefined;
1241
- const localStatusCache = new Map();
1242
- return lanes.map((lane) => {
1243
- if (config.providers[lane.provider].accounts[lane.account] === undefined) {
1244
- throw new LanesError("watch_account_unconfigured", `Lane ${lane.lane_id} records unconfigured ${lane.provider} account ${lane.account}`, { lane_id: lane.lane_id, provider: lane.provider, account: lane.account, host: lane.host });
1245
- }
1246
- let loggedIn;
1247
- if (isCurrentHost(lane.host)) {
1248
- const key = JSON.stringify([lane.provider, lane.account, currentHost.kind, currentHost.id]);
1249
- if (!localStatusCache.has(key)) {
1250
- localStatusCache.set(key, accountStatus(lane.provider, lane.account, config, {
1251
- existsSync: ctx.existsSync,
1252
- probe: ctx.probeProvider,
1253
- env: ctx.env,
1254
- hostname: currentHostname,
1255
- }).logged_in);
1256
- }
1257
- loggedIn = localStatusCache.get(key);
1258
- }
1259
- else if (lane.host.kind === "outpost") {
1260
- loggedIn = outpostSnapshots.get(lane.host.id)?.providers[lane.provider].accounts[lane.account]?.logged_in;
1261
- }
1262
- else {
1263
- const snapshot = remoteLocalHosts?.snapshots.find((candidate) => candidate.capacity.host.kind === lane.host.kind &&
1264
- candidate.capacity.host.id === lane.host.id);
1265
- if (!snapshot) {
1266
- const gap = remoteLocalHosts?.unavailableHosts.find((candidate) => candidate.host.kind === lane.host.kind && candidate.host.id === lane.host.id);
1267
- throw new LanesError("watch_account_host_unavailable", `Cannot check ${lane.provider} account ${lane.account} on ${lane.host.kind}:${lane.host.id}${gap ? `: ${gap.reason}` : ""}`, { lane_id: lane.lane_id, provider: lane.provider, account: lane.account, host: lane.host });
1268
- }
1269
- loggedIn = snapshot.capacity.providers[lane.provider].accounts[lane.account]?.logged_in;
1270
- }
1271
- if (loggedIn === undefined) {
1272
- throw new LanesError("watch_account_unconfigured", `Lane ${lane.lane_id} records unconfigured ${lane.provider} account ${lane.account} on ${lane.host.kind}:${lane.host.id}`, { lane_id: lane.lane_id, provider: lane.provider, account: lane.account, host: lane.host });
1273
- }
1274
- return {
1275
- lane_id: lane.lane_id,
1276
- provider: lane.provider,
1277
- account: lane.account,
1278
- host: lane.host,
1279
- logged_in: loggedIn,
1280
- };
1281
- });
1282
- },
1283
- writeLine: (line) => console.log(opts.json ? JSON.stringify({ event: line }) : line),
1284
- writeAccountEvent: (event) => console.log(opts.json ? JSON.stringify(event) : `account_logged_out provider=${event.provider} account=${event.account} host=${event.host.kind}:${event.host.id} lane_ids=${event.lane_ids.join(",")}`),
1285
- writeError: (line) => process.stderr.write(`${line}\n`),
1286
- });
1287
- return 0;
1288
- }
1289
- function monitorCallsForLaneIds(laneIds) {
1290
- const calls = [];
1291
- for (let offset = 0; offset < laneIds.length; offset += MAX_WATCHED_LANES) {
1292
- const chunk = laneIds.slice(offset, offset + MAX_WATCHED_LANES);
1293
- calls.push({
1294
- command: `hq lanes watch ${chunk.join(" ")}`,
1295
- timeout_ms: MONITOR_MAX_TIMEOUT_MS,
1296
- persistent: true,
1297
- });
1298
- }
1299
- return calls;
1300
- }
1301
- async function runMonitorCheck(ctx, opts) {
1302
- if (opts.reminder) {
1303
- console.log(monitorReminderText());
1304
- return 0;
1305
- }
1306
- const sessionId = opts.session?.trim();
1307
- if (!sessionId) {
1308
- throw new LanesError("monitor_session_required", "hq lanes monitor-check requires --session <id> unless --reminder is used");
1309
- }
1310
- const engine = resolveMonitorEngine(opts.engine, ctx.env);
1311
- const activeLanes = loadAllLanes(ctx.hqRoot()).filter((lane) => lane.senior.kind === "session" &&
1312
- lane.senior.id === sessionId &&
1313
- MONITOR_ACTIVE_LANE_STATES.has(lane.state));
1314
- if (engine !== "claude") {
1315
- if (opts.json) {
1316
- printJson({
1317
- ok: true,
1318
- action: "monitor-check",
1319
- session_id: sessionId,
1320
- engine,
1321
- active_lane_ids: activeLanes.map((lane) => lane.lane_id),
1322
- covered_lane_ids: [],
1323
- uncovered_lane_ids: [],
1324
- skipped: "engine_has_no_claude_monitor",
1325
- });
1326
- }
1327
- return 0;
1328
- }
1329
- if (activeLanes.length === 0) {
1330
- if (opts.json) {
1331
- printJson({
1332
- ok: true,
1333
- action: "monitor-check",
1334
- session_id: sessionId,
1335
- engine,
1336
- active_lane_ids: [],
1337
- covered_lane_ids: [],
1338
- uncovered_lane_ids: [],
1339
- });
1340
- }
1341
- return 0;
1342
- }
1343
- const evidence = await ctx.monitorEvidence(sessionId);
1344
- if (evidence.sessionId !== sessionId) {
1345
- throw new LanesError("monitor_evidence_session_mismatch", `Monitor evidence was for ${JSON.stringify(evidence.sessionId)}, expected ${JSON.stringify(sessionId)}`);
1346
- }
1347
- const covered = coveredLaneIds(evidence, ctx.now());
1348
- const activeIds = activeLanes.map((lane) => lane.lane_id);
1349
- const uncovered = activeIds.filter((laneId) => !covered.has(laneId));
1350
- const monitorCalls = monitorCallsForLaneIds(uncovered);
1351
- if (opts.json) {
1352
- printJson({
1353
- ok: uncovered.length === 0,
1354
- action: "monitor-check",
1355
- session_id: sessionId,
1356
- engine,
1357
- active_lane_ids: activeIds,
1358
- covered_lane_ids: activeIds.filter((laneId) => covered.has(laneId)),
1359
- uncovered_lane_ids: uncovered,
1360
- monitor_call: monitorCalls.length === 1 ? monitorCalls[0] : null,
1361
- monitor_calls: monitorCalls,
1362
- });
1363
- }
1364
- else if (uncovered.length > 0) {
1365
- for (const laneId of uncovered)
1366
- console.log(`uncovered lane: ${laneId}`);
1367
- for (const monitorCall of monitorCalls) {
1368
- console.log(`Monitor(command=${JSON.stringify(monitorCall.command)}, timeout_ms=${monitorCall.timeout_ms}, persistent=true)`);
1369
- }
1370
- }
1371
- return uncovered.length > 0 ? MONITOR_UNCOVERED_EXIT_CODE : 0;
1372
- }
1373
1199
  function sessionMetaPath(hqRoot, sessionId) {
1374
1200
  return path.join(hqRoot, "workspace", "sessions", sessionId, "meta.yaml");
1375
1201
  }
@@ -1858,37 +1684,6 @@ function assertKnownOutpost(requested, known) {
1858
1684
  */
1859
1685
  export const ASK_BACKOFF_MS = [5_000, 15_000];
1860
1686
  export const ASK_ATTEMPTS = ASK_BACKOFF_MS.length + 1;
1861
- function laneWaitHumanLine(result) {
1862
- const details = Object.entries(result.change)
1863
- .filter(([key]) => key !== "type")
1864
- .map(([key, value]) => `${key}=${String(value)}`)
1865
- .join(" ");
1866
- return `lane=${result.lane_id} change=${result.change.type}${details ? ` ${details}` : ""} at=${result.at}`;
1867
- }
1868
- async function runWait(ctx, opts) {
1869
- const timeoutSeconds = opts.timeout === undefined ? Number.NaN : Number(opts.timeout);
1870
- try {
1871
- const result = await runLaneWait(ctx.hqRoot(), opts.any ?? [], opts.for ?? [], timeoutSeconds * 1000, { now: ctx.now, sleep: ctx.sleep });
1872
- if (result) {
1873
- console.log(opts.json ? JSON.stringify(result) : laneWaitHumanLine(result));
1874
- return 0;
1875
- }
1876
- console.log(opts.json
1877
- ? JSON.stringify({ ok: false, error: "wait_timeout" })
1878
- : "wait_timeout");
1879
- return LANE_WAIT_TIMEOUT_EXIT_CODE;
1880
- }
1881
- catch (err) {
1882
- if (!isLanesError(err) || err.error !== "wait_invalid_input")
1883
- throw err;
1884
- const output = { ok: false, error: err.error, message: err.message };
1885
- if (opts.json)
1886
- console.log(JSON.stringify(output));
1887
- else
1888
- process.stderr.write(`${err.message}\n`);
1889
- return LANE_WAIT_INVALID_INPUT_EXIT_CODE;
1890
- }
1891
- }
1892
1687
  function withCasRetry(fn, attempts = 5) {
1893
1688
  let last;
1894
1689
  for (let i = 0; i < attempts; i += 1) {
@@ -7050,6 +6845,14 @@ export function registerLanesCommand(parent, deps = {}) {
7050
6845
  validateResumeProfileModel,
7051
6846
  withResumeHireAdmission,
7052
6847
  });
6848
+ const watchHandlerDeps = {
6849
+ laneHostFromCtx,
6850
+ loadAllLanes,
6851
+ loadClaudeBalanceHostSnapshots,
6852
+ printJson,
6853
+ readOutpostCapacity,
6854
+ };
6855
+ const watchHandlers = createLaneWatchHandlers(watchHandlerDeps);
7053
6856
  const lanes = parent
7054
6857
  .command("lanes", { hidden: true })
7055
6858
  .description("Create, supervise, and steer worker lanes; routed decision queue");
@@ -7253,13 +7056,13 @@ export function registerLanesCommand(parent, deps = {}) {
7253
7056
  .option("--for <change-type>", "envelope | question | state", collectRepeatable, [])
7254
7057
  .option("--timeout <seconds>", "Positive timeout in seconds")
7255
7058
  .option("--json", "JSON output")
7256
- .action(wrapText(async (opts) => runWait(ctx, opts)));
7059
+ .action(wrapText(async (opts) => watchHandlers.runWait(ctx, opts)));
7257
7060
  lanes
7258
7061
  .command("watch")
7259
7062
  .description("Watch lane state and question changes until all lanes are terminal")
7260
7063
  .argument("<lane-id...>", "Lane ids to watch")
7261
7064
  .option("--interval-ms <ms>", "Polling interval, bounded to 100ms–30s", "5000")
7262
- .action(wrapText(async (laneIds, opts) => runWatch(ctx, laneIds, opts)));
7065
+ .action(wrapText(async (laneIds, opts) => watchHandlers.runWatch(ctx, laneIds, opts)));
7263
7066
  lanes
7264
7067
  .command("monitor-check")
7265
7068
  .description("Check that active Claude senior lanes have live Monitor watch tasks")
@@ -7267,7 +7070,7 @@ export function registerLanesCommand(parent, deps = {}) {
7267
7070
  .option("--engine <engine>", "claude | codex | grok; defaults to HQ_CONDUCT_ENGINE or claude")
7268
7071
  .option("--reminder", "Print the senior Monitor reminder text")
7269
7072
  .option("--json", "JSON on stdout")
7270
- .action(wrapText(async (opts) => runMonitorCheck(ctx, opts)));
7073
+ .action(wrapText(async (opts) => watchHandlers.runMonitorCheck(ctx, opts)));
7271
7074
  lanes
7272
7075
  .command("show")
7273
7076
  .description("Show one lane")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.254.0",
3
+ "version": "5.255.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {