@indigoai-us/hq-cli 5.241.0 → 5.243.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.
@@ -10,8 +10,10 @@ import { randomBytes } from "node:crypto";
10
10
  import * as fs from "node:fs";
11
11
  import * as os from "node:os";
12
12
  import * as path from "node:path";
13
- import { readLocalOverride } from "@indigoai-us/hq-flags-client";
14
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";
15
17
  import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
16
18
  import { registerLaneChecksCommand } from "./lanes-checks.js";
17
19
  import { registerLaneReportCommands } from "./lanes-report.js";
@@ -27,10 +29,10 @@ import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } fr
27
29
  import { createOutpostRegistryWorkerApprovalContext, parseOutpostRegistryWorkerApprovalContext, } from "../lib/lanes/outpost-registry-worker-approval.js";
28
30
  import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
29
31
  export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
30
- 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";
31
33
  import { outpostHireSummaryCacheKey, readOutpostHireSummaryCache, writeOutpostHireSummaryCache, } from "../lib/lanes/outpost-hire-cache.js";
32
34
  import { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "../lib/lanes/host-capacity.js";
33
- 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";
34
36
  import { parseUserYaml } from "../utils/user-yaml-error.js";
35
37
  import { backfillOutpostIdentityMarker as backfillOutpostIdentityMarkerImpl, createOutpostIdentityMarkerIfMissing, validateOutpostIdentityId, } from "../lib/lanes/outpost-identity.js";
36
38
  import { vaultApiFetch } from "../utils/vault-api.js";
@@ -41,21 +43,20 @@ import { assertNoSecretText } from "../lib/lanes/text-scan.js";
41
43
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
42
44
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
43
45
  import { effectiveAccountHome } from "../lib/lanes/account-home.js";
44
- 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";
45
47
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
46
48
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
47
49
  import { keepAliveExclusion, LEAD_LANE_KEEP_ALIVE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
48
50
  import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
49
51
  import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
50
52
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
51
- import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
52
53
  import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
53
54
  import { LanesError } from "../lib/lanes/errors.js";
54
55
  import { LANE_WAIT_INVALID_INPUT_EXIT_CODE, LANE_WAIT_TIMEOUT_EXIT_CODE, runLaneWait, } from "../lib/lanes/wait.js";
55
56
  import { dispatchLaneDelivery } from "../lib/lanes/delivery-core.js";
56
57
  import { dropboxInboxHasMessages, drainDropbox, formatDropboxMessages, formatMonitorEvent, requeueDropboxMessages, } from "../lib/lanes/dropbox.js";
57
58
  import { isPidAlive, runWatcherLoop, WATCHER_OUT_MAX_BYTES, watcherOutLogPath, writeWatcherDecision, } from "../lib/lanes/watcher.js";
58
- import { accountQuotaView, loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
59
+ import { loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
59
60
  import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
60
61
  import { ensureSessionHome } from "../lib/lanes/session-relink.js";
61
62
  import { defaultProbe, validateProfileLaneModel, validateLaneModel, } from "../lib/lanes/models.js";
@@ -65,43 +66,15 @@ export { spawnLaneWorker };
65
66
  export { questionBodyAbsPath };
66
67
  export { MAX_LANE_DEPTH };
67
68
  export const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
68
- const VENDOR_LOGIN = {
69
- claude: { command: "claude", args: ["auth", "login"] },
70
- codex: { command: "codex", args: ["login"] },
71
- grok: { command: "grok", args: ["login"] },
72
- };
69
+ /** The lane → mesh seam. Defined in lib/lanes/mesh-events.ts; the watcher shares it. */
73
70
  export { emitLaneDecisionEvents };
74
71
  export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
75
- export const LANE_OUTPOST_QUESTION_VISIBILITY_FLAG = "lanes.outpost-question-visibility";
76
- export const LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG = "lanes.host-scoped-account-homes";
77
72
  function defaultLanePlacementEnabled() {
78
73
  return resolveProcessFlagGate(LANE_OUTPOST_LOAD_BALANCING_FLAG, { fallback: false }, () => false);
79
74
  }
80
75
  function defaultLaneTrackerEnabled(company) {
81
76
  return readLanesRegistryFlag(LANES_TRACKER_FLAG, { company });
82
77
  }
83
- function defaultLaneOutpostQuestionVisibilityEnabled(company, env) {
84
- return readLanesRegistryFlag(LANE_OUTPOST_QUESTION_VISIBILITY_FLAG, {
85
- company,
86
- env,
87
- });
88
- }
89
- function defaultLaneHostScopedAccountHomesEnabled(company, env) {
90
- return readLanesRegistryFlag(LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG, { company, env });
91
- }
92
- async function hostScopedAccountHomesEnabled(ctx, company) {
93
- const localOverride = readLocalOverride(LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG, ctx.env);
94
- if (localOverride !== null)
95
- return localOverride;
96
- return ctx.laneHostScopedAccountHomesEnabled(company ?? await ctx.resolveCompany());
97
- }
98
- async function resolveSelectedAccountHomeCompany(ctx, selector) {
99
- const company = await ctx.resolveActiveCompany(selector);
100
- if (!company) {
101
- throw new CompanySelectionError(`No unique active company membership for ${selector}. Pass an active company UID with --company.`);
102
- }
103
- return company;
104
- }
105
78
  function withoutProviderHome(value) {
106
79
  if (Array.isArray(value))
107
80
  return value.map(withoutProviderHome);
@@ -312,11 +285,7 @@ function resolveDeps(deps = {}) {
312
285
  storyLedgerEnabled: deps.storyLedgerEnabled ?? laneStoryLedgerEnabled,
313
286
  isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
314
287
  withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
315
- laneOutpostQuestionVisibilityEnabled: deps.laneOutpostQuestionVisibilityEnabled ??
316
- ((company) => defaultLaneOutpostQuestionVisibilityEnabled(company, env)),
317
288
  laneTrackerEnabled: deps.laneTrackerEnabled ?? defaultLaneTrackerEnabled,
318
- laneHostScopedAccountHomesEnabled: deps.laneHostScopedAccountHomesEnabled ??
319
- ((company) => defaultLaneHostScopedAccountHomesEnabled(company, env)),
320
289
  trackerActor: deps.trackerActor ?? defaultTrackerActor,
321
290
  };
322
291
  }
@@ -376,21 +345,6 @@ function parseHireGrant(raw) {
376
345
  function seniorKey(ref) {
377
346
  return ref.kind === "user" ? "user" : `${ref.kind}:${ref.id ?? ""}`;
378
347
  }
379
- export function resolveAnswerChoice(choice, options, freeform) {
380
- const trimmed = choice.trim();
381
- if (/^[0-9]+$/.test(trimmed)) {
382
- const index = Number(trimmed);
383
- if (Number.isInteger(index) && index >= 1 && index <= options.length) {
384
- return options[index - 1];
385
- }
386
- }
387
- if (options.includes(trimmed) || options.includes(choice)) {
388
- return options.includes(trimmed) ? trimmed : choice;
389
- }
390
- if (freeform)
391
- return choice;
392
- throw new LanesError("off_menu_choice", `Choice ${JSON.stringify(choice)} is not on the menu. Pass --freeform to allow it.`, { choice, options });
393
- }
394
348
  function parsePriority(raw) {
395
349
  if (raw === undefined || raw === null || raw === "")
396
350
  return 1;
@@ -493,17 +447,6 @@ function readQuestionBody(hqRoot, question) {
493
447
  }
494
448
  return stored.body;
495
449
  }
496
- function requireQuestionBody(hqRoot, question) {
497
- const body = readQuestionBody(hqRoot, question);
498
- if (!body) {
499
- throw new LanesError("question_body_unavailable", `Question body is not available on this host for ${question.question_id}`, {
500
- question_id: question.question_id,
501
- store: question.body.store,
502
- ref: question.body.ref,
503
- });
504
- }
505
- return body;
506
- }
507
450
  function loadAllLanes(hqRoot) {
508
451
  return listLaneIds(hqRoot).map((id) => readLane(hqRoot, id));
509
452
  }
@@ -1686,18 +1629,6 @@ function assertQuestionCompanyMatchesLane(question, lane, code, hqRoot) {
1686
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 });
1687
1630
  }
1688
1631
  }
1689
- function assertQuestionCompanyAccess(question, lane, requestedCompany, hqRoot) {
1690
- assertQuestionCompanyMatchesLane(question, lane, "question_company_mismatch", hqRoot);
1691
- const questionCompany = questionCompanyForLane(question, lane, hqRoot);
1692
- if (questionCompany !== null && !sameQuestionCompany(questionCompany, requestedCompany)) {
1693
- 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 });
1694
- }
1695
- }
1696
- function assertQuestionCompanyResolvedForAnswer(question, lane, hqRoot, resolvedCompany) {
1697
- if (questionCompanyForLane(question, lane, hqRoot) !== null || resolvedCompany)
1698
- return;
1699
- 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 });
1700
- }
1701
1632
  function questionCompanyRefMatches(recordCompany, filter) {
1702
1633
  return recordCompany !== null &&
1703
1634
  recordCompany !== undefined &&
@@ -1756,25 +1687,6 @@ function resolveAudience(hqRoot, question, ctx) {
1756
1687
  }
1757
1688
  return current;
1758
1689
  }
1759
- function audienceMatches(audience, filter) {
1760
- if (filter.kind === "me") {
1761
- return audience.kind === "session" && audience.id === filter.id;
1762
- }
1763
- if (filter.kind === "user")
1764
- return audience.kind === "user";
1765
- return audience.kind === filter.kind && audience.id === filter.id;
1766
- }
1767
- function parseForFilter(raw, ctx) {
1768
- const trimmed = raw.trim();
1769
- if (trimmed === "me") {
1770
- const sessionId = sessionIdFromEnv(ctx.env);
1771
- if (!sessionId) {
1772
- throw new LanesError("missing_session_id", "--for me requires a current session id (HQ_SESSION_ID)", {});
1773
- }
1774
- return { kind: "me", id: sessionId };
1775
- }
1776
- return parseSeniorRef(trimmed);
1777
- }
1778
1690
  function timeoutPath(hqRoot, laneId) {
1779
1691
  return path.join(runDir(hqRoot, laneId), "timeout.json");
1780
1692
  }
@@ -1938,16 +1850,6 @@ function assertKnownOutpost(requested, known) {
1938
1850
  throw new LanesError("unknown_outpost", `Unknown Outpost ${JSON.stringify(requested)}. Accepted: ${known.join(", ") || "(none)"}`, { accepted: known, value: requested });
1939
1851
  }
