@indigoai-us/hq-cli 5.253.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.
@@ -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,18 +46,18 @@ 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
+ import { managerStatusGateEnabledForLane, resolveManagerStatusGateForLane, } from "../lib/lanes/manager-status-gate.js";
60
61
  import { ensureSessionHome } from "../lib/lanes/session-relink.js";
61
62
  import { defaultProbe, validateProfileLaneModel, validateLaneModel, } from "../lib/lanes/models.js";
62
63
  import { parseCliModel, refuseOutpostSelfTarget, resolveLaneModel, FOREGROUND_ONLY_RULE, STANDING_AUTHORIZATION, spawnLaneWorker, laneRunDir, readSpawnManifest, stopLaneProcesses, } from "../lib/lanes/spawn.js";
@@ -1195,180 +1196,6 @@ function assertPlacementEnabled(ctx) {
1195
1196
  function loadAllQuestions(hqRoot) {
1196
1197
  return listQuestionIds(hqRoot).map((id) => readQuestion(hqRoot, id));
1197
1198
  }
1198
- const MONITOR_ACTIVE_LANE_STATES = new Set([
1199
- "running",
1200
- "queued",
1201
- "awaiting_input",
1202
- "blocked",
1203
- ]);
1204
- function resolveMonitorEngine(requested, env) {
1205
- const value = requested?.trim().toLowerCase() ||
1206
- env.HQ_CONDUCT_ENGINE?.trim().toLowerCase() ||
1207
- "claude";
1208
- if (!MONITOR_ENGINES.includes(value)) {
1209
- throw new LanesError("monitor_invalid_engine", `Unknown monitor-check engine ${JSON.stringify(value)}. Accepted: ${MONITOR_ENGINES.join(", ")}`, { accepted: [...MONITOR_ENGINES], value });
1210
- }
1211
- return value;
1212
- }
1213
- async function runWatch(ctx, laneIds, opts) {
1214
- const rawInterval = opts.intervalMs === undefined ? undefined : Number(opts.intervalMs);
1215
- const hqRoot = ctx.hqRoot();
1216
- await runLaneWatch(hqRoot, laneIds, {
1217
- intervalMs: rawInterval,
1218
- sleep: ctx.sleep,
1219
- keepAliveEnabled: (lane) => readLanesRegistryFlag(LEAD_LANE_KEEP_ALIVE_FLAG, {
1220
- company: lane.company,
1221
- env: ctx.env,
1222
- }),
1223
- readAccountStatuses: async (lanes) => {
1224
- if (lanes.length === 0)
1225
- return [];
1226
- const config = loadLanesConfig(hqRoot);
1227
- const currentHost = laneHostFromCtx(ctx);
1228
- const currentHostname = ctx.hostname().trim();
1229
- const isCurrentHost = (host) => (host.kind === currentHost.kind && host.id === currentHost.id) ||
1230
- (host.kind === "local" && host.id === currentHostname);
1231
- const remoteOutpostIds = [...new Set(lanes
1232
- .filter((lane) => lane.host.kind === "outpost" && !isCurrentHost(lane.host))
1233
- .map((lane) => lane.host.id))];
1234
- const remoteOutposts = await Promise.all(remoteOutpostIds.map(async (id) => [id, await readOutpostCapacity(ctx, id)]));
1235
- const outpostSnapshots = new Map(remoteOutposts);
1236
- const remoteLocalLanes = lanes.filter((lane) => lane.host.kind === "local" && !isCurrentHost(lane.host));
1237
- const remoteLocalHosts = remoteLocalLanes.length > 0
1238
- ? await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config)
1239
- : undefined;
1240
- const localStatusCache = new Map();
1241
- return lanes.map((lane) => {
1242
- if (config.providers[lane.provider].accounts[lane.account] === undefined) {
1243
- 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 });
1244
- }
1245
- let loggedIn;
1246
- if (isCurrentHost(lane.host)) {
1247
- const key = JSON.stringify([lane.provider, lane.account, currentHost.kind, currentHost.id]);
1248
- if (!localStatusCache.has(key)) {
1249
- localStatusCache.set(key, accountStatus(lane.provider, lane.account, config, {
1250
- existsSync: ctx.existsSync,
1251
- probe: ctx.probeProvider,
1252
- env: ctx.env,
1253
- hostname: currentHostname,
1254
- }).logged_in);
1255
- }
1256
- loggedIn = localStatusCache.get(key);
1257
- }
1258
- else if (lane.host.kind === "outpost") {
1259
- loggedIn = outpostSnapshots.get(lane.host.id)?.providers[lane.provider].accounts[lane.account]?.logged_in;
1260
- }
1261
- else {
1262
- const snapshot = remoteLocalHosts?.snapshots.find((candidate) => candidate.capacity.host.kind === lane.host.kind &&
1263
- candidate.capacity.host.id === lane.host.id);
1264
- if (!snapshot) {
1265
- const gap = remoteLocalHosts?.unavailableHosts.find((candidate) => candidate.host.kind === lane.host.kind && candidate.host.id === lane.host.id);
1266
- 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 });
1267
- }
1268
- loggedIn = snapshot.capacity.providers[lane.provider].accounts[lane.account]?.logged_in;
1269
- }
1270
- if (loggedIn === undefined) {
1271
- 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 });
1272
- }
1273
- return {
1274
- lane_id: lane.lane_id,
1275
- provider: lane.provider,
1276
- account: lane.account,
1277
- host: lane.host,
1278
- logged_in: loggedIn,
1279
- };
1280
- });
1281
- },
1282
- writeLine: (line) => console.log(opts.json ? JSON.stringify({ event: line }) : line),
1283
- 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(",")}`),
1284
- writeError: (line) => process.stderr.write(`${line}\n`),
1285
- });
1286
- return 0;
1287
- }
1288
- function monitorCallsForLaneIds(laneIds) {
1289
- const calls = [];
1290
- for (let offset = 0; offset < laneIds.length; offset += MAX_WATCHED_LANES) {
1291
- const chunk = laneIds.slice(offset, offset + MAX_WATCHED_LANES);
1292
- calls.push({
1293
- command: `hq lanes watch ${chunk.join(" ")}`,
1294
- timeout_ms: MONITOR_MAX_TIMEOUT_MS,
1295
- persistent: true,
1296
- });
1297
- }
1298
- return calls;
1299
- }
1300
- async function runMonitorCheck(ctx, opts) {
1301
- if (opts.reminder) {
1302
- console.log(monitorReminderText());
1303
- return 0;
1304
- }
1305
- const sessionId = opts.session?.trim();
1306
- if (!sessionId) {
1307
- throw new LanesError("monitor_session_required", "hq lanes monitor-check requires --session <id> unless --reminder is used");
1308
- }
1309
- const engine = resolveMonitorEngine(opts.engine, ctx.env);
1310
- const activeLanes = loadAllLanes(ctx.hqRoot()).filter((lane) => lane.senior.kind === "session" &&
1311
- lane.senior.id === sessionId &&
1312
- MONITOR_ACTIVE_LANE_STATES.has(lane.state));
1313
- if (engine !== "claude") {
1314
- if (opts.json) {
1315
- printJson({
1316
- ok: true,
1317
- action: "monitor-check",
1318
- session_id: sessionId,
1319
- engine,
1320
- active_lane_ids: activeLanes.map((lane) => lane.lane_id),
1321
- covered_lane_ids: [],
1322
- uncovered_lane_ids: [],
1323
- skipped: "engine_has_no_claude_monitor",
1324
- });
1325
- }
1326
- return 0;
1327
- }
1328
- if (activeLanes.length === 0) {
1329
- if (opts.json) {
1330
- printJson({
1331
- ok: true,
1332
- action: "monitor-check",
1333
- session_id: sessionId,
1334
- engine,
1335
- active_lane_ids: [],
1336
- covered_lane_ids: [],
1337
- uncovered_lane_ids: [],
1338
- });
1339
- }
1340
- return 0;
1341
- }
1342
- const evidence = await ctx.monitorEvidence(sessionId);
1343
- if (evidence.sessionId !== sessionId) {
1344
- throw new LanesError("monitor_evidence_session_mismatch", `Monitor evidence was for ${JSON.stringify(evidence.sessionId)}, expected ${JSON.stringify(sessionId)}`);
1345
- }
1346
- const covered = coveredLaneIds(evidence, ctx.now());
1347
- const activeIds = activeLanes.map((lane) => lane.lane_id);
1348
- const uncovered = activeIds.filter((laneId) => !covered.has(laneId));
1349
- const monitorCalls = monitorCallsForLaneIds(uncovered);
1350
- if (opts.json) {
1351
- printJson({
1352
- ok: uncovered.length === 0,
1353
- action: "monitor-check",
1354
- session_id: sessionId,
1355
- engine,
1356
- active_lane_ids: activeIds,
1357
- covered_lane_ids: activeIds.filter((laneId) => covered.has(laneId)),
1358
- uncovered_lane_ids: uncovered,
1359
- monitor_call: monitorCalls.length === 1 ? monitorCalls[0] : null,
1360
- monitor_calls: monitorCalls,
1361
- });
1362
- }
1363
- else if (uncovered.length > 0) {
1364
- for (const laneId of uncovered)
1365
- console.log(`uncovered lane: ${laneId}`);
1366
- for (const monitorCall of monitorCalls) {
1367
- console.log(`Monitor(command=${JSON.stringify(monitorCall.command)}, timeout_ms=${monitorCall.timeout_ms}, persistent=true)`);
1368
- }
1369
- }
1370
- return uncovered.length > 0 ? MONITOR_UNCOVERED_EXIT_CODE : 0;
1371
- }
1372
1199
  function sessionMetaPath(hqRoot, sessionId) {
1373
1200
  return path.join(hqRoot, "workspace", "sessions", sessionId, "meta.yaml");
1374
1201
  }
@@ -1857,37 +1684,6 @@ function assertKnownOutpost(requested, known) {
1857
1684
  */
1858
1685
  export const ASK_BACKOFF_MS = [5_000, 15_000];
1859
1686
  export const ASK_ATTEMPTS = ASK_BACKOFF_MS.length + 1;
1860
- function laneWaitHumanLine(result) {
1861
- const details = Object.entries(result.change)
1862
- .filter(([key]) => key !== "type")
1863
- .map(([key, value]) => `${key}=${String(value)}`)
1864
- .join(" ");
1865
- return `lane=${result.lane_id} change=${result.change.type}${details ? ` ${details}` : ""} at=${result.at}`;
1866
- }
1867
- async function runWait(ctx, opts) {
1868
- const timeoutSeconds = opts.timeout === undefined ? Number.NaN : Number(opts.timeout);
1869
- try {
1870
- const result = await runLaneWait(ctx.hqRoot(), opts.any ?? [], opts.for ?? [], timeoutSeconds * 1000, { now: ctx.now, sleep: ctx.sleep });
1871
- if (result) {
1872
- console.log(opts.json ? JSON.stringify(result) : laneWaitHumanLine(result));
1873
- return 0;
1874
- }
1875
- console.log(opts.json
1876
- ? JSON.stringify({ ok: false, error: "wait_timeout" })
1877
- : "wait_timeout");
1878
- return LANE_WAIT_TIMEOUT_EXIT_CODE;
1879
- }
1880
- catch (err) {
1881
- if (!isLanesError(err) || err.error !== "wait_invalid_input")
1882
- throw err;
1883
- const output = { ok: false, error: err.error, message: err.message };
1884
- if (opts.json)
1885
- console.log(JSON.stringify(output));
1886
- else
1887
- process.stderr.write(`${err.message}\n`);
1888
- return LANE_WAIT_INVALID_INPUT_EXIT_CODE;
1889
- }
1890
- }
1891
1687
  function withCasRetry(fn, attempts = 5) {
1892
1688
  let last;
1893
1689
  for (let i = 0; i < attempts; i += 1) {
@@ -3072,6 +2868,7 @@ async function runCreate(ctx, opts) {
3072
2868
  if (admission.action === "start") {
3073
2869
  started = spawnLaneWorker(hqRoot, lane, {
3074
2870
  env: ctx.env,
2871
+ managerStatusGateEnabled: await managerStatusGateEnabledForLane(hqRoot, lane, { env: ctx.env }),
3075
2872
  modelDeps: {
3076
2873
  readFileSync: ctx.readFileSync,
3077
2874
  probe: ctx.probeProvider,
@@ -4043,6 +3840,7 @@ async function runPickup(ctx, opts) {
4043
3840
  try {
4044
3841
  return spawnLaneWorker(hqRoot, current, {
4045
3842
  env: ctx.env,
3843
+ managerStatusGateEnabled: await managerStatusGateEnabledForLane(hqRoot, current, { env: ctx.env }),
4046
3844
  modelDeps: {
4047
3845
  readFileSync: ctx.readFileSync,
4048
3846
  probe: ctx.probeProvider,
@@ -4724,6 +4522,8 @@ async function runShow(ctx, laneId) {
4724
4522
  : storedLane;
4725
4523
  const askDeliveryFailed = readJsonFile(askDeliveryFailedPath(hqRoot, laneId));
4726
4524
  const keepAliveState = resolveLaneKeepAlive(hqRoot, storedLane);
4525
+ const managerStatusGate = await resolveManagerStatusGateForLane(hqRoot, storedLane, { env: ctx.env });
4526
+ const managerStatusTarget = managerStatusGate.target;
4727
4527
  printJson({
4728
4528
  ok: true,
4729
4529
  action: "show",
@@ -4762,6 +4562,22 @@ async function runShow(ctx, laneId) {
4762
4562
  next_eligible_at: lane.keep_alive_next_eligible_at ?? null,
4763
4563
  exhausted_at: lane.keep_alive_exhausted_at ?? null,
4764
4564
  },
4565
+ manager_status_gate: managerStatusGate.error !== undefined
4566
+ ? {
4567
+ status_path: null,
4568
+ skipped: null,
4569
+ consecutive_misses: storedLane.status_gate?.consecutive_misses ?? 0,
4570
+ error: managerStatusGate.error,
4571
+ }
4572
+ : managerStatusTarget?.manager
4573
+ ? {
4574
+ status_path: managerStatusTarget.statusPath ?? null,
4575
+ skipped: managerStatusTarget.skipped === "no_juniors_row"
4576
+ ? "no_declared_status_file"
4577
+ : null,
4578
+ consecutive_misses: storedLane.status_gate?.consecutive_misses ?? 0,
4579
+ }
4580
+ : null,
4765
4581
  last_message_at: lane.last_message_at ?? null,
4766
4582
  last_compaction: lane.last_compaction
4767
4583
  ? { ...lane.last_compaction, after: compactionAfterUsage(storedLane) }
@@ -5221,6 +5037,7 @@ async function runAdmit(ctx, laneId, opts) {
5221
5037
  }
5222
5038
  const started = spawnLaneWorker(hqRoot, lane, {
5223
5039
  env: ctx.env,
5040
+ managerStatusGateEnabled: await managerStatusGateEnabledForLane(hqRoot, lane, { env: ctx.env }),
5224
5041
  modelDeps: {
5225
5042
  readFileSync: ctx.readFileSync,
5226
5043
  probe: ctx.probeProvider,
@@ -7028,6 +6845,14 @@ export function registerLanesCommand(parent, deps = {}) {
7028
6845
  validateResumeProfileModel,
7029
6846
  withResumeHireAdmission,
7030
6847
  });
6848
+ const watchHandlerDeps = {
6849
+ laneHostFromCtx,
6850
+ loadAllLanes,
6851
+ loadClaudeBalanceHostSnapshots,
6852
+ printJson,
6853
+ readOutpostCapacity,
6854
+ };
6855
+ const watchHandlers = createLaneWatchHandlers(watchHandlerDeps);
7031
6856
  const lanes = parent
7032
6857
  .command("lanes", { hidden: true })
7033
6858
  .description("Create, supervise, and steer worker lanes; routed decision queue");
@@ -7231,13 +7056,13 @@ export function registerLanesCommand(parent, deps = {}) {
7231
7056
  .option("--for <change-type>", "envelope | question | state", collectRepeatable, [])
7232
7057
  .option("--timeout <seconds>", "Positive timeout in seconds")
7233
7058
  .option("--json", "JSON output")
7234
- .action(wrapText(async (opts) => runWait(ctx, opts)));
7059
+ .action(wrapText(async (opts) => watchHandlers.runWait(ctx, opts)));
7235
7060
  lanes
7236
7061
  .command("watch")
7237
7062
  .description("Watch lane state and question changes until all lanes are terminal")
7238
7063
  .argument("<lane-id...>", "Lane ids to watch")
7239
7064
  .option("--interval-ms <ms>", "Polling interval, bounded to 100ms–30s", "5000")
7240
- .action(wrapText(async (laneIds, opts) => runWatch(ctx, laneIds, opts)));
7065
+ .action(wrapText(async (laneIds, opts) => watchHandlers.runWatch(ctx, laneIds, opts)));
7241
7066
  lanes
7242
7067
  .command("monitor-check")
7243
7068
  .description("Check that active Claude senior lanes have live Monitor watch tasks")
@@ -7245,7 +7070,7 @@ export function registerLanesCommand(parent, deps = {}) {
7245
7070
  .option("--engine <engine>", "claude | codex | grok; defaults to HQ_CONDUCT_ENGINE or claude")
7246
7071
  .option("--reminder", "Print the senior Monitor reminder text")
7247
7072
  .option("--json", "JSON on stdout")
7248
- .action(wrapText(async (opts) => runMonitorCheck(ctx, opts)));
7073
+ .action(wrapText(async (opts) => watchHandlers.runMonitorCheck(ctx, opts)));
7249
7074
  lanes
7250
7075
  .command("show")
7251
7076
  .description("Show one lane")
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Remote SQL via secrets-exec style injection (US-010).
2
+ * Remote SQL helper with injected credentials and executor (US-010).
3
3
  *
4
4
  * Connection string never appears in argv, logs, or stdout.
5
- * Local remains default when --remote is absent (handled by command layer).
5
+ * The CLI command does not currently wire this helper to a remote executor.
6
6
  */
7
7
  export interface RemoteSqlOptions {
8
8
  company: string;
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Remote SQL via secrets-exec style injection (US-010).
2
+ * Remote SQL helper with injected credentials and executor (US-010).
3
3
  *
4
4
  * Connection string never appears in argv, logs, or stdout.
5
- * Local remains default when --remote is absent (handled by command layer).
5
+ * The CLI command does not currently wire this helper to a remote executor.
6
6
  */
7
7
  function assertNoLeak(label, value) {
8
8
  const s = typeof value === "string" ? value : JSON.stringify(value);
@@ -23,7 +23,7 @@ export async function runRemoteSql(opts) {
23
23
  }
24
24
  catch (e) {
25
25
  const msg = e instanceof Error ? e.message : "no remote binding";
26
- throw new Error(`remote SQL unavailable: ${msg}. Run \`hq db provision --company ${opts.company}\` first.`);
26
+ throw new Error(`remote SQL unavailable: ${msg}`);
27
27
  }
