@indigoai-us/hq-cli 5.242.0 → 5.244.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.
@@ -11,6 +11,9 @@ import * as fs from "node:fs";
11
11
  import * as os from "node:os";
12
12
  import * as path from "node:path";
13
13
  import { Option } from "commander";
14
+ import { createLaneAccountsHandlers } from "./lanes-accounts.js";
15
+ import { createLaneQuestionsHandlers } from "./lanes-questions.js";
16
+ export { resolveAnswerChoice } from "./lanes-questions.js";
14
17
  import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
15
18
  import { registerLaneChecksCommand } from "./lanes-checks.js";
16
19
  import { registerLaneReportCommands } from "./lanes-report.js";
@@ -26,10 +29,10 @@ import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } fr
26
29
  import { createOutpostRegistryWorkerApprovalContext, parseOutpostRegistryWorkerApprovalContext, } from "../lib/lanes/outpost-registry-worker-approval.js";
27
30
  import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
28
31
  export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
29
- import { buildOutpostCapacityCommand, buildOutpostBalanceMoveCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostQuestionAnswerCommand, buildOutpostQuestionListCommand, buildOutpostQuestionShowCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
32
+ import { buildOutpostCapacityCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostQuestionListCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
30
33
  import { outpostHireSummaryCacheKey, readOutpostHireSummaryCache, writeOutpostHireSummaryCache, } from "../lib/lanes/outpost-hire-cache.js";
31
34
  import { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "../lib/lanes/host-capacity.js";
32
- import { BALANCE_MOVABLE_STATES, accountLoadsForReport, planClaudeBalanceMoves, selectClaudeAccount, } from "../lib/lanes/claude-balance.js";
35
+ import { BALANCE_MOVABLE_STATES, selectClaudeAccount, } from "../lib/lanes/claude-balance.js";
33
36
  import { parseUserYaml } from "../utils/user-yaml-error.js";
34
37
  import { backfillOutpostIdentityMarker as backfillOutpostIdentityMarkerImpl, createOutpostIdentityMarkerIfMissing, validateOutpostIdentityId, } from "../lib/lanes/outpost-identity.js";
35
38
  import { vaultApiFetch } from "../utils/vault-api.js";
@@ -40,21 +43,20 @@ import { assertNoSecretText } from "../lib/lanes/text-scan.js";
40
43
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
41
44
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
42
45
  import { effectiveAccountHome } from "../lib/lanes/account-home.js";
43
- import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, 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, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
46
+ 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, appendDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
44
47
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
45
48
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
46
49
  import { keepAliveExclusion, LEAD_LANE_KEEP_ALIVE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
47
50
  import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
48
51
  import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
49
52
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
50
- import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
51
53
  import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
52
54
  import { LanesError } from "../lib/lanes/errors.js";
53
55
  import { LANE_WAIT_INVALID_INPUT_EXIT_CODE, LANE_WAIT_TIMEOUT_EXIT_CODE, runLaneWait, } from "../lib/lanes/wait.js";
54
56
  import { dispatchLaneDelivery } from "../lib/lanes/delivery-core.js";
55
57
  import { dropboxInboxHasMessages, drainDropbox, formatDropboxMessages, formatMonitorEvent, requeueDropboxMessages, } from "../lib/lanes/dropbox.js";
56
58
  import { isPidAlive, runWatcherLoop, WATCHER_OUT_MAX_BYTES, watcherOutLogPath, writeWatcherDecision, } from "../lib/lanes/watcher.js";
57
- import { accountQuotaView, loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
59
+ import { loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
58
60
  import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
59
61
  import { ensureSessionHome } from "../lib/lanes/session-relink.js";
60
62
  import { defaultProbe, validateProfileLaneModel, validateLaneModel, } from "../lib/lanes/models.js";
@@ -64,11 +66,7 @@ export { spawnLaneWorker };
64
66
  export { questionBodyAbsPath };
65
67
  export { MAX_LANE_DEPTH };
66
68
  export const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
67
- const VENDOR_LOGIN = {
68
- claude: { command: "claude", args: ["auth", "login"] },
69
- codex: { command: "codex", args: ["login"] },
70
- grok: { command: "grok", args: ["login"] },
71
- };
69
+ /** The lane → mesh seam. Defined in lib/lanes/mesh-events.ts; the watcher shares it. */
72
70
  export { emitLaneDecisionEvents };
73
71
  export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
74
72
  function defaultLanePlacementEnabled() {
@@ -77,13 +75,6 @@ function defaultLanePlacementEnabled() {
77
75
  function defaultLaneTrackerEnabled(company) {
78
76
  return readLanesRegistryFlag(LANES_TRACKER_FLAG, { company });
79
77
  }
80
- async function resolveSelectedAccountHomeCompany(ctx, selector) {
81
- const company = await ctx.resolveActiveCompany(selector);
82
- if (!company) {
83
- throw new CompanySelectionError(`No unique active company membership for ${selector}. Pass an active company UID with --company.`);
84
- }
85
- return company;
86
- }
87
78
  function withoutProviderHome(value) {
88
79
  if (Array.isArray(value))
89
80
  return value.map(withoutProviderHome);
@@ -354,21 +345,6 @@ function parseHireGrant(raw) {
354
345
  function seniorKey(ref) {
355
346
  return ref.kind === "user" ? "user" : `${ref.kind}:${ref.id ?? ""}`;
356
347
  }
357
- export function resolveAnswerChoice(choice, options, freeform) {
358
- const trimmed = choice.trim();
359
- if (/^[0-9]+$/.test(trimmed)) {
360
- const index = Number(trimmed);
361
- if (Number.isInteger(index) && index >= 1 && index <= options.length) {
362
- return options[index - 1];
363
- }
364
- }
365
- if (options.includes(trimmed) || options.includes(choice)) {
366
- return options.includes(trimmed) ? trimmed : choice;
367
- }
368
- if (freeform)
369
- return choice;
370
- throw new LanesError("off_menu_choice", `Choice ${JSON.stringify(choice)} is not on the menu. Pass --freeform to allow it.`, { choice, options });
371
- }
372
348
  function parsePriority(raw) {
373
349
  if (raw === undefined || raw === null || raw === "")
374
350
  return 1;
@@ -471,17 +447,6 @@ function readQuestionBody(hqRoot, question) {
471
447
  }
472
448
  return stored.body;
473
449
  }
474
- function requireQuestionBody(hqRoot, question) {
475
- const body = readQuestionBody(hqRoot, question);
476
- if (!body) {
477
- throw new LanesError("question_body_unavailable", `Question body is not available on this host for ${question.question_id}`, {
478
- question_id: question.question_id,
479
- store: question.body.store,
480
- ref: question.body.ref,
481
- });
482
- }
483
- return body;
484
- }
485
450
  function loadAllLanes(hqRoot) {
486
451
  return listLaneIds(hqRoot).map((id) => readLane(hqRoot, id));
487
452
  }
@@ -1664,18 +1629,6 @@ function assertQuestionCompanyMatchesLane(question, lane, code, hqRoot) {
1664
1629
  throw new LanesError(code, `Question ${question.question_id} does not match lane ${lane.lane_id}'s company`, { question_id: question.question_id, lane_id: lane.lane_id });
1665
1630
  }
1666
1631
  }
1667
- function assertQuestionCompanyAccess(question, lane, requestedCompany, hqRoot) {
1668
- assertQuestionCompanyMatchesLane(question, lane, "question_company_mismatch", hqRoot);
1669
- const questionCompany = questionCompanyForLane(question, lane, hqRoot);
1670
- if (questionCompany !== null && !sameQuestionCompany(questionCompany, requestedCompany)) {
1671
- throw new LanesError("question_company_mismatch", `Question ${question.question_id} does not belong to the requested or active company`, { question_id: question.question_id, lane_id: lane.lane_id });
1672
- }
1673
- }
1674
- function assertQuestionCompanyResolvedForAnswer(question, lane, hqRoot, resolvedCompany) {
1675
- if (questionCompanyForLane(question, lane, hqRoot) !== null || resolvedCompany)
1676
- return;
1677
- throw new LanesError("question_company_unavailable", `Cannot answer legacy question ${question.question_id}: its company cannot be resolved from the question or lane; re-run with --company <slug>`, { question_id: question.question_id, lane_id: lane.lane_id });
1678
- }
1679
1632
  function questionCompanyRefMatches(recordCompany, filter) {
1680
1633
  return recordCompany !== null &&
1681
1634
  recordCompany !== undefined &&
@@ -1734,25 +1687,6 @@ function resolveAudience(hqRoot, question, ctx) {
1734
1687
  }
1735
1688
  return current;
1736
1689
  }
1737
- function audienceMatches(audience, filter) {
1738
- if (filter.kind === "me") {
1739
- return audience.kind === "session" && audience.id === filter.id;
1740
- }
1741
- if (filter.kind === "user")
1742
- return audience.kind === "user";
1743
- return audience.kind === filter.kind && audience.id === filter.id;
1744
- }
1745
- function parseForFilter(raw, ctx) {
1746
- const trimmed = raw.trim();
1747
- if (trimmed === "me") {
1748
- const sessionId = sessionIdFromEnv(ctx.env);
1749
- if (!sessionId) {
1750
- throw new LanesError("missing_session_id", "--for me requires a current session id (HQ_SESSION_ID)", {});
1751
- }
1752
- return { kind: "me", id: sessionId };
1753
- }
1754
- return parseSeniorRef(trimmed);
1755
- }
1756
1690
  function timeoutPath(hqRoot, laneId) {
1757
1691
  return path.join(runDir(hqRoot, laneId), "timeout.json");
1758
1692
  }
@@ -1916,16 +1850,6 @@ function assertKnownOutpost(requested, known) {
1916
1850
  throw new LanesError("unknown_outpost", `Unknown Outpost ${JSON.stringify(requested)}. Accepted: ${known.join(", ") || "(none)"}`, { accepted: known, value: requested });
1917
1851
  }
1918
1852
  }
1919
- function copyHookRails(fromHome, toHome) {
1920
- const src = path.join(fromHome, "hooks");
1921
- if (!fs.existsSync(src))
1922
- return false;
1923
- fs.cpSync(src, path.join(toHome, "hooks"), { recursive: true });
1924
- return true;
1925
- }
1926
- function railsPresent(home) {
1927
- return fs.existsSync(path.join(home, "hooks"));
1928
- }
1929
1853
  /**
1930
1854
  * Spec §5.1: `hq lanes ask` from inside a lane can hit a binary or dist
1931
1855
  * being replaced mid-call (ENOENT / ETXTBSY / a module that no longer
@@ -1934,11 +1858,6 @@ function railsPresent(home) {
1934
1858
  */
1935
1859
  export const ASK_BACKOFF_MS = [5_000, 15_000];
1936
1860
  export const ASK_ATTEMPTS = ASK_BACKOFF_MS.length + 1;
1937
- function yieldEventLoop() {
1938
- return new Promise((resolve) => {
1939
- setImmediate(resolve);
1940
- });
1941
- }
1942
1861
  function laneWaitHumanLine(result) {
1943
1862
  const details = Object.entries(result.change)
1944
1863
  .filter(([key]) => key !== "type")
@@ -1996,11 +1915,6 @@ export function casUpdateLane(hqRoot, laneId, updater) {
1996
1915
  return updateLane(hqRoot, laneId, current.version, "command", updater);
1997
1916
  });
1998
1917
  }
1999
- function throwIfQuestionNotPending(current) {
2000
- if (current.status === "pending")
2001
- return;
2002
- throw new LanesError("question_not_pending", `Question ${current.question_id} is ${current.status}`, { question_id: current.question_id, status: current.status });
2003
- }
2004
1918
  function formatUnknownValue(value) {
2005
1919
  if (typeof value === "string")
2006
1920
  return value;
@@ -3924,159 +3838,6 @@ async function readOutpostLaneSnapshot(ctx, outpostId, laneId) {
3924
3838
  }
3925
3839
  return remote.lane;
3926
3840
  }
3927
- async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
3928
- if (lane.host.kind !== "outpost") {
3929
- throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to answer its question`, { lane_id: lane.lane_id, question_id: questionId });
3930
- }
3931
- const outpostId = lane.host.id;
3932
- let dispatched;
3933
- try {
3934
- dispatched = await ctx.dispatchOutpostExec({
3935
- outpostId,
3936
- command: buildOutpostQuestionAnswerCommand({
3937
- questionId,
3938
- choice: options.choice,
3939
- freeform: options.freeform,
3940
- notes: options.notes,
3941
- company: options.company,
3942
- }),
3943
- });
3944
- }
3945
- catch (err) {
3946
- throw new LanesError("outpost_dispatch_failed", `Could not dispatch question ${questionId} to Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
3947
- }
3948
- if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
3949
- throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} did not confirm the answer for question ${questionId}`, {
3950
- lane_id: lane.lane_id,
3951
- question_id: questionId,
3952
- outpost_id: outpostId,
3953
- command_id: dispatched.commandId,
3954
- status: dispatched.status,
3955
- done: dispatched.done ?? false,
3956
- exit_code: dispatched.exitCode,
3957
- });
3958
- }
3959
- const remote = parseRemoteJson(dispatched.stdout, "Outpost question answer");
3960
- if (remote?.error !== undefined) {
3961
- throw new LanesError("outpost_remote_command_failed", typeof remote.message === "string"
3962
- ? remote.message
3963
- : `Outpost ${outpostId} rejected the answer for question ${questionId}`, {
3964
- lane_id: lane.lane_id,
3965
- question_id: questionId,
3966
- outpost_id: outpostId,
3967
- command_id: dispatched.commandId,
3968
- remote_error: remote.error,
3969
- });
3970
- }
3971
- const remoteQuestion = remote?.question !== null && typeof remote?.question === "object"
3972
- ? remote.question
3973
- : null;
3974
- const awaitingWakeConfirmed = lane.state !== "awaiting_input" ||
3975
- (remote?.wake_status === "resumed" && remote.lane_state === "running") ||
3976
- (remote?.wake_status === "queued" && remote.lane_state === "queued") ||
3977
- (remote?.wake_status === "awaiting_answers" && remote.lane_state === "awaiting_input");
3978
- if (remote?.ok !== true ||
3979
- remote.action !== "questions.answer" ||
3980
- remoteQuestion?.question_id !== questionId ||
3981
- remoteQuestion.lane_id !== lane.lane_id ||
3982
- remoteQuestion.status !== "answered" ||
3983
- typeof remote.delivery !== "string" ||
3984
- !awaitingWakeConfirmed) {
3985
- throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without confirming the answer and wake status for question ${questionId}`, {
3986
- lane_id: lane.lane_id,
3987
- question_id: questionId,
3988
- outpost_id: outpostId,
3989
- command_id: dispatched.commandId,
3990
- });
3991
- }
3992
- let ownerQuestion;
3993
- try {
3994
- ownerQuestion = parseQuestionRecord(remoteQuestion);
3995
- }
3996
- catch (err) {
3997
- throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return a valid answered question record for ${questionId}: ${dispatchErrorMessage(err)}`, {
3998
- lane_id: lane.lane_id,
3999
- question_id: questionId,
4000
- outpost_id: outpostId,
4001
- command_id: dispatched.commandId,
4002
- });
4003
- }
4004
- const ownerAnswer = ownerQuestion.answer;
4005
- const ownerLaneCompany = effectiveLaneQuestionCompany(lane, ctx.hqRoot());
4006
- if (!isQuestionAnswer(ownerAnswer) ||
4007
- ownerQuestion.lane_id !== lane.lane_id ||
4008
- (ownerQuestion.company !== null && ownerLaneCompany !== null &&
4009
- !sameQuestionCompany(ownerQuestion.company, ownerLaneCompany))) {
4010
- throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return the confirmed answer for question ${questionId}`, {
4011
- lane_id: lane.lane_id,
4012
- question_id: questionId,
4013
- outpost_id: outpostId,
4014
- command_id: dispatched.commandId,
4015
- });
4016
- }
4017
- const updatePrimaryCopy = () => casUpdateQuestion(ctx.hqRoot(), questionId, (current) => {
4018
- if (current.lane_id !== lane.lane_id) {
4019
- throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} belongs to a different lane than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
4020
- }
4021
- if (current.company !== null &&
4022
- effectiveLaneQuestionCompany(lane, ctx.hqRoot()) !== null &&
4023
- !sameQuestionCompany(current.company, effectiveLaneQuestionCompany(lane, ctx.hqRoot()))) {
4024
- throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} belongs to a different company than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
4025
- }
4026
- if (current.company !== null &&
4027
- ownerQuestion.company !== null &&
4028
- !sameQuestionCompany(current.company, ownerQuestion.company)) {
4029
- throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} has a different company than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
4030
- }
4031
- if (current.status === "pending") {
4032
- return {
4033
- ...current,
4034
- company: current.company ?? ownerQuestion.company,
4035
- status: "answered",
4036
- answer: ownerAnswer,
4037
- delivered_at: ownerQuestion.delivered_at ?? current.delivered_at ?? null,
4038
- };
4039
- }
4040
- if (current.status === "answered" && sameQuestionAnswer(current.answer, ownerAnswer)) {
4041
- const deliveredAt = ownerQuestion.delivered_at ?? current.delivered_at ?? null;
4042
- const company = current.company ?? ownerQuestion.company;
4043
- return deliveredAt === current.delivered_at && company === current.company
4044
- ? current
4045
- : { ...current, company, delivered_at: deliveredAt };
4046
- }
4047
- throw new LanesError("outpost_question_sync_conflict", `Primary question ${questionId} is ${current.status} or has a different answer than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
4048
- });
4049
- let syncedQuestion;
4050
- try {
4051
- syncedQuestion = updatePrimaryCopy();
4052
- }
4053
- catch (err) {
4054
- if (!isLanesError(err) || err.error !== "question_not_found")
4055
- throw err;
4056
- const primaryLane = readLane(ctx.hqRoot(), lane.lane_id);
4057
- const primaryCompany = effectiveLaneQuestionCompany(primaryLane, ctx.hqRoot());
4058
- const ownerCompany = effectiveLaneQuestionCompany(lane, ctx.hqRoot());
4059
- if (primaryCompany !== null && ownerCompany !== null && !sameQuestionCompany(primaryCompany, ownerCompany)) {
4060
- throw new LanesError("outpost_question_sync_conflict", `Primary lane ${lane.lane_id} belongs to a different company than Outpost ${outpostId} confirmed`, { lane_id: lane.lane_id, question_id: questionId, outpost_id: outpostId });
4061
- }
4062
- try {
4063
- syncedQuestion = createQuestion(ctx.hqRoot(), ownerQuestion);
4064
- }
4065
- catch (createErr) {
4066
- if (!isLanesError(createErr) || createErr.error !== "question_exists")
4067
- throw createErr;
4068
- syncedQuestion = updatePrimaryCopy();
4069
- }
4070
- }
4071
- printJson({
4072
- ...remote,
4073
- lane_id: lane.lane_id,
4074
- question: syncedQuestion,
4075
- outpost_id: outpostId,
4076
- outpost_command_id: dispatched.commandId,
4077
- outpost_transport: "outposts_exec",
4078
- });
4079
- }
4080
3841
  function isQuestionAnswer(value) {
4081
3842
  if (value === null || typeof value !== "object")
4082
3843
  return false;
@@ -6530,74 +6291,6 @@ function persistClaudeAccountSelection(hqRoot, lane, selection) {
6530
6291
  return { ...current, account_selection: selection };
6531
6292
  });
6532
6293
  }
6533
- async function applyClaudeBalanceMove(ctx, suggestion) {
6534
- const currentHost = reconcileHostFromCtx(ctx);
6535
- const suggestionRunsHere = suggestion.host.kind === "outpost"
6536
- ? currentHost.outpostId === suggestion.host.id
6537
- : currentHost.outpostId === null && currentHost.hostname === suggestion.host.id;
6538
- if (suggestion.host.kind === "outpost" && !suggestionRunsHere) {
6539
- let result;
6540
- try {
6541
- result = await ctx.dispatchOutpostExec({
6542
- outpostId: suggestion.host.id,
6543
- command: buildOutpostBalanceMoveCommand({
6544
- laneId: suggestion.lane_id,
6545
- account: suggestion.to,
6546
- fromAccount: suggestion.from,
6547
- expectedState: suggestion.state,
6548
- }),
6549
- timeoutSeconds: 120,
6550
- waitMs: 120_000,
6551
- });
6552
- }
6553
- catch (err) {
6554
- throw new LanesError("claude_account_balance_move_failed", `Could not move lane ${suggestion.lane_id} on Outpost ${suggestion.host.id}: ${dispatchErrorMessage(err)}`, { lane_id: suggestion.lane_id, host: suggestion.host, account: suggestion.to });
6555
- }
6556
- if (result.done !== true || result.status !== "Success" || result.exitCode !== 0) {
6557
- throw new LanesError("claude_account_balance_move_failed", `Outpost ${suggestion.host.id} did not confirm the account move for lane ${suggestion.lane_id}`, { lane_id: suggestion.lane_id, host: suggestion.host, account: suggestion.to, status: result.status, exit_code: result.exitCode });
6558
- }
6559
- const remote = parseRemoteJson(result.stdout, "Claude account balance move");
6560
- if (remote?.ok !== true || remote.action !== "balance-move-target" ||
6561
- remote.lane_id !== suggestion.lane_id || remote.from !== suggestion.from ||
6562
- remote.account !== suggestion.to ||
6563
- remote.state !== suggestion.state) {
6564
- throw new LanesError("claude_account_balance_move_unconfirmed", `Outpost ${suggestion.host.id} returned an invalid account-move result`, {
6565
- lane_id: suggestion.lane_id,
6566
- host: suggestion.host,
6567
- account: suggestion.to,
6568
- });
6569
- }
6570
- return { lane_id: suggestion.lane_id, host: suggestion.host, from: suggestion.from, to: suggestion.to, state: suggestion.state };
6571
- }
6572
- const lane = laneWithRecoveredProviderSession(ctx.hqRoot(), readLane(ctx.hqRoot(), suggestion.lane_id));
6573
- if (lane.state === "running") {
6574
- throw new LanesError("lane_running", `Lane ${lane.lane_id} is running; balance never moves a running turn`, {
6575
- lane_id: lane.lane_id,
6576
- state: lane.state,
6577
- });
6578
- }
6579
- if (lane.host.kind !== suggestion.host.kind || lane.host.id !== suggestion.host.id ||
6580
- lane.account !== suggestion.from || lane.state !== suggestion.state) {
6581
- throw new LanesError("claude_account_balance_lane_changed", `Lane ${lane.lane_id} changed after balance was calculated`, {
6582
- lane_id: lane.lane_id,
6583
- host: lane.host,
6584
- account: lane.account ?? null,
6585
- });
6586
- }
6587
- if (!BALANCE_MOVABLE_STATES.includes(lane.state)) {
6588
- throw new LanesError("claude_account_balance_state_not_movable", `Lane ${lane.lane_id} is no longer between turns`, {
6589
- lane_id: lane.lane_id,
6590
- state: lane.state,
6591
- });
6592
- }
6593
- const moved = await prepareAccountMove(ctx, lane, {
6594
- account: suggestion.to,
6595
- cliModel: undefined,
6596
- accountHistoryReason: automaticBalanceMoveHistoryReason(ctx, lane),
6597
- deferTurnStart: true,
6598
- });
6599
- return { lane_id: moved.lane.lane_id, host: moved.lane.host, from: moved.from ?? suggestion.from, to: moved.to, state: moved.lane.state };
6600
- }
6601
6294
  async function runClaudeBalanceMoveTarget(ctx, laneId, opts) {
6602
6295
  const lane = laneWithRecoveredProviderSession(ctx.hqRoot(), readLane(ctx.hqRoot(), laneId));
6603
6296
  assertLaneRunsHereForOperatorMutation(ctx, lane, "balance-move-target");
@@ -8167,1119 +7860,70 @@ function parseOutpostQuestionBody(raw, outpostId, questionId) {
8167
7860
  }
8168
7861
  return parseQuestionBody(raw);
8169
7862
  }
8170
- async function readOutpostQuestionDetail(ctx, owner) {
8171
- if (owner.lane.host.kind !== "outpost") {
8172
- throw new LanesError("outpost_question_owner_invalid", `Question ${owner.question.question_id} does not have an Outpost owner`, { question_id: owner.question.question_id, lane_id: owner.lane.lane_id });
8173
- }
8174
- const outpostId = owner.lane.host.id;
8175
- let result;
8176
- try {
8177
- result = await ctx.dispatchOutpostExec({
8178
- outpostId,
8179
- command: buildOutpostQuestionShowCommand(owner.question.question_id),
8180
- timeoutSeconds: CAPACITY_PROBE_TIMEOUT_MS / 1000,
8181
- waitMs: CAPACITY_PROBE_TIMEOUT_MS,
8182
- });
8183
- }
8184
- catch {
8185
- throw new LanesError("outpost_question_show_unavailable", `Could not read question ${owner.question.question_id} from Outpost ${outpostId}`, { outpost_id: outpostId, question_id: owner.question.question_id });
8186
- }
8187
- if (result.done !== true || result.status !== "Success" || result.exitCode !== 0) {
8188
- throw new LanesError("outpost_question_show_unavailable", `Outpost ${outpostId} did not return question ${owner.question.question_id}`, { outpost_id: outpostId, question_id: owner.question.question_id });
8189
- }
8190
- let raw;
8191
- try {
8192
- raw = JSON.parse(result.stdout);
8193
- }
8194
- catch {
8195
- throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned invalid question-show JSON`, { outpost_id: outpostId, question_id: owner.question.question_id });
8196
- }
8197
- if (raw === null ||
8198
- typeof raw !== "object" ||
8199
- Array.isArray(raw) ||
8200
- raw.ok !== true ||
8201
- raw.action !== "questions.show" ||
8202
- !Object.prototype.hasOwnProperty.call(raw, "body")) {
8203
- throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned a malformed question-show response`, { outpost_id: outpostId, question_id: owner.question.question_id });
8204
- }
8205
- const response = raw;
8206
- let question;
8207
- try {
8208
- question = parseQuestionRecord(response.question);
8209
- }
8210
- catch {
8211
- throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned an invalid question record`, { outpost_id: outpostId, question_id: owner.question.question_id });
8212
- }
8213
- if (question.question_id !== owner.question.question_id ||
8214
- question.lane_id !== owner.lane.lane_id) {
8215
- throw new LanesError("outpost_question_company_mismatch", `Outpost ${outpostId} returned question ${owner.question.question_id} for a different lane or company`, { outpost_id: outpostId, question_id: owner.question.question_id });
8216
- }
8217
- assertQuestionCompanyMatchesLane(question, owner.lane, "outpost_question_company_mismatch", ctx.hqRoot());
8218
- const audience = parseOutpostQuestionAudience(response.audience, outpostId, 0);
8219
- const body = parseOutpostQuestionBody(response.body, outpostId, question.question_id);
8220
- return { question, audience, body };
8221
- }
8222
- async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
8223
- const hostedLanes = loadAllLanes(hqRoot).filter((lane) => lane.host.kind === "outpost" &&
8224
- (!companyFilter ||
8225
- effectiveLaneQuestionCompany(lane, hqRoot) === null ||
8226
- questionCompanyRefMatches(effectiveLaneQuestionCompany(lane, hqRoot), companyFilter)));
8227
- const lanesByOutpost = new Map();
8228
- for (const lane of hostedLanes) {
8229
- if (lane.host.kind !== "outpost")
8230
- continue;
8231
- const lanes = lanesByOutpost.get(lane.host.id) ?? [];
8232
- lanes.push(lane);
8233
- lanesByOutpost.set(lane.host.id, lanes);
8234
- }
8235
- const questions = [];
8236
- const errors = [];
8237
- const eligibleGroups = [];
8238
- for (const [outpostId, lanes] of [...lanesByOutpost.entries()].sort(([a], [b]) => a.localeCompare(b))) {
8239
- for (const group of groupQuestionLanesByCompany(hqRoot, lanes, companyFilter)) {
8240
- if (group.company === null) {
8241
- errors.push({
8242
- host: `outpost:${outpostId}`,
8243
- error: "outpost_question_company_unavailable",
8244
- });
8245
- continue;
8246
- }
8247
- if (companyFilter && !questionCompanyRefMatches(group.company, companyFilter))
8248
- continue;
8249
- eligibleGroups.push({ outpostId, group });
8250
- }
8251
- }
8252
- if (eligibleGroups.length === 0) {
8253
- return { questions, errors: [...new Map(errors.map((entry) => [entry.host, entry])).values()] };
8254
- }
8255
- let knownOutpostIds;
7863
+ function writeAnswerDeliveryFailed(hqRoot, laneId, questionId, err, at) {
7864
+ const failure = {
7865
+ schema_version: "hq-lane-ask-delivery-failed.v1",
7866
+ at,
7867
+ lane_id: laneId,
7868
+ question_id: questionId,
7869
+ error: err instanceof Error
7870
+ ? { name: err.name, message: err.message }
7871
+ : { name: "Error", message: String(err) },
7872
+ };
8256
7873
  try {
8257
- knownOutpostIds = await ctx.listOutpostIds();
8258
- }
8259
- catch {
8260
- return {
8261
- questions,
8262
- errors: [...new Map([
8263
- ...errors,
8264
- ...[...new Set(eligibleGroups.map(({ outpostId }) => outpostId))].map((outpostId) => ({
8265
- host: `outpost:${outpostId}`,
8266
- error: "outpost_list_unavailable",
8267
- })),
8268
- ].map((entry) => [entry.host, entry])).values()],
8269
- };
7874
+ writeJsonFile(askDeliveryFailedPath(hqRoot, laneId), failure);
8270
7875
  }
8271
- const normalizedOutpostIds = knownOutpostIds.map((id) => typeof id === "string" ? id.trim() : "");
8272
- if (knownOutpostIds.length > MAX_PLACEMENT_OUTPOSTS ||
8273
- normalizedOutpostIds.some((id) => id === "") ||
8274
- new Set(normalizedOutpostIds).size !== normalizedOutpostIds.length) {
8275
- throw new LanesError("outpost_list_invalid", "Outpost list is malformed or exceeds the placement limit", { count: knownOutpostIds.length, maximum: MAX_PLACEMENT_OUTPOSTS });
7876
+ catch (writeErr) {
7877
+ process.stderr.write(`hq lanes: failed to write ask_delivery_failed for ${laneId}: ${writeErr instanceof Error ? writeErr.message : String(writeErr)}\n`);
8276
7878
  }
8277
- const knownOutpostIdSet = new Set(normalizedOutpostIds);
8278
- const loadedGroups = await mapWithConcurrency(eligibleGroups, OUTPOST_QUESTION_LOOKUP_CONCURRENCY, async ({ outpostId, group }) => {
8279
- if (!knownOutpostIdSet.has(outpostId)) {
8280
- return {
8281
- questions: [],
8282
- errors: [{ host: `outpost:${outpostId}`, error: "unknown_outpost" }],
8283
- };
8284
- }
8285
- try {
8286
- const remoteQuestions = await readOutpostQuestionRecords(ctx, outpostId, group.company);
8287
- const localLanesById = new Map(group.lanes.map((lane) => [lane.lane_id, lane]));
8288
- const groupQuestions = [];
8289
- const seenQuestionIds = new Set();
8290
- for (const { question, audience } of remoteQuestions) {
8291
- const lane = localLanesById.get(question.lane_id);
8292
- if (!lane)
8293
- continue;
8294
- assertQuestionCompanyMatchesLane(question, lane, "outpost_question_company_mismatch", hqRoot);
8295
- if (seenQuestionIds.has(question.question_id)) {
8296
- throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned a duplicate question id`, { outpost_id: outpostId, question_id: question.question_id });
8297
- }
8298
- seenQuestionIds.add(question.question_id);
8299
- groupQuestions.push({
8300
- question,
8301
- lane,
8302
- host: `outpost:${outpostId}`,
8303
- audience,
8304
- });
8305
- }
8306
- return { questions: groupQuestions, errors: [] };
8307
- }
8308
- catch (err) {
8309
- return {
8310
- questions: [],
8311
- errors: [{
8312
- host: `outpost:${outpostId}`,
8313
- error: isLanesError(err) ? err.error : "outpost_question_list_unavailable",
8314
- }],
8315
- };
8316
- }
7879
+ }
7880
+ /**
7881
+ * Record is already durable. Delivery is best-effort onto the asking lane's
7882
+ * drop box (spec §6.5). Target is question.lane_id, never a project/story lookup.
7883
+ */
7884
+ async function runReconcile(ctx, opts) {
7885
+ const fromEnv = ctx.env.HQ_ROOT?.trim();
7886
+ const hqRoot = fromEnv && ctx.existsSync(fromEnv) ? fromEnv : ctx.hqRoot();
7887
+ const summary = await runReconcilePass(hqRoot, {
7888
+ now: ctx.now,
7889
+ host: reconcileHostFromCtx(ctx),
7890
+ dryRun: Boolean(opts.dryRun),
7891
+ spawnLaneDeps: {
7892
+ env: ctx.env,
7893
+ modelDeps: {
7894
+ readFileSync: ctx.readFileSync,
7895
+ probe: ctx.probeProvider,
7896
+ env: ctx.env,
7897
+ },
7898
+ existsSync: ctx.existsSync,
7899
+ spawnProcess: ctx.spawnProcess,
7900
+ proveDetached: ctx.proveDetached,
7901
+ killProcess: ctx.killProcess,
7902
+ now: ctx.now,
7903
+ resolveMachineIdentity: ctx.resolveMachineIdentity,
7904
+ runCompaction: ctx.runCompaction,
7905
+ locateTranscript: ctx.locateTranscript,
7906
+ },
7907
+ throttleDeps: ctx.throttleDeps,
7908
+ livenessDeps: livenessDepsFromCtx(ctx),
7909
+ presenceConnected: ctx.presenceConnected,
7910
+ resolveQuestionActiveCompany: () => resolveActiveQuestionCompany(ctx),
7911
+ recoverQueuedMessages: async (lane) => {
7912
+ await recoverQueuedMessages(ctx, lane.lane_id);
7913
+ },
7914
+ mesh: ctx.mesh,
7915
+ env: ctx.env,
8317
7916
  });
8318
- questions.push(...loadedGroups.flatMap(({ questions: groupQuestions }) => groupQuestions));
8319
- errors.push(...loadedGroups.flatMap(({ errors: groupErrors }) => groupErrors));
8320
- return {
8321
- questions,
8322
- errors: [...new Map(errors.map((entry) => [entry.host, entry])).values()],
8323
- };
7917
+ printJson(summary);
7918
+ if (!summary.ok) {
7919
+ process.exit(1);
7920
+ }
8324
7921
  }
8325
- function syncPrimaryQuestionDelivery(hqRoot, owner) {
8326
- const deliveredAt = owner.question.delivered_at;
8327
- if (!deliveredAt)
8328
- return;
8329
- try {
8330
- casUpdateQuestion(hqRoot, owner.question.question_id, (current) => {
8331
- if (current.lane_id !== owner.lane.lane_id) {
8332
- throw new LanesError("outpost_question_sync_conflict", `Primary question ${current.question_id} belongs to a different lane than its Outpost owner`, { lane_id: owner.lane.lane_id, question_id: current.question_id });
8333
- }
8334
- if (current.company !== null &&
8335
- owner.question.company !== null &&
8336
- !sameQuestionCompany(current.company, owner.question.company)) {
8337
- throw new LanesError("outpost_question_sync_conflict", `Primary question ${current.question_id} belongs to a different company than its Outpost owner`, { lane_id: owner.lane.lane_id, question_id: current.question_id });
8338
- }
8339
- const company = current.company ?? owner.question.company;
8340
- const nextDeliveredAt = current.delivered_at ?? deliveredAt;
8341
- return company === current.company && nextDeliveredAt === current.delivered_at
8342
- ? current
8343
- : { ...current, company, delivered_at: nextDeliveredAt };
8344
- });
8345
- }
8346
- catch (err) {
8347
- if (isLanesError(err) && err.error === "question_not_found")
8348
- return;
8349
- throw err;
8350
- }
8351
- }
8352
- async function runQuestionsList(ctx, opts) {
8353
- const hqRoot = ctx.hqRoot();
8354
- const status = opts.status ? parseQuestionStatus(opts.status) : undefined;
8355
- const forFilter = opts.for ? parseForFilter(opts.for, ctx) : undefined;
8356
- const company = opts.company
8357
- ? await ctx.resolveCompany(opts.company)
8358
- : await resolveActiveQuestionCompany(ctx, "Cannot access lane questions because the active HQ company could not be resolved; re-run with --company <slug>");
8359
- const localLanes = new Map(loadAllLanes(hqRoot).map((lane) => [lane.lane_id, lane]));
8360
- const targeted = opts.from !== undefined || opts.project !== undefined || opts.story !== undefined;
8361
- const localQuestions = loadAllQuestions(hqRoot)
8362
- .filter((q) => {
8363
- if (status && q.status !== status)
8364
- return false;
8365
- if (opts.from && q.lane_id !== opts.from && q.origin.id !== opts.from)
8366
- return false;
8367
- if (opts.project && q.project_id !== opts.project)
8368
- return false;
8369
- if (opts.story && q.story_id !== opts.story)
8370
- return false;
8371
- const lane = localLanes.get(q.lane_id);
8372
- if (lane)
8373
- assertQuestionCompanyMatchesLane(q, lane, "question_company_mismatch", hqRoot);
8374
- const questionCompany = lane ? questionCompanyForLane(q, lane, hqRoot) : q.company;
8375
- if (questionCompany !== null && !sameQuestionCompany(questionCompany, company)) {
8376
- if (targeted) {
8377
- throw new LanesError("question_company_mismatch", `Question ${q.question_id} does not belong to the requested or active company`, { question_id: q.question_id, lane_id: q.lane_id });
8378
- }
8379
- return false;
8380
- }
8381
- if (forFilter) {
8382
- const audience = resolveAudience(hqRoot, q, ctx);
8383
- if (!audienceMatches(audience, forFilter))
8384
- return false;
8385
- }
8386
- return true;
8387
- });
8388
- const outpostLoad = opts.allHosts
8389
- ? await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company)
8390
- : { questions: [], errors: [] };
8391
- for (const owner of outpostLoad.questions) {
8392
- assertQuestionCompanyAccess(owner.question, owner.lane, company, hqRoot);
8393
- syncPrimaryQuestionDelivery(hqRoot, owner);
8394
- }
8395
- const remoteQuestionIds = new Set(outpostLoad.questions.map(({ question }) => question.question_id));
8396
- const questions = [
8397
- ...localQuestions
8398
- .filter((q) => !remoteQuestionIds.has(q.question_id))
8399
- .map((q) => {
8400
- const row = {
8401
- ...q,
8402
- audience: resolveAudience(hqRoot, q, ctx),
8403
- ...(q.question_kind === "continue" ? { flag: "continue-loop" } : {}),
8404
- };
8405
- if (!opts.allHosts)
8406
- return row;
8407
- const lane = localLanes.get(q.lane_id);
8408
- const host = lane?.host.kind === "outpost" ? `outpost:${lane.host.id}` : "local";
8409
- return { ...row, host };
8410
- }),
8411
- ...outpostLoad.questions
8412
- .filter(({ question: q, lane, audience }) => {
8413
- if (status && q.status !== status)
8414
- return false;
8415
- if (opts.from && q.lane_id !== opts.from && q.origin.id !== opts.from)
8416
- return false;
8417
- if (opts.project && q.project_id !== opts.project)
8418
- return false;
8419
- if (opts.story && q.story_id !== opts.story)
8420
- return false;
8421
- assertQuestionCompanyAccess(q, lane, company, hqRoot);
8422
- if (forFilter) {
8423
- if (!audienceMatches(audience, forFilter))
8424
- return false;
8425
- }
8426
- return true;
8427
- })
8428
- .map(({ question: q, host, audience }) => ({
8429
- ...q,
8430
- host,
8431
- audience,
8432
- ...(q.question_kind === "continue" ? { flag: "continue-loop" } : {}),
8433
- })),
8434
- ];
8435
- printJson({
8436
- ok: true,
8437
- action: "questions.list",
8438
- questions,
8439
- ...(outpostLoad.errors.length > 0 ? { errors: outpostLoad.errors } : {}),
8440
- });
8441
- }
8442
- async function runQuestionsShow(ctx, questionId) {
8443
- const hqRoot = ctx.hqRoot();
8444
- let question;
8445
- try {
8446
- question = readQuestion(hqRoot, questionId);
8447
- }
8448
- catch (err) {
8449
- if (!isLanesError(err) || err.error !== "question_not_found")
8450
- throw err;
8451
- const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
8452
- if (lookup.errors.length > 0) {
8453
- throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
8454
- question_id: questionId,
8455
- hosts: lookup.errors.map(({ host, error }) => ({ host, error })),
8456
- });
8457
- }
8458
- const matches = lookup.questions.filter((entry) => entry.question.question_id === questionId);
8459
- if (matches.length === 0) {
8460
- throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
8461
- }
8462
- if (matches.length > 1) {
8463
- throw new LanesError("outpost_question_ambiguous", `Question ${questionId} is present on more than one Outpost`, { question_id: questionId, owner_count: matches.length });
8464
- }
8465
- const owner = matches[0];
8466
- if (!owner) {
8467
- throw new LanesError("outpost_question_ambiguous", `Question ${questionId} has no unique Outpost owner`, { question_id: questionId });
8468
- }
8469
- const detail = await readOutpostQuestionDetail(ctx, owner);
8470
- printJson({
8471
- ok: true,
8472
- action: "questions.show",
8473
- question: detail.question,
8474
- audience: detail.audience,
8475
- body: detail.body,
8476
- host: owner.host,
8477
- });
8478
- return;
8479
- }
8480
- const body = readQuestionBody(hqRoot, question);
8481
- printJson({
8482
- ok: true,
8483
- action: "questions.show",
8484
- question,
8485
- audience: resolveAudience(hqRoot, question, ctx),
8486
- body,
8487
- });
8488
- }
8489
- async function runQuestionsAnswer(ctx, questionId, opts) {
8490
- const hqRoot = ctx.hqRoot();
8491
- if (opts.choice !== undefined)
8492
- assertNoSecretText("answer choice", opts.choice);
8493
- if (opts.freeform !== undefined)
8494
- assertNoSecretText("freeform answer", opts.freeform);
8495
- if (opts.notes !== undefined)
8496
- assertNoSecretText("answer notes", opts.notes);
8497
- const explicitCompany = opts.company ? await ctx.resolveCompany(opts.company) : undefined;
8498
- let question;
8499
- try {
8500
- question = readQuestion(hqRoot, questionId);
8501
- }
8502
- catch (err) {
8503
- if (!isLanesError(err) || err.error !== "question_not_found")
8504
- throw err;
8505
- const company = explicitCompany ?? await resolveActiveQuestionCompany(ctx, "Cannot look up this lane question because the active HQ company could not be resolved; re-run with --company <slug>");
8506
- const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company);
8507
- if (lookup.errors.length > 0) {
8508
- throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
8509
- question_id: questionId,
8510
- hosts: lookup.errors.map(({ host, error }) => ({ host, error })),
8511
- });
8512
- }
8513
- const matches = lookup.questions.filter((entry) => entry.question.question_id === questionId);
8514
- if (matches.length === 0) {
8515
- const allCompanyLookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
8516
- const otherCompanyMatch = allCompanyLookup.questions.find((entry) => entry.question.question_id === questionId);
8517
- if (otherCompanyMatch) {
8518
- assertQuestionCompanyAccess(otherCompanyMatch.question, otherCompanyMatch.lane, company, hqRoot);
8519
- }
8520
- if (allCompanyLookup.errors.length > 0) {
8521
- throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
8522
- question_id: questionId,
8523
- hosts: allCompanyLookup.errors.map(({ host, error }) => ({ host, error })),
8524
- });
8525
- }
8526
- throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
8527
- }
8528
- if (matches.length > 1) {
8529
- throw new LanesError("outpost_question_ambiguous", `Question ${questionId} is present on more than one Outpost`, { question_id: questionId, owner_count: matches.length });
8530
- }
8531
- const owner = matches[0];
8532
- if (!owner) {
8533
- throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
8534
- }
8535
- assertQuestionCompanyAccess(owner.question, owner.lane, company, hqRoot);
8536
- const ownerCompany = await resolveQuestionCompanyWithFallback(hqRoot, owner.lane, async () => explicitCompany ?? company);
8537
- assertQuestionCompanyResolvedForAnswer(owner.question, owner.lane, hqRoot, ownerCompany);
8538
- throwIfQuestionNotPending(owner.question);
8539
- await yieldEventLoop();
8540
- await dispatchOutpostQuestionAnswer(ctx, owner.lane, questionId, {
8541
- ...opts,
8542
- company: questionListCompanyArg(ownerCompany),
8543
- });
8544
- return;
8545
- }
8546
- await yieldEventLoop();
8547
- const askingLane = readLane(hqRoot, laneIdForAnswerDelivery(question));
8548
- const resolvedCompany = await resolveQuestionCompanyWithFallback(hqRoot, askingLane, async () => explicitCompany ?? question.company ?? await resolveActiveQuestionCompany(ctx, `Cannot answer legacy question ${questionId}: its company cannot be resolved from the question or lane; re-run with --company <slug>`));
8549
- const requestedCompany = explicitCompany ?? resolvedCompany;
8550
- assertQuestionCompanyAccess(question, askingLane, requestedCompany, hqRoot);
8551
- assertQuestionCompanyResolvedForAnswer(question, askingLane, hqRoot, resolvedCompany);
8552
- const questionCompany = question.company ?? effectiveLaneQuestionCompany(askingLane, hqRoot) ?? resolvedCompany;
8553
- const questionForBody = question.company === null
8554
- ? { ...question, company: questionCompany }
8555
- : question;
8556
- if (askingLane.host.kind === "outpost" && !laneIsHostedHere(ctx, askingLane)) {
8557
- await dispatchOutpostQuestionAnswer(ctx, askingLane, questionId, {
8558
- ...opts,
8559
- company: questionListCompanyArg(questionCompany),
8560
- });
8561
- return;
8562
- }
8563
- throwIfQuestionNotPending(question);
8564
- if (askingLane.state === "awaiting_input") {
8565
- assertLaneRunsHereForOperatorMutation(ctx, askingLane, "wake after answering");
8566
- }
8567
- if (laneAcceptsDropboxDelivery(askingLane)) {
8568
- assertLaneRunsHereForOperatorMutation(ctx, askingLane, "deliver an answer to");
8569
- }
8570
- const body = requireQuestionBody(hqRoot, questionForBody);
8571
- // A trimmed-empty --freeform is absent: it must not win over --choice and
8572
- // must not become a freeform answer of "".
8573
- const freeform = opts.freeform?.trim() || undefined;
8574
- if (freeform !== undefined && body.options.length === 0) {
8575
- throw new LanesError("freeform_not_allowed", "--freeform is only allowed for a question with options", { question_id: questionId });
8576
- }
8577
- if (!freeform && !opts.choice) {
8578
- throw new LanesError("missing_choice", "--choice or --freeform is required", {});
8579
- }
8580
- const choice = freeform ?? resolveAnswerChoice(opts.choice, body.options, false);
8581
- const at = ctx.now().toISOString();
8582
- const answeredBy = sessionIdFromEnv(ctx.env)
8583
- ? `session:${sessionIdFromEnv(ctx.env)}`
8584
- : "user";
8585
- const updated = casUpdateQuestion(hqRoot, questionId, (current) => {
8586
- throwIfQuestionNotPending(current);
8587
- assertQuestionCompanyAccess(current, askingLane, requestedCompany, hqRoot);
8588
- const recordedCompany = current.company ?? questionCompany;
8589
- const currentForBody = current.company === null
8590
- ? { ...current, company: recordedCompany }
8591
- : current;
8592
- let notesRef;
8593
- let nextBody = current.body;
8594
- if (opts.notes) {
8595
- const stored = writeQuestionBody(hqRoot, currentForBody, {
8596
- ...body,
8597
- notes: opts.notes,
8598
- });
8599
- notesRef = stored.ref;
8600
- nextBody = stored;
8601
- }
8602
- return {
8603
- ...current,
8604
- company: recordedCompany,
8605
- body: nextBody,
8606
- status: "answered",
8607
- answer: {
8608
- choice,
8609
- answer_kind: freeform ? "freeform" : "option",
8610
- notes_ref: notesRef,
8611
- answered_by: answeredBy,
8612
- answered_at: at,
8613
- answered_level: current.hop,
8614
- },
8615
- };
8616
- });
8617
- const latestAskingLane = readLane(hqRoot, askingLane.lane_id);
8618
- if (latestAskingLane.state === "awaiting_input") {
8619
- let wake;
8620
- try {
8621
- wake = await wakeAnsweredLaneNow(hqRoot, askingLane.lane_id, {
8622
- now: ctx.now,
8623
- host: reconcileHostFromCtx(ctx),
8624
- dryRun: false,
8625
- spawnLaneDeps: {
8626
- env: ctx.env,
8627
- existsSync: ctx.existsSync,
8628
- spawnProcess: ctx.spawnProcess,
8629
- proveDetached: ctx.proveDetached,
8630
- killProcess: ctx.killProcess,
8631
- now: ctx.now,
8632
- resolveMachineIdentity: ctx.resolveMachineIdentity,
8633
- runCompaction: ctx.runCompaction,
8634
- locateTranscript: ctx.locateTranscript,
8635
- },
8636
- throttleDeps: ctx.throttleDeps,
8637
- livenessDeps: livenessDepsFromCtx(ctx),
8638
- presenceConnected: ctx.presenceConnected,
8639
- mesh: ctx.mesh,
8640
- env: ctx.env,
8641
- });
8642
- }
8643
- catch (err) {
8644
- throw new LanesError("question_wake_failed", `Question ${questionId} was recorded, but lane ${askingLane.lane_id} could not be woken: ${dispatchErrorMessage(err)}`, { lane_id: askingLane.lane_id, question_id: questionId });
8645
- }
8646
- if (wake.status === "failed") {
8647
- throw new LanesError("question_wake_failed", `Question ${questionId} was recorded, but lane ${askingLane.lane_id} could not be woken: ${wake.error}`, { lane_id: askingLane.lane_id, question_id: questionId, wake_error: wake.error });
8648
- }
8649
- printJson({
8650
- ok: true,
8651
- action: "questions.answer",
8652
- lane_id: askingLane.lane_id,
8653
- question: updated,
8654
- delivery: wake.status === "awaiting_answers" ? "awaiting_other_answers" : wake.status,
8655
- wake_status: wake.status,
8656
- lane_state: wake.lane_state,
8657
- ...(wake.queued_reason !== undefined ? { queued_reason: wake.queued_reason } : {}),
8658
- ...(wake.remaining_question_ids !== undefined
8659
- ? { remaining_question_ids: wake.remaining_question_ids }
8660
- : {}),
8661
- });
8662
- return;
8663
- }
8664
- const delivery = deliverAnswerToAskingLane(ctx, updated, choice, at);
8665
- printJson({
8666
- ok: true,
8667
- action: "questions.answer",
8668
- question: updated,
8669
- delivery: delivery.status,
8670
- });
8671
- }
8672
- function writeAnswerDeliveryFailed(hqRoot, laneId, questionId, err, at) {
8673
- const failure = {
8674
- schema_version: "hq-lane-ask-delivery-failed.v1",
8675
- at,
8676
- lane_id: laneId,
8677
- question_id: questionId,
8678
- error: err instanceof Error
8679
- ? { name: err.name, message: err.message }
8680
- : { name: "Error", message: String(err) },
8681
- };
8682
- try {
8683
- writeJsonFile(askDeliveryFailedPath(hqRoot, laneId), failure);
8684
- }
8685
- catch (writeErr) {
8686
- process.stderr.write(`hq lanes: failed to write ask_delivery_failed for ${laneId}: ${writeErr instanceof Error ? writeErr.message : String(writeErr)}\n`);
8687
- }
8688
- }
8689
- /**
8690
- * Record is already durable. Delivery is best-effort onto the asking lane's
8691
- * drop box (spec §6.5). Target is question.lane_id, never a project/story lookup.
8692
- */
8693
- function deliverAnswerToAskingLane(ctx, question, choice, at) {
8694
- const hqRoot = ctx.hqRoot();
8695
- const laneId = laneIdForAnswerDelivery(question);
8696
- try {
8697
- const lane = readLane(hqRoot, laneId);
8698
- if (!laneAcceptsDropboxDelivery(lane)) {
8699
- return { status: "recorded_only" };
8700
- }
8701
- assertLaneRunsHereForOperatorMutation(ctx, lane, "deliver an answer to");
8702
- appendDropboxMessage(hqRoot, laneId, answerDropboxMessage(question, choice, at, mintMessageId()));
8703
- return { status: "awaiting_next_tool_call" };
8704
- }
8705
- catch (err) {
8706
- writeAnswerDeliveryFailed(hqRoot, laneId, question.question_id, err, at);
8707
- return { status: "failed" };
8708
- }
8709
- }
8710
- async function runQuestionsEscalate(ctx, questionId, opts) {
8711
- const hqRoot = ctx.hqRoot();
8712
- const question = readQuestion(hqRoot, questionId);
8713
- throwIfQuestionNotPending(question);
8714
- await yieldEventLoop();
8715
- const originalHop = question.hop;
8716
- const audience = resolveAudience(hqRoot, question, ctx);
8717
- if (audience.kind === "user") {
8718
- throw new LanesError("already_at_top_authority", "Cannot escalate past the user", { question_id: questionId });
8719
- }
8720
- const at = ctx.now().toISOString();
8721
- if (opts.note) {
8722
- requireQuestionBody(hqRoot, question);
8723
- }
8724
- const updated = casUpdateQuestion(hqRoot, questionId, (current) => {
8725
- throwIfQuestionNotPending(current);
8726
- if (current.hop !== originalHop) {
8727
- throw new LanesError("question_already_escalated", `Question ${questionId} hop already advanced`, {
8728
- question_id: questionId,
8729
- hop: current.hop,
8730
- expected_hop: originalHop,
8731
- });
8732
- }
8733
- let nextBody = current.body;
8734
- if (opts.note) {
8735
- const body = requireQuestionBody(hqRoot, current);
8736
- nextBody = writeQuestionBody(hqRoot, current, {
8737
- ...body,
8738
- notes: [body.notes, opts.note].filter(Boolean).join("\n"),
8739
- });
8740
- }
8741
- const nextHop = current.hop + 1;
8742
- return {
8743
- ...current,
8744
- body: nextBody,
8745
- hop: nextHop,
8746
- routing_history: appendCappedHistory(current.routing_history, {
8747
- hop: nextHop,
8748
- audience: resolveAudience(hqRoot, { ...current, hop: nextHop }, ctx),
8749
- at,
8750
- reason: "escalated",
8751
- }),
8752
- };
8753
- });
8754
- const nextAudience = resolveAudience(hqRoot, updated, ctx);
8755
- if (nextAudience.kind === "user") {
8756
- const lane = readLane(hqRoot, updated.lane_id);
8757
- await emitLaneMeshEvent(ctx, lane.mesh_company ?? lane.company, lane.thread_id, "lane_question_at_user", { lane_id: lane.lane_id, question_id: updated.question_id });
8758
- }
8759
- printJson({ ok: true, action: "questions.escalate", question: updated });
8760
- }
8761
- async function runQuestionsCancel(ctx, questionId, opts) {
8762
- const hqRoot = ctx.hqRoot();
8763
- const question = readQuestion(hqRoot, questionId);
8764
- throwIfQuestionNotPending(question);
8765
- await yieldEventLoop();
8766
- const at = ctx.now().toISOString();
8767
- const updated = casUpdateQuestion(hqRoot, questionId, (current) => {
8768
- throwIfQuestionNotPending(current);
8769
- const entry = {
8770
- hop: current.hop,
8771
- audience: resolveAudience(hqRoot, current, ctx),
8772
- at,
8773
- reason: "cancelled",
8774
- ...(opts.reason ? { detail: opts.reason } : {}),
8775
- };
8776
- return {
8777
- ...current,
8778
- status: "cancelled",
8779
- routing_history: appendCappedHistory(current.routing_history, entry),
8780
- };
8781
- });
8782
- printJson({ ok: true, action: "questions.cancel", question: updated });
8783
- }
8784
- async function runAccountsList(ctx, opts) {
8785
- const hqRoot = ctx.hqRoot();
8786
- const config = loadLanesConfig(hqRoot);
8787
- const providers = opts.provider
8788
- ? [parseLaneProvider(opts.provider)]
8789
- : [...LANE_PROVIDERS];
8790
- // model and effort are what a spawn on each account would start from
8791
- // (spec: --model/--effort, then the account, then the provider).
8792
- const quota = loadQuotaState(hqRoot);
8793
- const accounts = providers.flatMap((provider) => listAccountStatuses(provider, config, {
8794
- existsSync: ctx.existsSync,
8795
- hostname: ctx.hostname(),
8796
- probe: ctx.probeProvider,
8797
- env: ctx.env,
8798
- }).map((status) => {
8799
- const accountCfg = config.providers[provider].accounts[status.id];
8800
- const model = resolveLaneModel({
8801
- accountModel: accountCfg?.model,
8802
- providerModel: config.providers[provider].model,
8803
- });
8804
- const effort = resolveLaneEffort({
8805
- accountEffort: accountCfg?.effort,
8806
- providerEffort: config.providers[provider].effort,
8807
- model,
8808
- });
8809
- return {
8810
- ...status,
8811
- effective_home: status.home,
8812
- model: accountCfg?.model ?? null,
8813
- effort: accountCfg?.effort ?? null,
8814
- effective_model: model ?? null,
8815
- effective_effort: effort.effort ?? null,
8816
- effective_effort_source: effort.source,
8817
- ...accountQuotaView(quota, provider, status.id),
8818
- };
8819
- }));
8820
- const providerDefaults = providers.map((provider) => ({
8821
- provider,
8822
- model: config.providers[provider].model ?? null,
8823
- effort: config.providers[provider].effort ?? null,
8824
- compact_after_ms: providerCompactAfterMs(config.providers[provider]),
8825
- }));
8826
- printJson({
8827
- ok: true,
8828
- action: "accounts.list",
8829
- providers: providerDefaults,
8830
- accounts,
8831
- });
8832
- }
8833
- async function runAccountsBalance(ctx, opts) {
8834
- const provider = parseLaneProvider(opts.provider ?? "");
8835
- if (provider !== "claude") {
8836
- throw new LanesError("claude_account_balance_provider_unsupported", "Account balance is available only for --provider claude", {
8837
- provider,
8838
- accepted: ["claude"],
8839
- });
8840
- }
8841
- const hqRoot = ctx.hqRoot();
8842
- const config = loadLanesConfig(hqRoot);
8843
- const loaded = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config);
8844
- // Retain the established response field for clients; balancing is now
8845
- // unconditional, so this compatibility value is always true.
8846
- const balanceGateEnabled = true;
8847
- const loads = accountLoadsForReport({ hosts: loaded.snapshots, config });
8848
- const suggestions = planClaudeBalanceMoves({ hosts: loaded.snapshots, config });
8849
- const selections = loaded.snapshots.map((host) => {
8850
- try {
8851
- const selection = selectClaudeAccount({
8852
- hosts: loaded.snapshots,
8853
- targetHost: host.capacity.host,
8854
- config,
8855
- unavailableHosts: loaded.unavailableHosts,
8856
- });
8857
- return {
8858
- target_host: host.capacity.host,
8859
- account: selection.account,
8860
- signal: selection.signal,
8861
- skipped_accounts: selection.skipped_accounts,
8862
- };
8863
- }
8864
- catch (err) {
8865
- return {
8866
- target_host: host.capacity.host,
8867
- error: isLanesError(err) ? err.error : "claude_account_balance_selection_failed",
8868
- message: dispatchErrorMessage(err),
8869
- };
8870
- }
8871
- });
8872
- const accounts = Object.keys(config.providers.claude.accounts).sort().map((account) => {
8873
- const load = loads.find((row) => row.account === account);
8874
- return {
8875
- account,
8876
- enabled: config.providers.claude.accounts[account]?.enabled === true,
8877
- live_lanes: load?.live_lanes ?? 0,
8878
- counts_complete: load?.counts_complete ?? false,
8879
- hosts: loaded.snapshots.map((host) => {
8880
- const accountStatus = host.capacity.providers.claude.accounts[account];
8881
- const accountLoad = load?.hosts.find((row) => row.host.kind === host.capacity.host.kind && row.host.id === host.capacity.host.id);
8882
- return {
8883
- host: host.capacity.host,
8884
- enabled: accountStatus?.enabled ?? false,
8885
- logged_in: accountStatus?.logged_in ?? null,
8886
- live_lanes: accountLoad?.live_lanes ?? null,
8887
- lane_counts: accountLoad?.lane_counts ?? { running: null, queued: null, awaiting_input: null },
8888
- count_source: accountLoad?.count_source ?? "unavailable",
8889
- usage: accountStatus?.usage ?? null,
8890
- };
8891
- }),
8892
- };
8893
- });
8894
- const moved = [];
8895
- const skipped = [];
8896
- if (opts.apply) {
8897
- for (const suggestion of suggestions) {
8898
- if (!suggestion.apply) {
8899
- skipped.push({
8900
- lane_id: suggestion.lane_id,
8901
- host: suggestion.host,
8902
- from: suggestion.from,
8903
- to: suggestion.to,
8904
- reason: suggestion.skip_reason ?? "not_movable",
8905
- });
8906
- continue;
8907
- }
8908
- try {
8909
- moved.push(await applyClaudeBalanceMove(ctx, suggestion));
8910
- }
8911
- catch (err) {
8912
- skipped.push({
8913
- lane_id: suggestion.lane_id,
8914
- host: suggestion.host,
8915
- from: suggestion.from,
8916
- to: suggestion.to,
8917
- reason: isLanesError(err) ? `${err.error}: ${err.message}` : dispatchErrorMessage(err),
8918
- });
8919
- }
8920
- }
8921
- }
8922
- printJson({
8923
- ok: true,
8924
- action: "accounts.balance",
8925
- provider: "claude",
8926
- balance_gate_enabled: balanceGateEnabled,
8927
- accounts,
8928
- selections,
8929
- unavailable_hosts: loaded.unavailableHosts,
8930
- suggestions,
8931
- ...(opts.apply ? { moved, skipped } : {}),
8932
- });
8933
- }
8934
- async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
8935
- const callerCwd = ctx.callerCwd();
8936
- const hqRoot = ctx.hqRoot();
8937
- const provider = parseLaneProvider(providerRaw);
8938
- const config = loadLanesConfig(hqRoot);
8939
- const id = accountId.trim();
8940
- if (!id) {
8941
- throw new LanesError("invalid_account", "account-id must be non-empty", {});
8942
- }
8943
- if (opts.deviceAuth && provider !== "codex") {
8944
- throw new LanesError("device_auth_unsupported", "--device-auth is supported only for codex account login", { provider });
8945
- }
8946
- const registeredAccount = config.providers[provider].accounts[id];
8947
- const hostname = ctx.hostname().trim();
8948
- if (!hostname) {
8949
- throw new LanesError("host_identity_unknown", "Cannot resolve this host's hostname for account login", {});
8950
- }
8951
- const requestedHome = opts.home?.trim();
8952
- if (opts.home !== undefined && !requestedHome) {
8953
- throw new LanesError("invalid_account_home", "--home must be a non-empty path", { provider, account: id });
8954
- }
8955
- if (opts.company !== undefined) {
8956
- await resolveSelectedAccountHomeCompany(ctx, opts.company);
8957
- }
8958
- const homeRaw = requestedHome ??
8959
- (registeredAccount !== undefined
8960
- ? effectiveAccountHome(registeredAccount, hostname).home
8961
- : id === "a"
8962
- ? DEFAULT_ACCOUNT_HOMES[provider]
8963
- : `~/.${provider}-${id}`);
8964
- const homeResolved = homeRaw.startsWith("~") || path.isAbsolute(homeRaw)
8965
- ? homeRaw
8966
- : resolveCallerPath(callerCwd, homeRaw);
8967
- const home = canonicalizeHome(homeResolved, {
8968
- homedir: ctx.homedir,
8969
- realpathSync: (p) => fs.realpathSync(p),
8970
- existsSync: (p) => fs.existsSync(p),
8971
- });
8972
- const nextAccount = registeredAccount === undefined
8973
- ? { home, enabled: true }
8974
- : { ...registeredAccount, enabled: true };
8975
- if (registeredAccount !== undefined && requestedHome !== undefined) {
8976
- if (home === registeredAccount.home) {
8977
- if (nextAccount.hosts !== undefined) {
8978
- delete nextAccount.hosts[hostname];
8979
- if (Object.keys(nextAccount.hosts).length === 0)
8980
- delete nextAccount.hosts;
8981
- }
8982
- }
8983
- else {
8984
- nextAccount.hosts = {
8985
- ...(registeredAccount.hosts ?? {}),
8986
- [hostname]: { home },
8987
- };
8988
- }
8989
- }
8990
- delete nextAccount.disabled_reason;
8991
- assertUniqueAccountHomes(provider, {
8992
- ...config.providers[provider].accounts,
8993
- [id]: nextAccount,
8994
- });
8995
- ensureDir(home);
8996
- const defaultId = config.providers[provider].default_account;
8997
- const defaultAccount = defaultId !== undefined
8998
- ? config.providers[provider].accounts[defaultId]
8999
- : Object.values(config.providers[provider].accounts).find((account) => account.enabled);
9000
- const defaultHome = (defaultAccount !== undefined
9001
- ? effectiveAccountHome(defaultAccount, hostname).home
9002
- : undefined) ??
9003
- canonicalizeHome(DEFAULT_ACCOUNT_HOMES[provider], {
9004
- homedir: ctx.homedir,
9005
- realpathSync: (p) => fs.realpathSync(p),
9006
- existsSync: (p) => fs.existsSync(p),
9007
- });
9008
- const copied = defaultHome !== home ? copyHookRails(defaultHome, home) : railsPresent(home);
9009
- installDrainHook(provider, home);
9010
- const envVar = PROVIDER_HOME_ENV[provider];
9011
- const login = VENDOR_LOGIN[provider];
9012
- const loginArgs = provider === "codex" && opts.deviceAuth
9013
- ? [...login.args, "--device-auth"]
9014
- : login.args;
9015
- const result = await ctx.spawnLogin({
9016
- command: login.command,
9017
- args: loginArgs,
9018
- env: { ...ctx.env, [envVar]: home },
9019
- cwd: home,
9020
- });
9021
- if (result.status !== 0) {
9022
- throw new LanesError("login_failed", `${login.command} ${loginArgs.join(" ")} exited ${result.status}`, { provider, account: id, status: result.status });
9023
- }
9024
- const grokNeedsRails = provider === "grok";
9025
- if ((grokNeedsRails || copied) && !railsPresent(home)) {
9026
- throw new LanesError("rails_missing", `Hook rails are missing under ${home}; account not enabled`, { provider, account: id, home });
9027
- }
9028
- config.providers[provider].accounts[id] = nextAccount;
9029
- const status = accountStatus(provider, id, config, {
9030
- existsSync: ctx.existsSync,
9031
- hostname,
9032
- probe: ctx.probeProvider,
9033
- env: ctx.env,
9034
- });
9035
- if (status.logged_in !== true) {
9036
- throw new LanesError(status.logged_in === false ? "account_not_logged_in" : "account_login_status_unknown", status.logged_in === false
9037
- ? `Account ${id} for ${provider} is not logged in`
9038
- : `Could not verify that account ${id} for ${provider} is logged in`, { provider, account: id, home });
9039
- }
9040
- writeLanesConfig(hqRoot, config);
9041
- printJson({ ok: true, action: "accounts.login", account: status });
9042
- }
9043
- async function runAccountsSetHome(ctx, providerRaw, accountIdRaw, homeRaw, opts) {
9044
- if (opts.company !== undefined)
9045
- await resolveSelectedAccountHomeCompany(ctx, opts.company);
9046
- const callerCwd = ctx.callerCwd();
9047
- const hqRoot = ctx.hqRoot();
9048
- const provider = parseLaneProvider(providerRaw);
9049
- const id = accountIdRaw.trim();
9050
- if (!id) {
9051
- throw new AccountResolutionError("unknown_account", "account-id must be non-empty", {
9052
- provider,
9053
- account: accountIdRaw,
9054
- enabled: [],
9055
- });
9056
- }
9057
- const hostname = opts.host === undefined ? ctx.hostname().trim() : opts.host.trim();
9058
- if (!hostname || (opts.host !== undefined && hostname !== opts.host)) {
9059
- throw new LanesError("invalid_host", "--host must be a non-empty hostname without surrounding whitespace", {
9060
- host: opts.host ?? "",
9061
- });
9062
- }
9063
- if (!homeRaw.trim()) {
9064
- throw new LanesError("invalid_account_home", "home path must be a non-empty path", {
9065
- provider,
9066
- account: id,
9067
- });
9068
- }
9069
- const config = loadLanesConfig(hqRoot);
9070
- const account = config.providers[provider].accounts[id];
9071
- if (account === undefined) {
9072
- throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(id)} for ${provider}`, {
9073
- provider,
9074
- account: id,
9075
- enabled: Object.keys(config.providers[provider].accounts).filter((key) => config.providers[provider].accounts[key]?.enabled),
9076
- });
9077
- }
9078
- const homePath = homeRaw.trim();
9079
- const isCurrentHost = hostname === ctx.hostname().trim();
9080
- let home;
9081
- if (isCurrentHost) {
9082
- const homeResolved = homePath.startsWith("~") || path.isAbsolute(homePath)
9083
- ? homePath
9084
- : resolveCallerPath(callerCwd, homePath);
9085
- home = canonicalizeHome(homeResolved, {
9086
- homedir: ctx.homedir,
9087
- realpathSync: (p) => fs.realpathSync(p),
9088
- existsSync: (p) => fs.existsSync(p),
9089
- });
9090
- }
9091
- else if (homePath === "~" || homePath.startsWith("~/")) {
9092
- home = homePath;
9093
- }
9094
- else if (path.isAbsolute(homePath)) {
9095
- home = path.normalize(homePath);
9096
- }
9097
- else {
9098
- throw new LanesError("invalid_remote_account_home", "A home for another host must be an absolute path or start with ~/", { provider, account: id, hostname, home: homePath });
9099
- }
9100
- for (const [otherId, other] of Object.entries(config.providers[provider].accounts)) {
9101
- if (otherId !== id && effectiveAccountHome(other, hostname).home === home) {
9102
- throw new LanesError("duplicate_account_home", `Home ${home} is already registered as ${provider} account ${otherId} on host ${hostname}`, { provider, accounts: [otherId, id], home, hostname });
9103
- }
9104
- }
9105
- if (home === account.home) {
9106
- if (account.hosts !== undefined) {
9107
- delete account.hosts[hostname];
9108
- if (Object.keys(account.hosts).length === 0)
9109
- delete account.hosts;
9110
- }
9111
- }
9112
- else {
9113
- const hosts = Object.assign(Object.create(null), account.hosts ?? {});
9114
- hosts[hostname] = { home };
9115
- account.hosts = hosts;
9116
- }
9117
- assertUniqueAccountHomes(provider, config.providers[provider].accounts);
9118
- writeLanesConfig(hqRoot, config);
9119
- printJson({
9120
- ok: true,
9121
- action: "accounts.set-home",
9122
- provider,
9123
- account: id,
9124
- hostname,
9125
- home,
9126
- home_source: home === account.home ? "default" : "host",
9127
- });
9128
- }
9129
- async function runAccountsEnable(ctx, providerRaw, accountId) {
9130
- const hqRoot = ctx.hqRoot();
9131
- const provider = parseLaneProvider(providerRaw);
9132
- const config = loadLanesConfig(hqRoot);
9133
- const account = config.providers[provider].accounts[accountId];
9134
- if (!account) {
9135
- throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(accountId)} for ${provider}`, {
9136
- provider,
9137
- account: accountId,
9138
- enabled: Object.entries(config.providers[provider].accounts)
9139
- .filter(([, a]) => a.enabled)
9140
- .map(([id]) => id),
9141
- });
9142
- }
9143
- const status = accountStatus(provider, accountId, config, {
9144
- existsSync: ctx.existsSync,
9145
- hostname: ctx.hostname(),
9146
- probe: ctx.probeProvider,
9147
- env: ctx.env,
9148
- });
9149
- if (status.logged_in !== true) {
9150
- throw new LanesError(status.logged_in === false ? "account_not_logged_in" : "account_login_status_unknown", status.logged_in === false
9151
- ? `Account ${accountId} for ${provider} is not logged in`
9152
- : `Could not verify that account ${accountId} for ${provider} is logged in`, { provider, account: accountId });
9153
- }
9154
- account.enabled = true;
9155
- delete account.disabled_reason;
9156
- writeLanesConfig(hqRoot, config);
9157
- printJson({
9158
- ok: true,
9159
- action: "accounts.enable",
9160
- account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
9161
- existsSync: ctx.existsSync,
9162
- hostname: ctx.hostname(),
9163
- probe: ctx.probeProvider,
9164
- env: ctx.env,
9165
- }),
9166
- });
9167
- }
9168
- async function runAccountsDisable(ctx, providerRaw, accountId, opts) {
9169
- const hqRoot = ctx.hqRoot();
9170
- const provider = parseLaneProvider(providerRaw);
9171
- const config = loadLanesConfig(hqRoot);
9172
- const account = config.providers[provider].accounts[accountId];
9173
- if (!account) {
9174
- throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(accountId)} for ${provider}`, {
9175
- provider,
9176
- account: accountId,
9177
- enabled: Object.entries(config.providers[provider].accounts)
9178
- .filter(([, a]) => a.enabled)
9179
- .map(([id]) => id),
9180
- });
9181
- }
9182
- account.enabled = false;
9183
- if (opts.reason)
9184
- account.disabled_reason = opts.reason;
9185
- writeLanesConfig(hqRoot, config);
9186
- printJson({
9187
- ok: true,
9188
- action: "accounts.disable",
9189
- account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
9190
- existsSync: ctx.existsSync,
9191
- hostname: ctx.hostname(),
9192
- probe: ctx.probeProvider,
9193
- env: ctx.env,
9194
- }),
9195
- });
9196
- }
9197
- async function runSessionsList(ctx, opts) {
9198
- const hqRoot = ctx.hqRoot();
9199
- const state = opts.state ? parseLivenessState(opts.state) : undefined;
9200
- const livenessDeps = livenessDepsFromCtx(ctx);
9201
- const presenceConnected = ctx.presenceConnected ?? readDaemonPresenceConnected(livenessDeps);
9202
- const rows = listSessionLiveness(hqRoot, {
9203
- nowMs: ctx.now().getTime(),
9204
- presenceConnected,
9205
- deps: livenessDeps,
9206
- }).filter((row) => {
9207
- if (state && row.state !== state)
9208
- return false;
9209
- if (!opts.company)
9210
- return true;
9211
- const meta = readSessionMeta(hqRoot, row.sessionId, ctx);
9212
- const slug = typeof meta?.company_slug === "string" ? meta.company_slug : "";
9213
- const uid = typeof meta?.company_uid === "string" ? meta.company_uid : "";
9214
- return slug === opts.company || uid === opts.company;
9215
- });
9216
- printJson({ ok: true, action: "sessions.list", sessions: rows });
9217
- }
9218
- async function runSessionsShow(ctx, sessionId) {
9219
- const hqRoot = ctx.hqRoot();
9220
- const livenessDeps = livenessDepsFromCtx(ctx);
9221
- const presenceConnected = ctx.presenceConnected ?? readDaemonPresenceConnected(livenessDeps);
9222
- const state = sessionLiveness(hqRoot, sessionId, {
9223
- nowMs: ctx.now().getTime(),
9224
- presenceConnected,
9225
- deps: livenessDeps,
9226
- });
9227
- if (state === null) {
9228
- throw new LanesError("session_not_found", `Session ${sessionId} is not in the liveness index`, { session_id: sessionId });
9229
- }
9230
- const meta = readSessionMeta(hqRoot, sessionId, ctx);
9231
- printJson({
9232
- ok: true,
9233
- action: "sessions.show",
9234
- session_id: sessionId,
9235
- state,
9236
- senior: meta ? seniorOfSession(hqRoot, sessionId, ctx) : { kind: "user" },
9237
- company_slug: typeof meta?.company_slug === "string" ? meta.company_slug : null,
9238
- });
9239
- }
9240
- async function runReconcile(ctx, opts) {
9241
- const fromEnv = ctx.env.HQ_ROOT?.trim();
9242
- const hqRoot = fromEnv && ctx.existsSync(fromEnv) ? fromEnv : ctx.hqRoot();
9243
- const summary = await runReconcilePass(hqRoot, {
9244
- now: ctx.now,
9245
- host: reconcileHostFromCtx(ctx),
9246
- dryRun: Boolean(opts.dryRun),
9247
- spawnLaneDeps: {
9248
- env: ctx.env,
9249
- modelDeps: {
9250
- readFileSync: ctx.readFileSync,
9251
- probe: ctx.probeProvider,
9252
- env: ctx.env,
9253
- },
9254
- existsSync: ctx.existsSync,
9255
- spawnProcess: ctx.spawnProcess,
9256
- proveDetached: ctx.proveDetached,
9257
- killProcess: ctx.killProcess,
9258
- now: ctx.now,
9259
- resolveMachineIdentity: ctx.resolveMachineIdentity,
9260
- runCompaction: ctx.runCompaction,
9261
- locateTranscript: ctx.locateTranscript,
9262
- },
9263
- throttleDeps: ctx.throttleDeps,
9264
- livenessDeps: livenessDepsFromCtx(ctx),
9265
- presenceConnected: ctx.presenceConnected,
9266
- resolveQuestionActiveCompany: () => resolveActiveQuestionCompany(ctx),
9267
- recoverQueuedMessages: async (lane) => {
9268
- await recoverQueuedMessages(ctx, lane.lane_id);
9269
- },
9270
- mesh: ctx.mesh,
9271
- env: ctx.env,
9272
- });
9273
- printJson(summary);
9274
- if (!summary.ok) {
9275
- process.exit(1);
9276
- }
9277
- }
9278
- async function runReconcileInstall(ctx, opts) {
9279
- // `hq daemon` runs lanes-reconcile on this machine; a separate timer would run it twice.
9280
- const handedOff = opts.systemdRoot ? null : handOffInstallHere(["lanes-reconcile"]);
9281
- if (handedOff) {
9282
- printJson({ ok: true, action: "hq-daemon", message: handedOff });
7922
+ async function runReconcileInstall(ctx, opts) {
7923
+ // `hq daemon` runs lanes-reconcile on this machine; a separate timer would run it twice.
7924
+ const handedOff = opts.systemdRoot ? null : handOffInstallHere(["lanes-reconcile"]);
7925
+ if (handedOff) {
7926
+ printJson({ ok: true, action: "hq-daemon", message: handedOff });
9283
7927
  return;
9284
7928
  }
9285
7929
  const callerCwd = ctx.callerCwd();
@@ -9336,6 +7980,53 @@ function attachJsonNoop(cmd) {
9336
7980
  }
9337
7981
  export function registerLanesCommand(parent, deps = {}) {
9338
7982
  const ctx = resolveDeps(deps);
7983
+ const accountHandlers = createLaneAccountsHandlers({
7984
+ automaticBalanceMoveHistoryReason,
7985
+ ensureDir,
7986
+ laneWithRecoveredProviderSession,
7987
+ loadClaudeBalanceHostSnapshots,
7988
+ parseRemoteJson,
7989
+ prepareAccountMove,
7990
+ printJson,
7991
+ reconcileHostFromCtx,
7992
+ resolveCallerPath,
7993
+ });
7994
+ const questionHandlers = createLaneQuestionsHandlers({
7995
+ CAPACITY_PROBE_TIMEOUT_MS,
7996
+ MAX_PLACEMENT_OUTPOSTS,
7997
+ OUTPOST_QUESTION_LOOKUP_CONCURRENCY,
7998
+ assertLaneRunsHereForOperatorMutation,
7999
+ assertQuestionCompanyMatchesLane,
8000
+ casUpdateQuestion,
8001
+ effectiveLaneQuestionCompany,
8002
+ emitLaneMeshEvent,
8003
+ groupQuestionLanesByCompany,
8004
+ isQuestionAnswer,
8005
+ laneIsHostedHere,
8006
+ livenessDepsFromCtx,
8007
+ loadAllLanes,
8008
+ loadAllQuestions,
8009
+ mapWithConcurrency,
8010
+ mintMessageId,
8011
+ parseOutpostQuestionAudience,
8012
+ parseOutpostQuestionBody,
8013
+ parseRemoteJson,
8014
+ parseSeniorRef,
8015
+ questionCompanyForLane,
8016
+ questionCompanyRefMatches,
8017
+ questionListCompanyArg,
8018
+ readOutpostQuestionRecords,
8019
+ readQuestionBody,
8020
+ printJson,
8021
+ readSessionMeta,
8022
+ reconcileHostFromCtx,
8023
+ resolveActiveQuestionCompany,
8024
+ resolveAudience,
8025
+ sameQuestionAnswer,
8026
+ sameQuestionCompany,
8027
+ seniorOfSession,
8028
+ writeAnswerDeliveryFailed,
8029
+ });
9339
8030
  const lanes = parent
9340
8031
  .command("lanes", { hidden: true })
9341
8032
  .description("Create, supervise, and steer worker lanes; routed decision queue");
@@ -9370,7 +8061,7 @@ export function registerLanesCommand(parent, deps = {}) {
9370
8061
  trackerEnabled: ctx.laneTrackerEnabled,
9371
8062
  now: ctx.now,
9372
8063
  readQuestionBody: (root, question) => {
9373
- const body = requireQuestionBody(root, question);
8064
+ const body = questionHandlers.requireQuestionBody(root, question);
9374
8065
  assertQuestionBodySecretSafe(body);
9375
8066
  return body;
9376
8067
  },
@@ -9743,14 +8434,14 @@ export function registerLanesCommand(parent, deps = {}) {
9743
8434
  .option("--story <id>", "Filter by story")
9744
8435
  .option("--status <status>", `Filter by status (${QUESTION_STATUSES.join("|")})`)
9745
8436
  .action(wrap(async (opts) => {
9746
- await runQuestionsList(ctx, opts);
8437
+ await questionHandlers.runQuestionsList(ctx, opts);
9747
8438
  }));
9748
8439
  questions
9749
8440
  .command("show")
9750
8441
  .description("Show one question")
9751
8442
  .argument("<question-id>", "Question id")
9752
8443
  .action(wrap(async (questionId) => {
9753
- await runQuestionsShow(ctx, questionId);
8444
+ await questionHandlers.runQuestionsShow(ctx, questionId);
9754
8445
  }));
9755
8446
  questions
9756
8447
  .command("answer")
@@ -9761,7 +8452,7 @@ export function registerLanesCommand(parent, deps = {}) {
9761
8452
  .option("--freeform <text>", "Supply a free-form answer for a question with options")
9762
8453
  .option("--company <slug|uid>", "Require the question lane to match this company")
9763
8454
  .action(wrap(async (questionId, opts) => {
9764
- await runQuestionsAnswer(ctx, questionId, opts);
8455
+ await questionHandlers.runQuestionsAnswer(ctx, questionId, opts);
9765
8456
  }));
9766
8457
  questions
9767
8458
  .command("escalate")
@@ -9769,7 +8460,7 @@ export function registerLanesCommand(parent, deps = {}) {
9769
8460
  .argument("<question-id>", "Question id")
9770
8461
  .option("--note <text>", "Note recorded with the escalation")
9771
8462
  .action(wrap(async (questionId, opts) => {
9772
- await runQuestionsEscalate(ctx, questionId, opts);
8463
+ await questionHandlers.runQuestionsEscalate(ctx, questionId, opts);
9773
8464
  }));
9774
8465
  questions
9775
8466
  .command("cancel")
@@ -9777,7 +8468,7 @@ export function registerLanesCommand(parent, deps = {}) {
9777
8468
  .argument("<question-id>", "Question id")
9778
8469
  .option("--reason <text>", "Why it was cancelled")
9779
8470
  .action(wrap(async (questionId, opts) => {
9780
- await runQuestionsCancel(ctx, questionId, opts);
8471
+ await questionHandlers.runQuestionsCancel(ctx, questionId, opts);
9781
8472
  }));
9782
8473
  const accounts = lanes
9783
8474
  .command("accounts")
@@ -9787,7 +8478,7 @@ export function registerLanesCommand(parent, deps = {}) {
9787
8478
  .description("List accounts")
9788
8479
  .option("--provider <name>", "claude | codex | grok")
9789
8480
  .action(wrap(async (opts) => {
9790
- await runAccountsList(ctx, opts);
8481
+ await accountHandlers.runAccountsList(ctx, opts);
9791
8482
  }));
9792
8483
  accounts
9793
8484
  .command("balance")
@@ -9796,7 +8487,7 @@ export function registerLanesCommand(parent, deps = {}) {
9796
8487
  .option("--apply", "Move eligible between-turn sessions to balance Claude accounts")
9797
8488
  .option("--json", "JSON on stdout (always)")
9798
8489
  .action(wrap(async (opts) => {
9799
- await runAccountsBalance(ctx, opts);
8490
+ await accountHandlers.runAccountsBalance(ctx, opts);
9800
8491
  }));
9801
8492
  accounts
9802
8493
  .command("set-home")
@@ -9808,7 +8499,7 @@ export function registerLanesCommand(parent, deps = {}) {
9808
8499
  .option("--company <slug|uid>", "Company scope for account-home validation")
9809
8500
  .option("--json", "JSON on stdout (always)")
9810
8501
  .action(wrap(async (provider, accountId, home, opts) => {
9811
- await runAccountsSetHome(ctx, provider, accountId, home, opts);
8502
+ await accountHandlers.runAccountsSetHome(ctx, provider, accountId, home, opts);
9812
8503
  }));
9813
8504
  accounts
9814
8505
  .command("login")
@@ -9819,7 +8510,7 @@ export function registerLanesCommand(parent, deps = {}) {
9819
8510
  .option("--company <slug|uid>", "Company scope for account-home validation")
9820
8511
  .option("--device-auth", "Use Codex device authentication instead of localhost callback")
9821
8512
  .action(wrap(async (provider, accountId, opts) => {
9822
- await runAccountsLogin(ctx, provider, accountId, opts);
8513
+ await accountHandlers.runAccountsLogin(ctx, provider, accountId, opts);
9823
8514
  }));
9824
8515
  accounts
9825
8516
  .command("enable")
@@ -9827,7 +8518,7 @@ export function registerLanesCommand(parent, deps = {}) {
9827
8518
  .argument("<provider>", "claude | codex | grok")
9828
8519
  .argument("<account-id>", "Account id")
9829
8520
  .action(wrap(async (provider, accountId) => {
9830
- await runAccountsEnable(ctx, provider, accountId);
8521
+ await accountHandlers.runAccountsEnable(ctx, provider, accountId);
9831
8522
  }));
9832
8523
  accounts
9833
8524
  .command("disable")
@@ -9836,7 +8527,7 @@ export function registerLanesCommand(parent, deps = {}) {
9836
8527
  .argument("<account-id>", "Account id")
9837
8528
  .option("--reason <text>", "Why the account is disabled")
9838
8529
  .action(wrap(async (provider, accountId, opts) => {
9839
- await runAccountsDisable(ctx, provider, accountId, opts);
8530
+ await accountHandlers.runAccountsDisable(ctx, provider, accountId, opts);
9840
8531
  }));
9841
8532
  const sessions = lanes
9842
8533
  .command("sessions")
@@ -9847,14 +8538,14 @@ export function registerLanesCommand(parent, deps = {}) {
9847
8538
  .option("--state <state>", `live | idle | ended | lost`)
9848
8539
  .option("--company <slug>", "Filter by session meta company_slug")
9849
8540
  .action(wrap(async (opts) => {
9850
- await runSessionsList(ctx, opts);
8541
+ await questionHandlers.runSessionsList(ctx, opts);
9851
8542
  }));
9852
8543
  sessions
9853
8544
  .command("show")
9854
8545
  .description("Show one session's liveness")
9855
8546
  .argument("<session-id>", "Session id")
9856
8547
  .action(wrap(async (sessionId) => {
9857
- await runSessionsShow(ctx, sessionId);
8548
+ await questionHandlers.runSessionsShow(ctx, sessionId);
9858
8549
  }));
9859
8550
  lanes
9860
8551
  .command("pickup", { hidden: true })