1940
1852
  }
1941
- function copyHookRails(fromHome, toHome) {
1942
- const src = path.join(fromHome, "hooks");
1943
- if (!fs.existsSync(src))
1944
- return false;
1945
- fs.cpSync(src, path.join(toHome, "hooks"), { recursive: true });
1946
- return true;
1947
- }
1948
- function railsPresent(home) {
1949
- return fs.existsSync(path.join(home, "hooks"));
1950
- }
1951
1853
  /**
1952
1854
  * Spec §5.1: `hq lanes ask` from inside a lane can hit a binary or dist
1953
1855
  * being replaced mid-call (ENOENT / ETXTBSY / a module that no longer
@@ -1956,11 +1858,6 @@ function railsPresent(home) {
1956
1858
  */
1957
1859
  export const ASK_BACKOFF_MS = [5_000, 15_000];
1958
1860
  export const ASK_ATTEMPTS = ASK_BACKOFF_MS.length + 1;
1959
- function yieldEventLoop() {
1960
- return new Promise((resolve) => {
1961
- setImmediate(resolve);
1962
- });
1963
- }
1964
1861
  function laneWaitHumanLine(result) {
1965
1862
  const details = Object.entries(result.change)
1966
1863
  .filter(([key]) => key !== "type")
@@ -2018,11 +1915,6 @@ export function casUpdateLane(hqRoot, laneId, updater) {
2018
1915
  return updateLane(hqRoot, laneId, current.version, "command", updater);
2019
1916
  });
2020
1917
  }
2021
- function throwIfQuestionNotPending(current) {
2022
- if (current.status === "pending")
2023
- return;
2024
- throw new LanesError("question_not_pending", `Question ${current.question_id} is ${current.status}`, { question_id: current.question_id, status: current.status });
2025
- }
2026
1918
  function formatUnknownValue(value) {
2027
1919
  if (typeof value === "string")
2028
1920
  return value;
@@ -3946,159 +3838,6 @@ async function readOutpostLaneSnapshot(ctx, outpostId, laneId) {
3946
3838
  }
3947
3839
  return remote.lane;
3948
3840
  }
3949
- async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
3950
- if (lane.host.kind !== "outpost") {
3951
- 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 });
3952
- }
3953
- const outpostId = lane.host.id;
3954
- let dispatched;
3955
- try {
3956
- dispatched = await ctx.dispatchOutpostExec({
3957
- outpostId,
3958
- command: buildOutpostQuestionAnswerCommand({
3959
- questionId,
3960
- choice: options.choice,
3961
- freeform: options.freeform,
3962
- notes: options.notes,
3963
- company: options.company,
3964
- }),
3965
- });
3966
- }
3967
- catch (err) {
3968
- 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 });
3969
- }
3970
- if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
3971
- throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} did not confirm the answer for question ${questionId}`, {
3972
- lane_id: lane.lane_id,
3973
- question_id: questionId,
3974
- outpost_id: outpostId,
3975
- command_id: dispatched.commandId,
3976
- status: dispatched.status,
3977
- done: dispatched.done ?? false,
3978
- exit_code: dispatched.exitCode,
3979
- });
3980
- }
3981
- const remote = parseRemoteJson(dispatched.stdout, "Outpost question answer");
3982
- if (remote?.error !== undefined) {
3983
- throw new LanesError("outpost_remote_command_failed", typeof remote.message === "string"
3984
- ? remote.message
3985
- : `Outpost ${outpostId} rejected the answer for question ${questionId}`, {
3986
- lane_id: lane.lane_id,
3987
- question_id: questionId,
3988
- outpost_id: outpostId,
3989
- command_id: dispatched.commandId,
3990
- remote_error: remote.error,
3991
- });
3992
- }
3993
- const remoteQuestion = remote?.question !== null && typeof remote?.question === "object"
3994
- ? remote.question
3995
- : null;
3996
- const awaitingWakeConfirmed = lane.state !== "awaiting_input" ||
3997
- (remote?.wake_status === "resumed" && remote.lane_state === "running") ||
3998
- (remote?.wake_status === "queued" && remote.lane_state === "queued") ||
3999
- (remote?.wake_status === "awaiting_answers" && remote.lane_state === "awaiting_input");
4000
- if (remote?.ok !== true ||
4001
- remote.action !== "questions.answer" ||
4002
- remoteQuestion?.question_id !== questionId ||
4003
- remoteQuestion.lane_id !== lane.lane_id ||
4004
- remoteQuestion.status !== "answered" ||
4005
- typeof remote.delivery !== "string" ||
4006
- !awaitingWakeConfirmed) {
4007
- throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without confirming the answer and wake status for question ${questionId}`, {
4008
- lane_id: lane.lane_id,
4009
- question_id: questionId,
4010
- outpost_id: outpostId,
4011
- command_id: dispatched.commandId,
4012
- });
4013
- }
4014
- let ownerQuestion;
4015
- try {
4016
- ownerQuestion = parseQuestionRecord(remoteQuestion);
4017
- }
4018
- catch (err) {
4019
- throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return a valid answered question record for ${questionId}: ${dispatchErrorMessage(err)}`, {
4020
- lane_id: lane.lane_id,
4021
- question_id: questionId,
4022
- outpost_id: outpostId,
4023
- command_id: dispatched.commandId,
4024
- });
4025
- }
4026
- const ownerAnswer = ownerQuestion.answer;
4027
- const ownerLaneCompany = effectiveLaneQuestionCompany(lane, ctx.hqRoot());
4028
- if (!isQuestionAnswer(ownerAnswer) ||
4029
- ownerQuestion.lane_id !== lane.lane_id ||
4030
- (ownerQuestion.company !== null && ownerLaneCompany !== null &&
4031
- !sameQuestionCompany(ownerQuestion.company, ownerLaneCompany))) {
4032
- throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return the confirmed answer for question ${questionId}`, {
4033
- lane_id: lane.lane_id,
4034
- question_id: questionId,
4035
- outpost_id: outpostId,
4036
- command_id: dispatched.commandId,
4037
- });
4038
- }
4039
- const updatePrimaryCopy = () => casUpdateQuestion(ctx.hqRoot(), questionId, (current) => {
4040
- if (current.lane_id !== lane.lane_id) {
4041
- 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 });
4042
- }
4043
- if (current.company !== null &&
4044
- effectiveLaneQuestionCompany(lane, ctx.hqRoot()) !== null &&
4045
- !sameQuestionCompany(current.company, effectiveLaneQuestionCompany(lane, ctx.hqRoot()))) {
4046
- 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 });
4047
- }
4048
- if (current.company !== null &&
4049
- ownerQuestion.company !== null &&
4050
- !sameQuestionCompany(current.company, ownerQuestion.company)) {
4051
- 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 });
4052
- }
4053
- if (current.status === "pending") {
4054
- return {
4055
- ...current,
4056
- company: current.company ?? ownerQuestion.company,
4057
- status: "answered",
4058
- answer: ownerAnswer,
4059
- delivered_at: ownerQuestion.delivered_at ?? current.delivered_at ?? null,
4060
- };
4061
- }
4062
- if (current.status === "answered" && sameQuestionAnswer(current.answer, ownerAnswer)) {
4063
- const deliveredAt = ownerQuestion.delivered_at ?? current.delivered_at ?? null;
4064
- const company = current.company ?? ownerQuestion.company;
4065
- return deliveredAt === current.delivered_at && company === current.company
4066
- ? current
4067
- : { ...current, company, delivered_at: deliveredAt };
4068
- }
4069
- 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 });
4070
- });
4071
- let syncedQuestion;
4072
- try {
4073
- syncedQuestion = updatePrimaryCopy();
4074
- }
4075
- catch (err) {
4076
- if (!isLanesError(err) || err.error !== "question_not_found")
4077
- throw err;
4078
- const primaryLane = readLane(ctx.hqRoot(), lane.lane_id);
4079
- const primaryCompany = effectiveLaneQuestionCompany(primaryLane, ctx.hqRoot());
4080
- const ownerCompany = effectiveLaneQuestionCompany(lane, ctx.hqRoot());
4081
- if (primaryCompany !== null && ownerCompany !== null && !sameQuestionCompany(primaryCompany, ownerCompany)) {
4082
- 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 });
4083
- }
4084
- try {
4085
- syncedQuestion = createQuestion(ctx.hqRoot(), ownerQuestion);
4086
- }
4087
- catch (createErr) {
4088
- if (!isLanesError(createErr) || createErr.error !== "question_exists")
4089
- throw createErr;
4090
- syncedQuestion = updatePrimaryCopy();
4091
- }
4092
- }
4093
- printJson({
4094
- ...remote,
4095
- lane_id: lane.lane_id,
4096
- question: syncedQuestion,
4097
- outpost_id: outpostId,
4098
- outpost_command_id: dispatched.commandId,
4099
- outpost_transport: "outposts_exec",
4100
- });
4101
- }
4102
3841
  function isQuestionAnswer(value) {
4103
3842
  if (value === null || typeof value !== "object")
4104
3843
  return false;
@@ -6552,74 +6291,6 @@ function persistClaudeAccountSelection(hqRoot, lane, selection) {
6552
6291
  return { ...current, account_selection: selection };
6553
6292
  });
6554
6293
  }