28
28
  assertNoLeak("connection config", config);
29
29
  if (!opts.execute) {
@@ -0,0 +1,3 @@
1
+ /** The shared HQ/company path validation used by lane tracker mappings. */
2
+ export declare function checkCompanyPath(root: string, company: string, file: string, detailsPath: string): string;
3
+ //# sourceMappingURL=checked-company-path.d.ts.map
@@ -0,0 +1,39 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { LanesError } from "./errors.js";
4
+ function fail(error, message, details) {
5
+ throw new LanesError(error, message, details);
6
+ }
7
+ function isWithin(root, candidate) {
8
+ const relative = path.relative(root, candidate);
9
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
10
+ }
11
+ /** The shared HQ/company path validation used by lane tracker mappings. */
12
+ export function checkCompanyPath(root, company, file, detailsPath) {
13
+ if (path.isAbsolute(file))
14
+ return fail("tracker_checks_mapping_invalid", "Mapped path must be relative to the HQ root", { path: detailsPath });
15
+ const absolute = path.resolve(root, file);
16
+ const rootReal = fs.realpathSync(root);
17
+ if (!isWithin(rootReal, absolute))
18
+ return fail("tracker_checks_mapping_invalid", "Mapped path escapes the HQ root", { path: detailsPath });
19
+ const relative = path.relative(rootReal, absolute).split(path.sep);
20
+ if (relative[0] === "companies" && relative[1] !== company) {
21
+ return fail("tracker_checks_mapping_invalid", "Mapped path crosses the active company boundary", { path: detailsPath });
22
+ }
23
+ try {
24
+ const real = fs.realpathSync(absolute);
25
+ if (!isWithin(rootReal, real))
26
+ return fail("tracker_checks_mapping_invalid", "Mapped path resolves outside the HQ root", { path: detailsPath });
27
+ const realRelative = path.relative(rootReal, real).split(path.sep);
28
+ if (realRelative[0] === "companies" && realRelative[1] !== company) {
29
+ return fail("tracker_checks_mapping_invalid", "Mapped path resolves into another company", { path: detailsPath });
30
+ }
31
+ }
32
+ catch (err) {
33
+ if (err.code !== "ENOENT") {
34
+ return fail("tracker_checks_mapping_unreadable", `Mapped path could not be checked: ${err instanceof Error ? err.message : String(err)}`, { path: absolute });
35
+ }
36
+ }
37
+ return absolute;
38
+ }
39
+ //# sourceMappingURL=checked-company-path.js.map
@@ -1,4 +1,4 @@
1
- import type { LaneRecord, QuestionRecord } from "./types.js";
1
+ import type { LaneRecord, ManagerStatusGateHistoryEntry, QuestionRecord } from "./types.js";
2
2
  export declare const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
3
3
  export declare const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
4
4
  /** Missing or unreadable registry values retain the legacy supersession behavior. */
@@ -7,6 +7,8 @@ export declare const LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED = false;
7
7
  export declare const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
8
8
  export declare const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS: number;
9
9
  export declare const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS: number;
10
+ /** Persist a manager status miss and queue its one bounded follow-up turn. */
11
+ export declare function queueManagerStatusGateResume(hqRoot: string, laneId: string, entry: Omit<ManagerStatusGateHistoryEntry, "event" | "resume_queued" | "resume_skipped_reason">, resumeText: string): LaneRecord;
10
12
  export declare const LEAD_LANE_KEEP_ALIVE_RESUME_TEXT = "Automatic keep-alive resume: your last turn ended with decision done and story_complete false. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
11
13
  export declare const LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT = "Automatic keep-alive resume: your previous turn was interrupted by a provider process exit. Check your last state before continuing. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
12
14
  export type KeepAliveBlockedReason = NonNullable<LaneRecord["keep_alive_blocked_reason"]>;
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { LanesError } from "./errors.js";
4
4
  import { dropboxInboxHasMessages } from "./dropbox.js";
5
- import { listQuestionIds, readQuestion } from "./store.js";
5
+ import { appendCappedHistory, listQuestionIds, readQuestion, retryUpdateLane } from "./store.js";
6
6
  export const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
7
7
  export const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
8
8
  /** Missing or unreadable registry values retain the legacy supersession behavior. */
@@ -11,6 +11,61 @@ export const LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED = false;
11
11
  export const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
12
12
  export const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS = 5 * 60 * 1000;
13
13
  export const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS = 60 * 60 * 1000;
14
+ /** Persist a manager status miss and queue its one bounded follow-up turn. */
15
+ export function queueManagerStatusGateResume(hqRoot, laneId, entry, resumeText) {
16
+ return retryUpdateLane(hqRoot, laneId, "watcher", (current) => {
17
+ const gate = current.status_gate;
18
+ if (gate === undefined || gate.turn_id !== entry.turn_id)
19
+ return current;
20
+ const history = current.status_gate_history ?? [];
21
+ if (history.some((prior) => prior.event === "status_missed" && prior.turn_id === entry.turn_id)) {
22
+ return current;
23
+ }
24
+ const consecutiveMisses = gate.consecutive_misses + 1;
25
+ let resumeSkippedReason;
26
+ if (current.state === "blocked")
27
+ resumeSkippedReason = "blocked";
28
+ else if (current.state === "awaiting_input")
29
+ resumeSkippedReason = "open_question";
30
+ else if (consecutiveMisses > 1)
31
+ resumeSkippedReason = "miss_limit";
32
+ else if (!current.provider_session_id)
33
+ resumeSkippedReason = "no_provider_session";
34
+ const resumeQueued = resumeSkippedReason === undefined &&
35
+ (current.state === "done" || current.state === "interrupted" || current.state === "failed");
36
+ const missed = {
37
+ event: "status_missed",
38
+ ...entry,
39
+ ...(resumeQueued ? { resume_queued: true } : {}),
40
+ ...(resumeSkippedReason !== undefined ? { resume_skipped_reason: resumeSkippedReason } : {}),
41
+ };
42
+ const next = {
43
+ ...current,
44
+ status_gate: { ...gate, consecutive_misses: consecutiveMisses },
45
+ status_gate_history: appendCappedHistory(history, missed),
46
+ };
47
+ if (!resumeQueued || !current.provider_session_id)
48
+ return next;
49
+ const resumedFrom = current.state;
50
+ return {
51
+ ...next,
52
+ state: "queued",
53
+ queued_reason: undefined,
54
+ queued_at: entry.at,
55
+ admission_reserved_at: undefined,
56
+ terminal_reason: undefined,
57
+ terminal_details: undefined,
58
+ pending_resume: {
59
+ session_id: current.provider_session_id,
60
+ trigger: "resume",
61
+ resumed_from: resumedFrom,
62
+ text: resumeText,
63
+ compact: true,
64
+ at: entry.at,
65
+ },
66
+ };
67
+ });
68
+ }
14
69
  export const LEAD_LANE_KEEP_ALIVE_RESUME_TEXT = "Automatic keep-alive resume: your last turn ended with decision done and story_complete false. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
15
70
  export const LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT = "Automatic keep-alive resume: your previous turn was interrupted by a provider process exit. Check your last state before continuing. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
16
71
  /**
@@ -0,0 +1,34 @@
1
+ import { readLanesRegistryFlag } from "./registry-flag.js";
2
+ import type { LaneRecord, ManagerStatusGate } from "./types.js";
3
+ export declare const MANAGER_STATUS_GATE_FLAG = "lanes.manager-status-gate";
4
+ export interface ManagerStatusTarget {
5
+ manager: boolean;
6
+ identified_by?: "story_id" | "worker_profile" | "juniors";
7
+ statusPath?: string;
8
+ skipped?: "no_juniors_row";
9
+ }
10
+ export interface StatusSnapshot {
11
+ mtime: number | null;
12
+ updatedAt: string | null;
13
+ }
14
+ export interface ManagerStatusGateResolution {
15
+ enabled: boolean;
16
+ target?: ManagerStatusTarget;
17
+ error?: string;
18
+ }
19
+ /** Resolve manager identity and the single declared status file, if present. */
20
+ export declare function resolveManagerStatusTarget(root: string, lane: LaneRecord): ManagerStatusTarget;
21
+ export declare function resolveManagerStatusGateForLane(root: string, lane: LaneRecord, options?: {
22
+ env?: NodeJS.ProcessEnv;
23
+ readFlag?: typeof readLanesRegistryFlag;
24
+ onError?: (error: unknown) => void;
25
+ }): Promise<ManagerStatusGateResolution>;
26
+ export declare function managerStatusGateEnabledForLane(root: string, lane: LaneRecord, options?: {
27
+ env?: NodeJS.ProcessEnv;
28
+ readFlag?: typeof readLanesRegistryFlag;
29
+ onError?: (error: unknown) => void;
30
+ }): Promise<boolean>;
31
+ export declare function readStatusSnapshot(filePath: string): StatusSnapshot;
32
+ export declare function managerStatusGateHeartbeat(root: string, lane: LaneRecord, turnId: string, turnStartedAt: string): ManagerStatusGate | undefined;
33
+ export declare function managerStatusWasWritten(gate: ManagerStatusGate, snapshot: StatusSnapshot): boolean;
34
+ //# sourceMappingURL=manager-status-gate.d.ts.map