6555
- async function applyClaudeBalanceMove(ctx, suggestion) {
6556
- const currentHost = reconcileHostFromCtx(ctx);
6557
- const suggestionRunsHere = suggestion.host.kind === "outpost"
6558
- ? currentHost.outpostId === suggestion.host.id
6559
- : currentHost.outpostId === null && currentHost.hostname === suggestion.host.id;
6560
- if (suggestion.host.kind === "outpost" && !suggestionRunsHere) {
6561
- let result;
6562
- try {
6563
- result = await ctx.dispatchOutpostExec({
6564
- outpostId: suggestion.host.id,
6565
- command: buildOutpostBalanceMoveCommand({
6566
- laneId: suggestion.lane_id,
6567
- account: suggestion.to,
6568
- fromAccount: suggestion.from,
6569
- expectedState: suggestion.state,
6570
- }),
6571
- timeoutSeconds: 120,
6572
- waitMs: 120_000,
6573
- });
6574
- }
6575
- catch (err) {
6576
- 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 });
6577
- }
6578
- if (result.done !== true || result.status !== "Success" || result.exitCode !== 0) {
6579
- 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 });
6580
- }
6581
- const remote = parseRemoteJson(result.stdout, "Claude account balance move");
6582
- if (remote?.ok !== true || remote.action !== "balance-move-target" ||
6583
- remote.lane_id !== suggestion.lane_id || remote.from !== suggestion.from ||
6584
- remote.account !== suggestion.to ||
6585
- remote.state !== suggestion.state) {
6586
- throw new LanesError("claude_account_balance_move_unconfirmed", `Outpost ${suggestion.host.id} returned an invalid account-move result`, {
6587
- lane_id: suggestion.lane_id,
6588
- host: suggestion.host,
6589
- account: suggestion.to,
6590
- });
6591
- }
6592
- return { lane_id: suggestion.lane_id, host: suggestion.host, from: suggestion.from, to: suggestion.to, state: suggestion.state };
6593
- }
6594
- const lane = laneWithRecoveredProviderSession(ctx.hqRoot(), readLane(ctx.hqRoot(), suggestion.lane_id));
6595
- if (lane.state === "running") {
6596
- throw new LanesError("lane_running", `Lane ${lane.lane_id} is running; balance never moves a running turn`, {
6597
- lane_id: lane.lane_id,
6598
- state: lane.state,
6599
- });
6600
- }
6601
- if (lane.host.kind !== suggestion.host.kind || lane.host.id !== suggestion.host.id ||
6602
- lane.account !== suggestion.from || lane.state !== suggestion.state) {
6603
- throw new LanesError("claude_account_balance_lane_changed", `Lane ${lane.lane_id} changed after balance was calculated`, {
6604
- lane_id: lane.lane_id,
6605
- host: lane.host,
6606
- account: lane.account ?? null,
6607
- });
6608
- }
6609
- if (!BALANCE_MOVABLE_STATES.includes(lane.state)) {
6610
- throw new LanesError("claude_account_balance_state_not_movable", `Lane ${lane.lane_id} is no longer between turns`, {
6611
- lane_id: lane.lane_id,
6612
- state: lane.state,
6613
- });
6614
- }
6615
- const moved = await prepareAccountMove(ctx, lane, {
6616
- account: suggestion.to,
6617
- cliModel: undefined,
6618
- accountHistoryReason: automaticBalanceMoveHistoryReason(ctx, lane),
6619
- deferTurnStart: true,
6620
- });
6621
- return { lane_id: moved.lane.lane_id, host: moved.lane.host, from: moved.from ?? suggestion.from, to: moved.to, state: moved.lane.state };
6622
- }
6623
6294
  async function runClaudeBalanceMoveTarget(ctx, laneId, opts) {
6624
6295
  const lane = laneWithRecoveredProviderSession(ctx.hqRoot(), readLane(ctx.hqRoot(), laneId));
6625
6296
  assertLaneRunsHereForOperatorMutation(ctx, lane, "balance-move-target");
@@ -8189,1136 +7860,70 @@ function parseOutpostQuestionBody(raw, outpostId, questionId) {
8189
7860
  }
8190
7861
  return parseQuestionBody(raw);
8191
7862
  }
8192
- async function readOutpostQuestionDetail(ctx, owner) {
8193
- if (owner.lane.host.kind !== "outpost") {
8194
- 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 });
8195
- }
8196
- const outpostId = owner.lane.host.id;
8197
- let result;
8198
- try {
8199
- result = await ctx.dispatchOutpostExec({
8200
- outpostId,
8201
- command: buildOutpostQuestionShowCommand(owner.question.question_id),
8202
- timeoutSeconds: CAPACITY_PROBE_TIMEOUT_MS / 1000,
8203
- waitMs: CAPACITY_PROBE_TIMEOUT_MS,
8204
- });
8205
- }
8206
- catch {
8207
- 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 });
8208
- }
8209
- if (result.done !== true || result.status !== "Success" || result.exitCode !== 0) {
8210
- 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 });
8211
- }
8212
- let raw;
8213
- try {
8214
- raw = JSON.parse(result.stdout);
8215
- }
8216
- catch {
8217
- throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned invalid question-show JSON`, { outpost_id: outpostId, question_id: owner.question.question_id });
8218
- }
8219
- if (raw === null ||
8220
- typeof raw !== "object" ||
8221
- Array.isArray(raw) ||
8222
- raw.ok !== true ||
8223
- raw.action !== "questions.show" ||
8224
- !Object.prototype.hasOwnProperty.call(raw, "body")) {
8225
- throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned a malformed question-show response`, { outpost_id: outpostId, question_id: owner.question.question_id });
8226
- }
8227
- const response = raw;
8228
- let question;
8229
- try {
8230
- question = parseQuestionRecord(response.question);
8231
- }
8232
- catch {
8233
- throw new LanesError("outpost_question_show_invalid", `Outpost ${outpostId} returned an invalid question record`, { outpost_id: outpostId, question_id: owner.question.question_id });
8234
- }
8235
- if (question.question_id !== owner.question.question_id ||
8236
- question.lane_id !== owner.lane.lane_id) {
8237
- 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 });
8238
- }
8239
- assertQuestionCompanyMatchesLane(question, owner.lane, "outpost_question_company_mismatch", ctx.hqRoot());
8240
- const audience = parseOutpostQuestionAudience(response.audience, outpostId, 0);
8241
- const body = parseOutpostQuestionBody(response.body, outpostId, question.question_id);
8242
- return { question, audience, body };
8243
- }
8244
- async function loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, companyFilter) {
8245
- const hostedLanes = loadAllLanes(hqRoot).filter((lane) => lane.host.kind === "outpost" &&
8246
- (!companyFilter ||
8247
- effectiveLaneQuestionCompany(lane, hqRoot) === null ||
8248
- questionCompanyRefMatches(effectiveLaneQuestionCompany(lane, hqRoot), companyFilter)));
8249
- const lanesByOutpost = new Map();
8250
- for (const lane of hostedLanes) {
8251
- if (lane.host.kind !== "outpost")
8252
- continue;
8253
- const lanes = lanesByOutpost.get(lane.host.id) ?? [];
8254
- lanes.push(lane);
8255
- lanesByOutpost.set(lane.host.id, lanes);
8256
- }
8257
- const questions = [];
8258
- const errors = [];
8259
- const eligibleGroups = [];
8260
- for (const [outpostId, lanes] of [...lanesByOutpost.entries()].sort(([a], [b]) => a.localeCompare(b))) {
8261
- for (const group of groupQuestionLanesByCompany(hqRoot, lanes, companyFilter)) {
8262
- if (group.company === null) {
8263
- if (await ctx.laneOutpostQuestionVisibilityEnabled(null)) {
8264
- errors.push({
8265
- host: `outpost:${outpostId}`,
8266
- error: "outpost_question_company_unavailable",
8267
- });
8268
- }
8269
- continue;
8270
- }
8271
- if (companyFilter && !questionCompanyRefMatches(group.company, companyFilter))
8272
- continue;
8273
- if (await ctx.laneOutpostQuestionVisibilityEnabled(group.company)) {
8274
- eligibleGroups.push({ outpostId, group });
8275
- }
8276
- }
8277
- }
8278
- if (eligibleGroups.length === 0) {
8279
- return { questions, errors: [...new Map(errors.map((entry) => [entry.host, entry])).values()] };
8280
- }
8281
- 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
+ };
8282
7873
  try {
8283
- knownOutpostIds = await ctx.listOutpostIds();
8284
- }
8285
- catch {
8286
- return {
8287
- questions,
8288
- errors: [...new Map([
8289
- ...errors,
8290
- ...[...new Set(eligibleGroups.map(({ outpostId }) => outpostId))].map((outpostId) => ({
8291
- host: `outpost:${outpostId}`,
8292
- error: "outpost_list_unavailable",
8293
- })),
8294
- ].map((entry) => [entry.host, entry])).values()],
8295
- };
7874
+ writeJsonFile(askDeliveryFailedPath(hqRoot, laneId), failure);
8296
7875
  }
8297
- const normalizedOutpostIds = knownOutpostIds.map((id) => typeof id === "string" ? id.trim() : "");
8298
- if (knownOutpostIds.length > MAX_PLACEMENT_OUTPOSTS ||
8299
- normalizedOutpostIds.some((id) => id === "") ||
8300
- new Set(normalizedOutpostIds).size !== normalizedOutpostIds.length) {
8301
- 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`);
8302
7878
  }
8303
- const knownOutpostIdSet = new Set(normalizedOutpostIds);
8304
- const loadedGroups = await mapWithConcurrency(eligibleGroups, OUTPOST_QUESTION_LOOKUP_CONCURRENCY, async ({ outpostId, group }) => {
8305
- if (!knownOutpostIdSet.has(outpostId)) {
8306
- return {
8307
- questions: [],
8308
- errors: [{ host: `outpost:${outpostId}`, error: "unknown_outpost" }],
8309
- };
8310
- }
8311
- try {
8312
- const remoteQuestions = await readOutpostQuestionRecords(ctx, outpostId, group.company);
8313
- const localLanesById = new Map(group.lanes.map((lane) => [lane.lane_id, lane]));
8314
- const groupQuestions = [];
8315
- const seenQuestionIds = new Set();
8316
- for (const { question, audience } of remoteQuestions) {
8317
- const lane = localLanesById.get(question.lane_id);
8318
- if (!lane)
8319
- continue;
8320
- assertQuestionCompanyMatchesLane(question, lane, "outpost_question_company_mismatch", hqRoot);
8321
- if (seenQuestionIds.has(question.question_id)) {
8322
- throw new LanesError("outpost_question_list_invalid", `Outpost ${outpostId} returned a duplicate question id`, { outpost_id: outpostId, question_id: question.question_id });
8323
- }
8324
- seenQuestionIds.add(question.question_id);
8325
- groupQuestions.push({
8326
- question,
8327
- lane,
8328
- host: `outpost:${outpostId}`,
8329
- audience,
8330
- });
8331
- }
8332
- return { questions: groupQuestions, errors: [] };
8333
- }
8334
- catch (err) {
8335
- return {
8336
- questions: [],
8337
- errors: [{
8338
- host: `outpost:${outpostId}`,
8339
- error: isLanesError(err) ? err.error : "outpost_question_list_unavailable",
8340
- }],
8341
- };
8342
- }
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,
8343
7916
  });
8344
- questions.push(...loadedGroups.flatMap(({ questions: groupQuestions }) => groupQuestions));
8345
- errors.push(...loadedGroups.flatMap(({ errors: groupErrors }) => groupErrors));
8346
- return {
8347
- questions,
8348
- errors: [...new Map(errors.map((entry) => [entry.host, entry])).values()],
8349
- };
7917
+ printJson(summary);
7918
+ if (!summary.ok) {
7919
+ process.exit(1);
7920
+ }
8350
7921
  }
8351
- function syncPrimaryQuestionDelivery(hqRoot, owner) {
8352
- const deliveredAt = owner.question.delivered_at;
8353
- if (!deliveredAt)
8354
- return;
8355
- try {
8356
- casUpdateQuestion(hqRoot, owner.question.question_id, (current) => {
8357
- if (current.lane_id !== owner.lane.lane_id) {
8358
- 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 });
8359
- }
8360
- if (current.company !== null &&
8361
- owner.question.company !== null &&
8362
- !sameQuestionCompany(current.company, owner.question.company)) {
8363
- 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 });
8364
- }
8365
- const company = current.company ?? owner.question.company;
8366
- const nextDeliveredAt = current.delivered_at ?? deliveredAt;
8367
- return company === current.company && nextDeliveredAt === current.delivered_at
8368
- ? current
8369
- : { ...current, company, delivered_at: nextDeliveredAt };
8370
- });
8371
- }
8372
- catch (err) {
8373
- if (isLanesError(err) && err.error === "question_not_found")
8374
- return;
8375
- throw err;
8376
- }
8377
- }
8378
- async function runQuestionsList(ctx, opts) {
8379
- const hqRoot = ctx.hqRoot();
8380
- const status = opts.status ? parseQuestionStatus(opts.status) : undefined;
8381
- const forFilter = opts.for ? parseForFilter(opts.for, ctx) : undefined;
8382
- const company = opts.company
8383
- ? await ctx.resolveCompany(opts.company)
8384
- : await resolveActiveQuestionCompany(ctx, "Cannot access lane questions because the active HQ company could not be resolved; re-run with --company <slug>");
8385
- const localLanes = new Map(loadAllLanes(hqRoot).map((lane) => [lane.lane_id, lane]));
8386
- const targeted = opts.from !== undefined || opts.project !== undefined || opts.story !== undefined;
8387
- const localQuestions = loadAllQuestions(hqRoot)
8388
- .filter((q) => {
8389
- if (status && q.status !== status)
8390
- return false;
8391
- if (opts.from && q.lane_id !== opts.from && q.origin.id !== opts.from)
8392
- return false;
8393
- if (opts.project && q.project_id !== opts.project)
8394
- return false;
8395
- if (opts.story && q.story_id !== opts.story)
8396
- return false;
8397
- const lane = localLanes.get(q.lane_id);
8398
- if (lane)
8399
- assertQuestionCompanyMatchesLane(q, lane, "question_company_mismatch", hqRoot);
8400
- const questionCompany = lane ? questionCompanyForLane(q, lane, hqRoot) : q.company;
8401
- if (questionCompany !== null && !sameQuestionCompany(questionCompany, company)) {
8402
- if (targeted) {
8403
- 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 });
8404
- }
8405
- return false;
8406
- }
8407
- if (forFilter) {
8408
- const audience = resolveAudience(hqRoot, q, ctx);
8409
- if (!audienceMatches(audience, forFilter))
8410
- return false;
8411
- }
8412
- return true;
8413
- });
8414
- const outpostLoad = opts.allHosts
8415
- ? await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company)
8416
- : { questions: [], errors: [] };
8417
- for (const owner of outpostLoad.questions) {
8418
- assertQuestionCompanyAccess(owner.question, owner.lane, company, hqRoot);
8419
- syncPrimaryQuestionDelivery(hqRoot, owner);
8420
- }
8421
- const remoteQuestionIds = new Set(outpostLoad.questions.map(({ question }) => question.question_id));
8422
- const questions = [
8423
- ...localQuestions
8424
- .filter((q) => !remoteQuestionIds.has(q.question_id))
8425
- .map((q) => {
8426
- const row = {
8427
- ...q,
8428
- audience: resolveAudience(hqRoot, q, ctx),
8429
- ...(q.question_kind === "continue" ? { flag: "continue-loop" } : {}),
8430
- };
8431
- if (!opts.allHosts)
8432
- return row;
8433
- const lane = localLanes.get(q.lane_id);
8434
- const host = lane?.host.kind === "outpost" ? `outpost:${lane.host.id}` : "local";
8435
- return { ...row, host };
8436
- }),
8437
- ...outpostLoad.questions
8438
- .filter(({ question: q, lane, audience }) => {
8439
- if (status && q.status !== status)
8440
- return false;
8441
- if (opts.from && q.lane_id !== opts.from && q.origin.id !== opts.from)
8442
- return false;
8443
- if (opts.project && q.project_id !== opts.project)
8444
- return false;
8445
- if (opts.story && q.story_id !== opts.story)
8446
- return false;
8447
- assertQuestionCompanyAccess(q, lane, company, hqRoot);
8448
- if (forFilter) {
8449
- if (!audienceMatches(audience, forFilter))
8450
- return false;
8451
- }
8452
- return true;
8453
- })
8454
- .map(({ question: q, host, audience }) => ({
8455
- ...q,
8456
- host,
8457
- audience,
8458
- ...(q.question_kind === "continue" ? { flag: "continue-loop" } : {}),
8459
- })),
8460
- ];
8461
- printJson({
8462
- ok: true,
8463
- action: "questions.list",
8464
- questions,
8465
- ...(outpostLoad.errors.length > 0 ? { errors: outpostLoad.errors } : {}),
8466
- });
8467
- }
8468
- async function runQuestionsShow(ctx, questionId) {
8469
- const hqRoot = ctx.hqRoot();
8470
- let question;
8471
- try {
8472
- question = readQuestion(hqRoot, questionId);
8473
- }
8474
- catch (err) {
8475
- if (!isLanesError(err) || err.error !== "question_not_found")
8476
- throw err;
8477
- const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
8478
- if (lookup.errors.length > 0) {
8479
- throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
8480
- question_id: questionId,
8481
- hosts: lookup.errors.map(({ host, error }) => ({ host, error })),
8482
- });
8483
- }
8484
- const matches = lookup.questions.filter((entry) => entry.question.question_id === questionId);
8485
- if (matches.length === 0) {
8486
- throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
8487
- }
8488
- if (matches.length > 1) {
8489
- throw new LanesError("outpost_question_ambiguous", `Question ${questionId} is present on more than one Outpost`, { question_id: questionId, owner_count: matches.length });
8490
- }
8491
- const owner = matches[0];
8492
- if (!owner) {
8493
- throw new LanesError("outpost_question_ambiguous", `Question ${questionId} has no unique Outpost owner`, { question_id: questionId });
8494
- }
8495
- const detail = await readOutpostQuestionDetail(ctx, owner);
8496
- printJson({
8497
- ok: true,
8498
- action: "questions.show",
8499
- question: detail.question,
8500
- audience: detail.audience,
8501
- body: detail.body,
8502
- host: owner.host,
8503
- });
8504
- return;
8505
- }
8506
- const body = readQuestionBody(hqRoot, question);
8507
- printJson({
8508
- ok: true,
8509
- action: "questions.show",
8510
- question,
8511
- audience: resolveAudience(hqRoot, question, ctx),
8512
- body,
8513
- });
8514
- }
8515
- async function runQuestionsAnswer(ctx, questionId, opts) {
8516
- const hqRoot = ctx.hqRoot();
8517
- if (opts.choice !== undefined)
8518
- assertNoSecretText("answer choice", opts.choice);
8519
- if (opts.freeform !== undefined)
8520
- assertNoSecretText("freeform answer", opts.freeform);
8521
- if (opts.notes !== undefined)
8522
- assertNoSecretText("answer notes", opts.notes);
8523
- const explicitCompany = opts.company ? await ctx.resolveCompany(opts.company) : undefined;
8524
- let question;
8525
- try {
8526
- question = readQuestion(hqRoot, questionId);
8527
- }
8528
- catch (err) {
8529
- if (!isLanesError(err) || err.error !== "question_not_found")
8530
- throw err;
8531
- 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>");
8532
- const lookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot, company);
8533
- if (lookup.errors.length > 0) {
8534
- throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
8535
- question_id: questionId,
8536
- hosts: lookup.errors.map(({ host, error }) => ({ host, error })),
8537
- });
8538
- }
8539
- const matches = lookup.questions.filter((entry) => entry.question.question_id === questionId);
8540
- if (matches.length === 0) {
8541
- const allCompanyLookup = await loadPrimaryOwnedOutpostQuestions(ctx, hqRoot);
8542
- const otherCompanyMatch = allCompanyLookup.questions.find((entry) => entry.question.question_id === questionId);
8543
- if (otherCompanyMatch) {
8544
- assertQuestionCompanyAccess(otherCompanyMatch.question, otherCompanyMatch.lane, company, hqRoot);
8545
- }
8546
- if (allCompanyLookup.errors.length > 0) {
8547
- throw new LanesError("outpost_question_lookup_unavailable", `Could not verify whether Outposts own question ${questionId}`, {
8548
- question_id: questionId,
8549
- hosts: allCompanyLookup.errors.map(({ host, error }) => ({ host, error })),
8550
- });
8551
- }
8552
- throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
8553
- }
8554
- if (matches.length > 1) {
8555
- throw new LanesError("outpost_question_ambiguous", `Question ${questionId} is present on more than one Outpost`, { question_id: questionId, owner_count: matches.length });
8556
- }
8557
- const owner = matches[0];
8558
- if (!owner) {
8559
- throw new LanesError("question_not_found", `Question ${questionId} not found on this primary or its Outposts`, { question_id: questionId });
8560
- }
8561
- assertQuestionCompanyAccess(owner.question, owner.lane, company, hqRoot);
8562
- const ownerCompany = await resolveQuestionCompanyWithFallback(hqRoot, owner.lane, async () => explicitCompany ?? company);
8563
- assertQuestionCompanyResolvedForAnswer(owner.question, owner.lane, hqRoot, ownerCompany);
8564
- throwIfQuestionNotPending(owner.question);
8565
- await yieldEventLoop();
8566
- await dispatchOutpostQuestionAnswer(ctx, owner.lane, questionId, {
8567
- ...opts,
8568
- company: questionListCompanyArg(ownerCompany),
8569
- });
8570
- return;
8571
- }
8572
- await yieldEventLoop();
8573
- const askingLane = readLane(hqRoot, laneIdForAnswerDelivery(question));
8574
- 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>`));
8575
- const requestedCompany = explicitCompany ?? resolvedCompany;
8576
- assertQuestionCompanyAccess(question, askingLane, requestedCompany, hqRoot);
8577
- assertQuestionCompanyResolvedForAnswer(question, askingLane, hqRoot, resolvedCompany);
8578
- const questionCompany = question.company ?? effectiveLaneQuestionCompany(askingLane, hqRoot) ?? resolvedCompany;
8579
- const questionForBody = question.company === null
8580
- ? { ...question, company: questionCompany }
8581
- : question;
8582
- if (askingLane.host.kind === "outpost" && !laneIsHostedHere(ctx, askingLane)) {
8583
- await dispatchOutpostQuestionAnswer(ctx, askingLane, questionId, {
8584
- ...opts,
8585
- company: questionListCompanyArg(questionCompany),
8586
- });
8587
- return;
8588
- }
8589
- throwIfQuestionNotPending(question);
8590
- if (askingLane.state === "awaiting_input") {
8591
- assertLaneRunsHereForOperatorMutation(ctx, askingLane, "wake after answering");
8592
- }
8593
- if (laneAcceptsDropboxDelivery(askingLane)) {
8594
- assertLaneRunsHereForOperatorMutation(ctx, askingLane, "deliver an answer to");
8595
- }
8596
- const body = requireQuestionBody(hqRoot, questionForBody);
8597
- // A trimmed-empty --freeform is absent: it must not win over --choice and
8598
- // must not become a freeform answer of "".
8599
- const freeform = opts.freeform?.trim() || undefined;
8600
- if (freeform !== undefined && body.options.length === 0) {
8601
- throw new LanesError("freeform_not_allowed", "--freeform is only allowed for a question with options", { question_id: questionId });
8602
- }
8603
- if (!freeform && !opts.choice) {
8604
- throw new LanesError("missing_choice", "--choice or --freeform is required", {});
8605
- }
8606
- const choice = freeform ?? resolveAnswerChoice(opts.choice, body.options, false);
8607
- const at = ctx.now().toISOString();
8608
- const answeredBy = sessionIdFromEnv(ctx.env)
8609
- ? `session:${sessionIdFromEnv(ctx.env)}`
8610
- : "user";
8611
- const updated = casUpdateQuestion(hqRoot, questionId, (current) => {
8612
- throwIfQuestionNotPending(current);
8613
- assertQuestionCompanyAccess(current, askingLane, requestedCompany, hqRoot);
8614
- const recordedCompany = current.company ?? questionCompany;
8615
- const currentForBody = current.company === null
8616
- ? { ...current, company: recordedCompany }
8617
- : current;
8618
- let notesRef;
8619
- let nextBody = current.body;
8620
- if (opts.notes) {
8621
- const stored = writeQuestionBody(hqRoot, currentForBody, {
8622
- ...body,
8623
- notes: opts.notes,
8624
- });
8625
- notesRef = stored.ref;
8626
- nextBody = stored;
8627
- }
8628
- return {
8629
- ...current,
8630
- company: recordedCompany,
8631
- body: nextBody,
8632
- status: "answered",
8633
- answer: {
8634
- choice,
8635
- answer_kind: freeform ? "freeform" : "option",
8636
- notes_ref: notesRef,
8637
- answered_by: answeredBy,
8638
- answered_at: at,
8639
- answered_level: current.hop,
8640
- },
8641
- };
8642
- });
8643
- const latestAskingLane = readLane(hqRoot, askingLane.lane_id);
8644
- if (latestAskingLane.state === "awaiting_input") {
8645
- let wake;
8646
- try {
8647
- wake = await wakeAnsweredLaneNow(hqRoot, askingLane.lane_id, {
8648
- now: ctx.now,
8649
- host: reconcileHostFromCtx(ctx),
8650
- dryRun: false,
8651
- spawnLaneDeps: {
8652
- env: ctx.env,
8653
- existsSync: ctx.existsSync,
8654
- spawnProcess: ctx.spawnProcess,
8655
- proveDetached: ctx.proveDetached,
8656
- killProcess: ctx.killProcess,
8657
- now: ctx.now,
8658
- resolveMachineIdentity: ctx.resolveMachineIdentity,
8659
- runCompaction: ctx.runCompaction,
8660
- locateTranscript: ctx.locateTranscript,
8661
- },
8662
- throttleDeps: ctx.throttleDeps,
8663
- livenessDeps: livenessDepsFromCtx(ctx),
8664
- presenceConnected: ctx.presenceConnected,
8665
- mesh: ctx.mesh,
8666
- env: ctx.env,
8667
- });
8668
- }
8669
- catch (err) {
8670
- 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 });
8671
- }
8672
- if (wake.status === "failed") {
8673
- 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 });
8674
- }
8675
- printJson({
8676
- ok: true,
8677
- action: "questions.answer",
8678
- lane_id: askingLane.lane_id,
8679
- question: updated,
8680
- delivery: wake.status === "awaiting_answers" ? "awaiting_other_answers" : wake.status,
8681
- wake_status: wake.status,
8682
- lane_state: wake.lane_state,
8683
- ...(wake.queued_reason !== undefined ? { queued_reason: wake.queued_reason } : {}),
8684
- ...(wake.remaining_question_ids !== undefined
8685
- ? { remaining_question_ids: wake.remaining_question_ids }
8686
- : {}),
8687
- });
8688
- return;
8689
- }
8690
- const delivery = deliverAnswerToAskingLane(ctx, updated, choice, at);
8691
- printJson({
8692
- ok: true,
8693
- action: "questions.answer",
8694
- question: updated,
8695
- delivery: delivery.status,
8696
- });
8697
- }
8698
- function writeAnswerDeliveryFailed(hqRoot, laneId, questionId, err, at) {
8699
- const failure = {
8700
- schema_version: "hq-lane-ask-delivery-failed.v1",
8701
- at,
8702
- lane_id: laneId,
8703
- question_id: questionId,
8704
- error: err instanceof Error
8705
- ? { name: err.name, message: err.message }
8706
- : { name: "Error", message: String(err) },
8707
- };
8708
- try {
8709
- writeJsonFile(askDeliveryFailedPath(hqRoot, laneId), failure);
8710
- }
8711
- catch (writeErr) {
8712
- process.stderr.write(`hq lanes: failed to write ask_delivery_failed for ${laneId}: ${writeErr instanceof Error ? writeErr.message : String(writeErr)}\n`);
8713
- }
8714
- }
8715
- /**
8716
- * Record is already durable. Delivery is best-effort onto the asking lane's
8717
- * drop box (spec §6.5). Target is question.lane_id, never a project/story lookup.
8718
- */
8719
- function deliverAnswerToAskingLane(ctx, question, choice, at) {
8720
- const hqRoot = ctx.hqRoot();
8721
- const laneId = laneIdForAnswerDelivery(question);
8722
- try {
8723
- const lane = readLane(hqRoot, laneId);
8724
- if (!laneAcceptsDropboxDelivery(lane)) {
8725
- return { status: "recorded_only" };
8726
- }
8727
- assertLaneRunsHereForOperatorMutation(ctx, lane, "deliver an answer to");
8728
- appendDropboxMessage(hqRoot, laneId, answerDropboxMessage(question, choice, at, mintMessageId()));
8729
- return { status: "awaiting_next_tool_call" };
8730
- }
8731
- catch (err) {
8732
- writeAnswerDeliveryFailed(hqRoot, laneId, question.question_id, err, at);
8733
- return { status: "failed" };
8734
- }
8735
- }
8736
- async function runQuestionsEscalate(ctx, questionId, opts) {
8737
- const hqRoot = ctx.hqRoot();
8738
- const question = readQuestion(hqRoot, questionId);
8739
- throwIfQuestionNotPending(question);
8740
- await yieldEventLoop();
8741
- const originalHop = question.hop;
8742
- const audience = resolveAudience(hqRoot, question, ctx);
8743
- if (audience.kind === "user") {
8744
- throw new LanesError("already_at_top_authority", "Cannot escalate past the user", { question_id: questionId });
8745
- }
8746
- const at = ctx.now().toISOString();
8747
- if (opts.note) {
8748
- requireQuestionBody(hqRoot, question);
8749
- }
8750
- const updated = casUpdateQuestion(hqRoot, questionId, (current) => {
8751
- throwIfQuestionNotPending(current);
8752
- if (current.hop !== originalHop) {
8753
- throw new LanesError("question_already_escalated", `Question ${questionId} hop already advanced`, {
8754
- question_id: questionId,
8755
- hop: current.hop,
8756
- expected_hop: originalHop,
8757
- });
8758
- }
8759
- let nextBody = current.body;
8760
- if (opts.note) {
8761
- const body = requireQuestionBody(hqRoot, current);
8762
- nextBody = writeQuestionBody(hqRoot, current, {
8763
- ...body,
8764
- notes: [body.notes, opts.note].filter(Boolean).join("\n"),
8765
- });
8766
- }
8767
- const nextHop = current.hop + 1;
8768
- return {
8769
- ...current,
8770
- body: nextBody,
8771
- hop: nextHop,
8772
- routing_history: appendCappedHistory(current.routing_history, {
8773
- hop: nextHop,
8774
- audience: resolveAudience(hqRoot, { ...current, hop: nextHop }, ctx),
8775
- at,
8776
- reason: "escalated",
8777
- }),
8778
- };
8779
- });
8780
- const nextAudience = resolveAudience(hqRoot, updated, ctx);
8781
- if (nextAudience.kind === "user") {
8782
- const lane = readLane(hqRoot, updated.lane_id);
8783
- 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 });
8784
- }
8785
- printJson({ ok: true, action: "questions.escalate", question: updated });
8786
- }
8787
- async function runQuestionsCancel(ctx, questionId, opts) {
8788
- const hqRoot = ctx.hqRoot();
8789
- const question = readQuestion(hqRoot, questionId);
8790
- throwIfQuestionNotPending(question);
8791
- await yieldEventLoop();
8792
- const at = ctx.now().toISOString();
8793
- const updated = casUpdateQuestion(hqRoot, questionId, (current) => {
8794
- throwIfQuestionNotPending(current);
8795
- const entry = {
8796
- hop: current.hop,
8797
- audience: resolveAudience(hqRoot, current, ctx),
8798
- at,
8799
- reason: "cancelled",
8800
- ...(opts.reason ? { detail: opts.reason } : {}),
8801
- };
8802
- return {
8803
- ...current,
8804
- status: "cancelled",
8805
- routing_history: appendCappedHistory(current.routing_history, entry),
8806
- };
8807
- });
8808
- printJson({ ok: true, action: "questions.cancel", question: updated });
8809
- }
8810
- async function runAccountsList(ctx, opts) {
8811
- const hqRoot = ctx.hqRoot();
8812
- const config = loadLanesConfig(hqRoot);
8813
- const providers = opts.provider
8814
- ? [parseLaneProvider(opts.provider)]
8815
- : [...LANE_PROVIDERS];
8816
- // model and effort are what a spawn on each account would start from
8817
- // (spec: --model/--effort, then the account, then the provider).
8818
- const quota = loadQuotaState(hqRoot);
8819
- const accounts = providers.flatMap((provider) => listAccountStatuses(provider, config, {
8820
- existsSync: ctx.existsSync,
8821
- hostname: ctx.hostname(),
8822
- probe: ctx.probeProvider,
8823
- env: ctx.env,
8824
- }).map((status) => {
8825
- const accountCfg = config.providers[provider].accounts[status.id];
8826
- const model = resolveLaneModel({
8827
- accountModel: accountCfg?.model,
8828
- providerModel: config.providers[provider].model,
8829
- });
8830
- const effort = resolveLaneEffort({
8831
- accountEffort: accountCfg?.effort,
8832
- providerEffort: config.providers[provider].effort,
8833
- model,
8834
- });
8835
- return {
8836
- ...status,
8837
- effective_home: status.home,
8838
- model: accountCfg?.model ?? null,
8839
- effort: accountCfg?.effort ?? null,
8840
- effective_model: model ?? null,
8841
- effective_effort: effort.effort ?? null,
8842
- effective_effort_source: effort.source,
8843
- ...accountQuotaView(quota, provider, status.id),
8844
- };
8845
- }));
8846
- const providerDefaults = providers.map((provider) => ({
8847
- provider,
8848
- model: config.providers[provider].model ?? null,
8849
- effort: config.providers[provider].effort ?? null,
8850
- compact_after_ms: providerCompactAfterMs(config.providers[provider]),
8851
- }));
8852
- printJson({
8853
- ok: true,
8854
- action: "accounts.list",
8855
- providers: providerDefaults,
8856
- accounts,
8857
- });
8858
- }
8859
- async function runAccountsBalance(ctx, opts) {
8860
- const provider = parseLaneProvider(opts.provider ?? "");
8861
- if (provider !== "claude") {
8862
- throw new LanesError("claude_account_balance_provider_unsupported", "Account balance is available only for --provider claude", {
8863
- provider,
8864
- accepted: ["claude"],
8865
- });
8866
- }
8867
- const hqRoot = ctx.hqRoot();
8868
- const config = loadLanesConfig(hqRoot);
8869
- const loaded = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config);
8870
- // Retain the established response field for clients; balancing is now
8871
- // unconditional, so this compatibility value is always true.
8872
- const balanceGateEnabled = true;
8873
- const loads = accountLoadsForReport({ hosts: loaded.snapshots, config });
8874
- const suggestions = planClaudeBalanceMoves({ hosts: loaded.snapshots, config });
8875
- const selections = loaded.snapshots.map((host) => {
8876
- try {
8877
- const selection = selectClaudeAccount({
8878
- hosts: loaded.snapshots,
8879
- targetHost: host.capacity.host,
8880
- config,
8881
- unavailableHosts: loaded.unavailableHosts,
8882
- });
8883
- return {
8884
- target_host: host.capacity.host,
8885
- account: selection.account,
8886
- signal: selection.signal,
8887
- skipped_accounts: selection.skipped_accounts,
8888
- };
8889
- }
8890
- catch (err) {
8891
- return {
8892
- target_host: host.capacity.host,
8893
- error: isLanesError(err) ? err.error : "claude_account_balance_selection_failed",
8894
- message: dispatchErrorMessage(err),
8895
- };
8896
- }
8897
- });
8898
- const accounts = Object.keys(config.providers.claude.accounts).sort().map((account) => {
8899
- const load = loads.find((row) => row.account === account);
8900
- return {
8901
- account,
8902
- enabled: config.providers.claude.accounts[account]?.enabled === true,
8903
- live_lanes: load?.live_lanes ?? 0,
8904
- counts_complete: load?.counts_complete ?? false,
8905
- hosts: loaded.snapshots.map((host) => {
8906
- const accountStatus = host.capacity.providers.claude.accounts[account];
8907
- const accountLoad = load?.hosts.find((row) => row.host.kind === host.capacity.host.kind && row.host.id === host.capacity.host.id);
8908
- return {
8909
- host: host.capacity.host,
8910
- enabled: accountStatus?.enabled ?? false,
8911
- logged_in: accountStatus?.logged_in ?? null,
8912
- live_lanes: accountLoad?.live_lanes ?? null,
8913
- lane_counts: accountLoad?.lane_counts ?? { running: null, queued: null, awaiting_input: null },
8914
- count_source: accountLoad?.count_source ?? "unavailable",
8915
- usage: accountStatus?.usage ?? null,
8916
- };
8917
- }),
8918
- };
8919
- });
8920
- const moved = [];
8921
- const skipped = [];
8922
- if (opts.apply) {
8923
- for (const suggestion of suggestions) {
8924
- if (!suggestion.apply) {
8925
- skipped.push({
8926
- lane_id: suggestion.lane_id,
8927
- host: suggestion.host,
8928
- from: suggestion.from,
8929
- to: suggestion.to,
8930
- reason: suggestion.skip_reason ?? "not_movable",
8931
- });
8932
- continue;
8933
- }
8934
- try {
8935
- moved.push(await applyClaudeBalanceMove(ctx, suggestion));
8936
- }
8937
- catch (err) {
8938
- skipped.push({
8939
- lane_id: suggestion.lane_id,
8940
- host: suggestion.host,
8941
- from: suggestion.from,
8942
- to: suggestion.to,
8943
- reason: isLanesError(err) ? `${err.error}: ${err.message}` : dispatchErrorMessage(err),
8944
- });
8945
- }
8946
- }
8947
- }
8948
- printJson({
8949
- ok: true,
8950
- action: "accounts.balance",
8951
- provider: "claude",
8952
- balance_gate_enabled: balanceGateEnabled,
8953
- accounts,
8954
- selections,
8955
- unavailable_hosts: loaded.unavailableHosts,
8956
- suggestions,
8957
- ...(opts.apply ? { moved, skipped } : {}),
8958
- });
8959
- }
8960
- async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
8961
- const callerCwd = ctx.callerCwd();
8962
- const hqRoot = ctx.hqRoot();
8963
- const provider = parseLaneProvider(providerRaw);
8964
- const config = loadLanesConfig(hqRoot);
8965
- const id = accountId.trim();
8966
- if (!id) {
8967
- throw new LanesError("invalid_account", "account-id must be non-empty", {});
8968
- }
8969
- if (opts.deviceAuth && provider !== "codex") {
8970
- throw new LanesError("device_auth_unsupported", "--device-auth is supported only for codex account login", { provider });
8971
- }
8972
- const registeredAccount = config.providers[provider].accounts[id];
8973
- const hostname = ctx.hostname().trim();
8974
- if (!hostname) {
8975
- throw new LanesError("host_identity_unknown", "Cannot resolve this host's hostname for account login", {});
8976
- }
8977
- const requestedHome = opts.home?.trim();
8978
- if (opts.home !== undefined && !requestedHome) {
8979
- throw new LanesError("invalid_account_home", "--home must be a non-empty path", { provider, account: id });
8980
- }
8981
- const company = opts.company !== undefined
8982
- ? await resolveSelectedAccountHomeCompany(ctx, opts.company)
8983
- : undefined;
8984
- const hostScopedWriterEnabled = registeredAccount !== undefined && requestedHome !== undefined
8985
- ? await hostScopedAccountHomesEnabled(ctx, company)
8986
- : false;
8987
- const homeRaw = requestedHome ??
8988
- (registeredAccount !== undefined
8989
- ? effectiveAccountHome(registeredAccount, hostname).home
8990
- : id === "a"
8991
- ? DEFAULT_ACCOUNT_HOMES[provider]
8992
- : `~/.${provider}-${id}`);
8993
- const homeResolved = homeRaw.startsWith("~") || path.isAbsolute(homeRaw)
8994
- ? homeRaw
8995
- : resolveCallerPath(callerCwd, homeRaw);
8996
- const home = canonicalizeHome(homeResolved, {
8997
- homedir: ctx.homedir,
8998
- realpathSync: (p) => fs.realpathSync(p),
8999
- existsSync: (p) => fs.existsSync(p),
9000
- });
9001
- const nextAccount = registeredAccount === undefined
9002
- ? { home, enabled: true }
9003
- : { ...registeredAccount, enabled: true };
9004
- if (registeredAccount !== undefined) {
9005
- if (hostScopedWriterEnabled && requestedHome !== undefined) {
9006
- if (home === registeredAccount.home) {
9007
- if (nextAccount.hosts !== undefined) {
9008
- delete nextAccount.hosts[hostname];
9009
- if (Object.keys(nextAccount.hosts).length === 0)
9010
- delete nextAccount.hosts;
9011
- }
9012
- }
9013
- else {
9014
- nextAccount.hosts = {
9015
- ...(registeredAccount.hosts ?? {}),
9016
- [hostname]: { home },
9017
- };
9018
- }
9019
- }
9020
- else if (requestedHome !== undefined) {
9021
- // With the rollout off, retain the legacy shared-home write shape.
9022
- nextAccount.home = home;
9023
- }
9024
- }
9025
- delete nextAccount.disabled_reason;
9026
- assertUniqueAccountHomes(provider, {
9027
- ...config.providers[provider].accounts,
9028
- [id]: nextAccount,
9029
- });
9030
- ensureDir(home);
9031
- const defaultId = config.providers[provider].default_account;
9032
- const defaultAccount = defaultId !== undefined
9033
- ? config.providers[provider].accounts[defaultId]
9034
- : Object.values(config.providers[provider].accounts).find((account) => account.enabled);
9035
- const defaultHome = (defaultAccount !== undefined
9036
- ? effectiveAccountHome(defaultAccount, hostname).home
9037
- : undefined) ??
9038
- canonicalizeHome(DEFAULT_ACCOUNT_HOMES[provider], {
9039
- homedir: ctx.homedir,
9040
- realpathSync: (p) => fs.realpathSync(p),
9041
- existsSync: (p) => fs.existsSync(p),
9042
- });
9043
- const copied = defaultHome !== home ? copyHookRails(defaultHome, home) : railsPresent(home);
9044
- installDrainHook(provider, home);
9045
- const envVar = PROVIDER_HOME_ENV[provider];
9046
- const login = VENDOR_LOGIN[provider];
9047
- const loginArgs = provider === "codex" && opts.deviceAuth
9048
- ? [...login.args, "--device-auth"]
9049
- : login.args;
9050
- const result = await ctx.spawnLogin({
9051
- command: login.command,
9052
- args: loginArgs,
9053
- env: { ...ctx.env, [envVar]: home },
9054
- cwd: home,
9055
- });
9056
- if (result.status !== 0) {
9057
- throw new LanesError("login_failed", `${login.command} ${loginArgs.join(" ")} exited ${result.status}`, { provider, account: id, status: result.status });
9058
- }
9059
- const grokNeedsRails = provider === "grok";
9060
- if ((grokNeedsRails || copied) && !railsPresent(home)) {
9061
- throw new LanesError("rails_missing", `Hook rails are missing under ${home}; account not enabled`, { provider, account: id, home });
9062
- }
9063
- config.providers[provider].accounts[id] = nextAccount;
9064
- const status = accountStatus(provider, id, config, {
9065
- existsSync: ctx.existsSync,
9066
- hostname,
9067
- probe: ctx.probeProvider,
9068
- env: ctx.env,
9069
- });
9070
- if (status.logged_in !== true) {
9071
- throw new LanesError(status.logged_in === false ? "account_not_logged_in" : "account_login_status_unknown", status.logged_in === false
9072
- ? `Account ${id} for ${provider} is not logged in`
9073
- : `Could not verify that account ${id} for ${provider} is logged in`, { provider, account: id, home });
9074
- }
9075
- writeLanesConfig(hqRoot, config);
9076
- printJson({ ok: true, action: "accounts.login", account: status });
9077
- }
9078
- async function runAccountsSetHome(ctx, providerRaw, accountIdRaw, homeRaw, opts) {
9079
- const company = opts.company !== undefined
9080
- ? await resolveSelectedAccountHomeCompany(ctx, opts.company)
9081
- : undefined;
9082
- if (!(await hostScopedAccountHomesEnabled(ctx, company))) {
9083
- throw new LanesError("host_scoped_account_homes_disabled", "Setting a host-specific account home requires the lanes.host-scoped-account-homes flag", { flag: LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG });
9084
- }
9085
- const callerCwd = ctx.callerCwd();
9086
- const hqRoot = ctx.hqRoot();
9087
- const provider = parseLaneProvider(providerRaw);
9088
- const id = accountIdRaw.trim();
9089
- if (!id) {
9090
- throw new AccountResolutionError("unknown_account", "account-id must be non-empty", {
9091
- provider,
9092
- account: accountIdRaw,
9093
- enabled: [],
9094
- });
9095
- }
9096
- const hostname = opts.host === undefined ? ctx.hostname().trim() : opts.host.trim();
9097
- if (!hostname || (opts.host !== undefined && hostname !== opts.host)) {
9098
- throw new LanesError("invalid_host", "--host must be a non-empty hostname without surrounding whitespace", {
9099
- host: opts.host ?? "",
9100
- });
9101
- }
9102
- if (!homeRaw.trim()) {
9103
- throw new LanesError("invalid_account_home", "home path must be a non-empty path", {
9104
- provider,
9105
- account: id,
9106
- });
9107
- }
9108
- const config = loadLanesConfig(hqRoot);
9109
- const account = config.providers[provider].accounts[id];
9110
- if (account === undefined) {
9111
- throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(id)} for ${provider}`, {
9112
- provider,
9113
- account: id,
9114
- enabled: Object.keys(config.providers[provider].accounts).filter((key) => config.providers[provider].accounts[key]?.enabled),
9115
- });
9116
- }
9117
- const homePath = homeRaw.trim();
9118
- const isCurrentHost = hostname === ctx.hostname().trim();
9119
- let home;
9120
- if (isCurrentHost) {
9121
- const homeResolved = homePath.startsWith("~") || path.isAbsolute(homePath)
9122
- ? homePath
9123
- : resolveCallerPath(callerCwd, homePath);
9124
- home = canonicalizeHome(homeResolved, {
9125
- homedir: ctx.homedir,
9126
- realpathSync: (p) => fs.realpathSync(p),
9127
- existsSync: (p) => fs.existsSync(p),
9128
- });
9129
- }
9130
- else if (homePath === "~" || homePath.startsWith("~/")) {
9131
- home = homePath;
9132
- }
9133
- else if (path.isAbsolute(homePath)) {
9134
- home = path.normalize(homePath);
9135
- }
9136
- else {
9137
- 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 });
9138
- }
9139
- for (const [otherId, other] of Object.entries(config.providers[provider].accounts)) {
9140
- if (otherId !== id && effectiveAccountHome(other, hostname).home === home) {
9141
- throw new LanesError("duplicate_account_home", `Home ${home} is already registered as ${provider} account ${otherId} on host ${hostname}`, { provider, accounts: [otherId, id], home, hostname });
9142
- }
9143
- }
9144
- if (home === account.home) {
9145
- if (account.hosts !== undefined) {
9146
- delete account.hosts[hostname];
9147
- if (Object.keys(account.hosts).length === 0)
9148
- delete account.hosts;
9149
- }
9150
- }
9151
- else {
9152
- const hosts = Object.assign(Object.create(null), account.hosts ?? {});
9153
- hosts[hostname] = { home };
9154
- account.hosts = hosts;
9155
- }
9156
- assertUniqueAccountHomes(provider, config.providers[provider].accounts);
9157
- writeLanesConfig(hqRoot, config);
9158
- printJson({
9159
- ok: true,
9160
- action: "accounts.set-home",
9161
- provider,
9162
- account: id,
9163
- hostname,
9164
- home,
9165
- home_source: home === account.home ? "default" : "host",
9166
- });
9167
- }
9168
- async function runAccountsEnable(ctx, providerRaw, accountId) {
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
- const status = accountStatus(provider, accountId, config, {
9183
- existsSync: ctx.existsSync,
9184
- hostname: ctx.hostname(),
9185
- probe: ctx.probeProvider,
9186
- env: ctx.env,
9187
- });
9188
- if (status.logged_in !== true) {
9189
- throw new LanesError(status.logged_in === false ? "account_not_logged_in" : "account_login_status_unknown", status.logged_in === false
9190
- ? `Account ${accountId} for ${provider} is not logged in`
9191
- : `Could not verify that account ${accountId} for ${provider} is logged in`, { provider, account: accountId });
9192
- }
9193
- account.enabled = true;
9194
- delete account.disabled_reason;
9195
- writeLanesConfig(hqRoot, config);
9196
- printJson({
9197
- ok: true,
9198
- action: "accounts.enable",
9199
- account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
9200
- existsSync: ctx.existsSync,
9201
- hostname: ctx.hostname(),
9202
- probe: ctx.probeProvider,
9203
- env: ctx.env,
9204
- }),
9205
- });
9206
- }
9207
- async function runAccountsDisable(ctx, providerRaw, accountId, opts) {
9208
- const hqRoot = ctx.hqRoot();
9209
- const provider = parseLaneProvider(providerRaw);
9210
- const config = loadLanesConfig(hqRoot);
9211
- const account = config.providers[provider].accounts[accountId];
9212
- if (!account) {
9213
- throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(accountId)} for ${provider}`, {
9214
- provider,
9215
- account: accountId,
9216
- enabled: Object.entries(config.providers[provider].accounts)
9217
- .filter(([, a]) => a.enabled)
9218
- .map(([id]) => id),
9219
- });
9220
- }
9221
- account.enabled = false;
9222
- if (opts.reason)
9223
- account.disabled_reason = opts.reason;
9224
- writeLanesConfig(hqRoot, config);
9225
- printJson({
9226
- ok: true,
9227
- action: "accounts.disable",
9228
- account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
9229
- existsSync: ctx.existsSync,
9230
- hostname: ctx.hostname(),
9231
- probe: ctx.probeProvider,
9232
- env: ctx.env,
9233
- }),
9234
- });
9235
- }
9236
- async function runSessionsList(ctx, opts) {
9237
- const hqRoot = ctx.hqRoot();
9238
- const state = opts.state ? parseLivenessState(opts.state) : undefined;
9239
- const livenessDeps = livenessDepsFromCtx(ctx);
9240
- const presenceConnected = ctx.presenceConnected ?? readDaemonPresenceConnected(livenessDeps);
9241
- const rows = listSessionLiveness(hqRoot, {
9242
- nowMs: ctx.now().getTime(),
9243
- presenceConnected,
9244
- deps: livenessDeps,
9245
- }).filter((row) => {
9246
- if (state && row.state !== state)
9247
- return false;
9248
- if (!opts.company)
9249
- return true;
9250
- const meta = readSessionMeta(hqRoot, row.sessionId, ctx);
9251
- const slug = typeof meta?.company_slug === "string" ? meta.company_slug : "";
9252
- const uid = typeof meta?.company_uid === "string" ? meta.company_uid : "";
9253
- return slug === opts.company || uid === opts.company;
9254
- });
9255
- printJson({ ok: true, action: "sessions.list", sessions: rows });
9256
- }
9257
- async function runSessionsShow(ctx, sessionId) {
9258
- const hqRoot = ctx.hqRoot();
9259
- const livenessDeps = livenessDepsFromCtx(ctx);
9260
- const presenceConnected = ctx.presenceConnected ?? readDaemonPresenceConnected(livenessDeps);
9261
- const state = sessionLiveness(hqRoot, sessionId, {
9262
- nowMs: ctx.now().getTime(),
9263
- presenceConnected,
9264
- deps: livenessDeps,
9265
- });
9266
- if (state === null) {
9267
- throw new LanesError("session_not_found", `Session ${sessionId} is not in the liveness index`, { session_id: sessionId });
9268
- }
9269
- const meta = readSessionMeta(hqRoot, sessionId, ctx);
9270
- printJson({
9271
- ok: true,
9272
- action: "sessions.show",
9273
- session_id: sessionId,
9274
- state,
9275
- senior: meta ? seniorOfSession(hqRoot, sessionId, ctx) : { kind: "user" },
9276
- company_slug: typeof meta?.company_slug === "string" ? meta.company_slug : null,
9277
- });
9278
- }
9279
- async function runReconcile(ctx, opts) {
9280
- const fromEnv = ctx.env.HQ_ROOT?.trim();
9281
- const hqRoot = fromEnv && ctx.existsSync(fromEnv) ? fromEnv : ctx.hqRoot();
9282
- const summary = await runReconcilePass(hqRoot, {
9283
- now: ctx.now,
9284
- host: reconcileHostFromCtx(ctx),
9285
- dryRun: Boolean(opts.dryRun),
9286
- spawnLaneDeps: {
9287
- env: ctx.env,
9288
- modelDeps: {
9289
- readFileSync: ctx.readFileSync,
9290
- probe: ctx.probeProvider,
9291
- env: ctx.env,
9292
- },
9293
- existsSync: ctx.existsSync,
9294
- spawnProcess: ctx.spawnProcess,
9295
- proveDetached: ctx.proveDetached,
9296
- killProcess: ctx.killProcess,
9297
- now: ctx.now,
9298
- resolveMachineIdentity: ctx.resolveMachineIdentity,
9299
- runCompaction: ctx.runCompaction,
9300
- locateTranscript: ctx.locateTranscript,
9301
- },
9302
- throttleDeps: ctx.throttleDeps,
9303
- livenessDeps: livenessDepsFromCtx(ctx),
9304
- presenceConnected: ctx.presenceConnected,
9305
- resolveQuestionActiveCompany: () => resolveActiveQuestionCompany(ctx),
9306
- recoverQueuedMessages: async (lane) => {
9307
- await recoverQueuedMessages(ctx, lane.lane_id);
9308
- },
9309
- mesh: ctx.mesh,
9310
- env: ctx.env,
9311
- });
9312
- printJson(summary);
9313
- if (!summary.ok) {
9314
- process.exit(1);
9315
- }
9316
- }
9317
- async function runReconcileInstall(ctx, opts) {
9318
- // `hq daemon` runs lanes-reconcile on this machine; a separate timer would run it twice.
9319
- const handedOff = opts.systemdRoot ? null : handOffInstallHere(["lanes-reconcile"]);
9320
- if (handedOff) {
9321
- 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 });
9322
7927
  return;
9323
7928
  }
9324
7929
  const callerCwd = ctx.callerCwd();
@@ -9375,6 +7980,53 @@ function attachJsonNoop(cmd) {
9375
7980
  }
9376
7981
  export function registerLanesCommand(parent, deps = {}) {
9377
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
+ });
9378
8030
  const lanes = parent
9379
8031
  .command("lanes", { hidden: true })
9380
8032
  .description("Create, supervise, and steer worker lanes; routed decision queue");
@@ -9409,7 +8061,7 @@ export function registerLanesCommand(parent, deps = {}) {
9409
8061
  trackerEnabled: ctx.laneTrackerEnabled,
9410
8062
  now: ctx.now,
9411
8063
  readQuestionBody: (root, question) => {
9412
- const body = requireQuestionBody(root, question);
8064
+ const body = questionHandlers.requireQuestionBody(root, question);
9413
8065
  assertQuestionBodySecretSafe(body);
9414
8066
  return body;
9415
8067
  },
@@ -9782,14 +8434,14 @@ export function registerLanesCommand(parent, deps = {}) {
9782
8434
  .option("--story <id>", "Filter by story")
9783
8435
  .option("--status <status>", `Filter by status (${QUESTION_STATUSES.join("|")})`)
9784
8436
  .action(wrap(async (opts) => {
9785
- await runQuestionsList(ctx, opts);
8437
+ await questionHandlers.runQuestionsList(ctx, opts);
9786
8438
  }));
9787
8439
  questions
9788
8440
  .command("show")
9789
8441
  .description("Show one question")
9790
8442
  .argument("<question-id>", "Question id")
9791
8443
  .action(wrap(async (questionId) => {
9792
- await runQuestionsShow(ctx, questionId);
8444
+ await questionHandlers.runQuestionsShow(ctx, questionId);
9793
8445
  }));
9794
8446
  questions
9795
8447
  .command("answer")
@@ -9800,7 +8452,7 @@ export function registerLanesCommand(parent, deps = {}) {
9800
8452
  .option("--freeform <text>", "Supply a free-form answer for a question with options")
9801
8453
  .option("--company <slug|uid>", "Require the question lane to match this company")
9802
8454
  .action(wrap(async (questionId, opts) => {
9803
- await runQuestionsAnswer(ctx, questionId, opts);
8455
+ await questionHandlers.runQuestionsAnswer(ctx, questionId, opts);
9804
8456
  }));
9805
8457
  questions
9806
8458
  .command("escalate")
@@ -9808,7 +8460,7 @@ export function registerLanesCommand(parent, deps = {}) {
9808
8460
  .argument("<question-id>", "Question id")
9809
8461
  .option("--note <text>", "Note recorded with the escalation")
9810
8462
  .action(wrap(async (questionId, opts) => {
9811
- await runQuestionsEscalate(ctx, questionId, opts);
8463
+ await questionHandlers.runQuestionsEscalate(ctx, questionId, opts);
9812
8464
  }));
9813
8465
  questions
9814
8466
  .command("cancel")
@@ -9816,7 +8468,7 @@ export function registerLanesCommand(parent, deps = {}) {
9816
8468
  .argument("<question-id>", "Question id")
9817
8469
  .option("--reason <text>", "Why it was cancelled")
9818
8470
  .action(wrap(async (questionId, opts) => {
9819
- await runQuestionsCancel(ctx, questionId, opts);
8471
+ await questionHandlers.runQuestionsCancel(ctx, questionId, opts);
9820
8472
  }));
9821
8473
  const accounts = lanes
9822
8474
  .command("accounts")
@@ -9826,7 +8478,7 @@ export function registerLanesCommand(parent, deps = {}) {
9826
8478
  .description("List accounts")
9827
8479
  .option("--provider <name>", "claude | codex | grok")
9828
8480
  .action(wrap(async (opts) => {
9829
- await runAccountsList(ctx, opts);
8481
+ await accountHandlers.runAccountsList(ctx, opts);
9830
8482
  }));
9831
8483
  accounts
9832
8484
  .command("balance")
@@ -9835,7 +8487,7 @@ export function registerLanesCommand(parent, deps = {}) {
9835
8487
  .option("--apply", "Move eligible between-turn sessions to balance Claude accounts")
9836
8488
  .option("--json", "JSON on stdout (always)")
9837
8489
  .action(wrap(async (opts) => {
9838
- await runAccountsBalance(ctx, opts);
8490
+ await accountHandlers.runAccountsBalance(ctx, opts);
9839
8491
  }));
9840
8492
  accounts
9841
8493
  .command("set-home")
@@ -9844,10 +8496,10 @@ export function registerLanesCommand(parent, deps = {}) {
9844
8496
  .argument("<account-id>", "Existing account id")
9845
8497
  .argument("<path>", "Home directory for this host")
9846
8498
  .option("--host <hostname>", "Host key (defaults to this host's hostname)")
9847
- .option("--company <slug|uid>", "Company scope for the host-account-home flag")
8499
+ .option("--company <slug|uid>", "Company scope for account-home validation")
9848
8500
  .option("--json", "JSON on stdout (always)")
9849
8501
  .action(wrap(async (provider, accountId, home, opts) => {
9850
- await runAccountsSetHome(ctx, provider, accountId, home, opts);
8502
+ await accountHandlers.runAccountsSetHome(ctx, provider, accountId, home, opts);
9851
8503
  }));
9852
8504
  accounts
9853
8505
  .command("login")
@@ -9855,10 +8507,10 @@ export function registerLanesCommand(parent, deps = {}) {
9855
8507
  .argument("<provider>", "claude | codex | grok")
9856
8508
  .argument("<account-id>", "Open-ended account id")
9857
8509
  .option("--home <path>", "Account home directory")
9858
- .option("--company <slug|uid>", "Company scope for the host-account-home flag")
8510
+ .option("--company <slug|uid>", "Company scope for account-home validation")
9859
8511
  .option("--device-auth", "Use Codex device authentication instead of localhost callback")
9860
8512
  .action(wrap(async (provider, accountId, opts) => {
9861
- await runAccountsLogin(ctx, provider, accountId, opts);
8513
+ await accountHandlers.runAccountsLogin(ctx, provider, accountId, opts);
9862
8514
  }));
9863
8515
  accounts
9864
8516
  .command("enable")
@@ -9866,7 +8518,7 @@ export function registerLanesCommand(parent, deps = {}) {
9866
8518
  .argument("<provider>", "claude | codex | grok")
9867
8519
  .argument("<account-id>", "Account id")
9868
8520
  .action(wrap(async (provider, accountId) => {
9869
- await runAccountsEnable(ctx, provider, accountId);
8521
+ await accountHandlers.runAccountsEnable(ctx, provider, accountId);
9870
8522
  }));
9871
8523
  accounts
9872
8524
  .command("disable")
@@ -9875,7 +8527,7 @@ export function registerLanesCommand(parent, deps = {}) {
9875
8527
  .argument("<account-id>", "Account id")
9876
8528
  .option("--reason <text>", "Why the account is disabled")
9877
8529
  .action(wrap(async (provider, accountId, opts) => {
9878
- await runAccountsDisable(ctx, provider, accountId, opts);
8530
+ await accountHandlers.runAccountsDisable(ctx, provider, accountId, opts);
9879
8531
  }));
9880
8532
  const sessions = lanes
9881
8533
  .command("sessions")
@@ -9886,14 +8538,14 @@ export function registerLanesCommand(parent, deps = {}) {
9886
8538
  .option("--state <state>", `live | idle | ended | lost`)
9887
8539
  .option("--company <slug>", "Filter by session meta company_slug")
9888
8540
  .action(wrap(async (opts) => {
9889
- await runSessionsList(ctx, opts);
8541
+ await questionHandlers.runSessionsList(ctx, opts);
9890
8542
  }));
9891
8543
  sessions
9892
8544
  .command("show")
9893
8545
  .description("Show one session's liveness")
9894
8546
  .argument("<session-id>", "Session id")
9895
8547
  .action(wrap(async (sessionId) => {
9896
- await runSessionsShow(ctx, sessionId);
8548
+ await questionHandlers.runSessionsShow(ctx, sessionId);
9897
8549
  }));
9898
8550
  lanes
9899
8551
  .command("pickup", { hidden: true })