@indigoai-us/hq-cli 5.203.0 → 5.205.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/command-catalog.generated.d.ts +33 -0
  3. package/dist/command-catalog.generated.js +43 -0
  4. package/dist/commands/lanes.d.ts +3 -0
  5. package/dist/commands/lanes.js +253 -50
  6. package/dist/lib/lanes/account-home.d.ts +8 -0
  7. package/dist/lib/lanes/account-home.js +10 -0
  8. package/dist/lib/lanes/accounts.d.ts +4 -1
  9. package/dist/lib/lanes/accounts.js +15 -10
  10. package/dist/lib/lanes/admission.js +6 -0
  11. package/dist/lib/lanes/claude-account-balance.d.ts +3 -0
  12. package/dist/lib/lanes/claude-account-balance.js +19 -4
  13. package/dist/lib/lanes/config.d.ts +2 -1
  14. package/dist/lib/lanes/config.js +55 -13
  15. package/dist/lib/lanes/hire-grants.d.ts +4 -0
  16. package/dist/lib/lanes/hire-grants.js +62 -0
  17. package/dist/lib/lanes/host-capacity.d.ts +4 -0
  18. package/dist/lib/lanes/host-capacity.js +8 -1
  19. package/dist/lib/lanes/index.d.ts +3 -1
  20. package/dist/lib/lanes/index.js +2 -1
  21. package/dist/lib/lanes/keep-alive.d.ts +8 -0
  22. package/dist/lib/lanes/keep-alive.js +14 -0
  23. package/dist/lib/lanes/liveness.d.ts +1 -0
  24. package/dist/lib/lanes/liveness.js +3 -1
  25. package/dist/lib/lanes/outpost-dispatch.d.ts +1 -0
  26. package/dist/lib/lanes/outpost-dispatch.js +2 -0
  27. package/dist/lib/lanes/profile.d.ts +1 -0
  28. package/dist/lib/lanes/profile.js +8 -0
  29. package/dist/lib/lanes/provider-rejection.d.ts +1 -1
  30. package/dist/lib/lanes/provider-rejection.js +15 -5
  31. package/dist/lib/lanes/reconcile.d.ts +3 -0
  32. package/dist/lib/lanes/reconcile.js +293 -13
  33. package/dist/lib/lanes/spawn.d.ts +1 -0
  34. package/dist/lib/lanes/spawn.js +30 -14
  35. package/dist/lib/lanes/store.js +25 -0
  36. package/dist/lib/lanes/types.d.ts +15 -0
  37. package/dist/lib/lanes/types.js +1 -0
  38. package/package.json +1 -1
@@ -10,6 +10,7 @@ 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";
13
14
  import { Option } from "commander";
14
15
  import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
15
16
  import { ensureCognitoToken } from "../utils/cognito-session.js";
@@ -32,11 +33,12 @@ import { vaultApiFetch } from "../utils/vault-api.js";
32
33
  import { openMeshTransport } from "../lib/mesh/client.js";
33
34
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
34
35
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
35
- 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, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, 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, 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, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
36
+ import { effectiveAccountHome } from "../lib/lanes/account-home.js";
37
+ 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, LANE_HIRE_GRANTS_FLAG, 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, 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, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
36
38
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
37
39
  import { questionBodyAbsPath } from "../lib/lanes/questions.js";
38
40
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
39
- import { providerCompactAfterMs } from "../lib/lanes/config.js";
41
+ import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
40
42
  import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
41
43
  import { LanesError } from "../lib/lanes/errors.js";
42
44
  import { dispatchLaneDelivery } from "../lib/lanes/delivery-core.js";
@@ -59,6 +61,7 @@ const VENDOR_LOGIN = {
59
61
  export { emitLaneDecisionEvents };
60
62
  export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
61
63
  export const LANE_OUTPOST_QUESTION_VISIBILITY_FLAG = "lanes.outpost-question-visibility";
64
+ export const LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG = "lanes.host-scoped-account-homes";
62
65
  function defaultLanePlacementEnabled() {
63
66
  return resolveProcessFlagGate(LANE_OUTPOST_LOAD_BALANCING_FLAG, { fallback: false }, () => false);
64
67
  }
@@ -82,6 +85,15 @@ function defaultLaneOutpostQuestionVisibilityEnabled(company, env) {
82
85
  env,
83
86
  });
84
87
  }
88
+ function defaultLaneHostScopedAccountHomesEnabled(company, env) {
89
+ return readLanesRegistryFlag(LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG, { company, env });
90
+ }
91
+ async function hostScopedAccountHomesEnabled(ctx) {
92
+ const localOverride = readLocalOverride(LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG, ctx.env);
93
+ if (localOverride !== null)
94
+ return localOverride;
95
+ return ctx.laneHostScopedAccountHomesEnabled(await ctx.resolveCompany());
96
+ }
85
97
  function withoutProviderHome(value) {
86
98
  if (Array.isArray(value))
87
99
  return value.map(withoutProviderHome);
@@ -286,6 +298,8 @@ function resolveDeps(deps = {}) {
286
298
  ((company) => defaultLaneOutpostQuestionVisibilityEnabled(company, env)),
287
299
  laneTrackerEnabled: deps.laneTrackerEnabled ?? defaultLaneTrackerEnabled,
288
300
  laneTrackerMeshEnabled: deps.laneTrackerMeshEnabled ?? defaultLaneTrackerMeshEnabled,
301
+ laneHostScopedAccountHomesEnabled: deps.laneHostScopedAccountHomesEnabled ??
302
+ ((company) => defaultLaneHostScopedAccountHomesEnabled(company, env)),
289
303
  trackerActor: deps.trackerActor ?? defaultTrackerActor,
290
304
  };
291
305
  }
@@ -298,6 +312,7 @@ function livenessDepsFromCtx(ctx) {
298
312
  env: ctx.env,
299
313
  existsSync: ctx.existsSync,
300
314
  readFileSync: ctx.readFileSync,
315
+ hostname: ctx.hostname,
301
316
  };
302
317
  }
303
318
  function reconcileHostFromCtx(ctx) {
@@ -849,33 +864,15 @@ async function withResumeHireAdmission(ctx, lane, fn) {
849
864
  }
850
865
  if (!await ctx.laneHireGrantsEnabled(lane.company))
851
866
  return fn();
852
- return withHireGrantAdmission(hqRoot, lane.senior.id, ({ parent, children, parentGrant, availableChildGrant }) => {
867
+ return withHireGrantAdmission(hqRoot, lane.senior.id, (snapshot) => {
853
868
  const current = readLane(hqRoot, lane.lane_id);
854
869
  // A different resume already reactivated this lane; it occupies the slot
855
870
  // in the shared snapshot and is not a second hire.
856
871
  if (!isTerminalLaneState(current.state))
857
872
  return fn();
858
- if (isTerminalLaneState(parent.state)) {
859
- throw new LanesError("hire_grant_senior_inactive", `Senior lane ${parent.lane_id} is ${parent.state} and cannot resume child lanes`, { senior_lane_id: parent.lane_id, state: parent.state });
860
- }
861
- if (children.length >= (parent.hire_grant ?? 0)) {
862
- throw hireGrantCapacityError(parent, children);
863
- }
864
- const requestedGrant = current.hire_grant ?? 0;
865
- if (requestedGrant > availableChildGrant) {
866
- throw new LanesError("hire_grant_carve_exceeded", `Senior lane ${parent.lane_id} has ${availableChildGrant} unassigned hire grant remaining, but terminal child ${current.lane_id} needs ${requestedGrant} to resume. Ask its senior for more: write an ask envelope with the desired total grant and why; the senior runs hq lanes grant ${parent.lane_id} --hire <n>.`, {
867
- senior_lane_id: parent.lane_id,
868
- parent_grant: parentGrant,
869
- alive_children: children.map((child) => child.lane_id),
870
- assigned_child_grants: assignedChildHireGrants(children),
871
- requested_hire_grant: requestedGrant,
872
- available_hire_grant: availableChildGrant,
873
- ask_guidance: {
874
- envelope: "decision=ask; include the desired total grant and the reason",
875
- senior_command: `hq lanes grant ${parent.lane_id} --hire <n>`,
876
- },
877
- });
878
- }
873
+ const refusal = terminalChildResumeHireGrantError(current, snapshot);
874
+ if (refusal !== null)
875
+ throw refusal;
879
876
  return fn();
880
877
  });
881
878
  }
@@ -1004,6 +1001,7 @@ async function loadHostCapacitySnapshots(ctx, hqRoot, config = loadLanesConfig(h
1004
1001
  lanes: loadAllLanes(hqRoot),
1005
1002
  config,
1006
1003
  existsSync: ctx.existsSync,
1004
+ hostname: ctx.hostname(),
1007
1005
  now: ctx.now(),
1008
1006
  });
1009
1007
  const remote = await Promise.all(remoteIds.map(async (id) => {
@@ -1118,6 +1116,7 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
1118
1116
  existsSync: ctx.existsSync,
1119
1117
  probe: ctx.probeProvider,
1120
1118
  env: ctx.env,
1119
+ hostname: ctx.hostname(),
1121
1120
  now: ctx.now(),
1122
1121
  });
1123
1122
  const snapshots = [{
@@ -1140,6 +1139,7 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
1140
1139
  existsSync: ctx.existsSync,
1141
1140
  probe: ctx.probeProvider,
1142
1141
  env: ctx.env,
1142
+ hostname: ctx.hostname(),
1143
1143
  now: ctx.now(),
1144
1144
  }),
1145
1145
  lanes: actualLocalLanes,
@@ -2196,6 +2196,7 @@ export function applyOutpostAccountPickup(hqRoot, laneId, modelDeps = {}) {
2196
2196
  try {
2197
2197
  const resolved = resolveAccount(lane.provider, lane.account, config, {
2198
2198
  hqRoot,
2199
+ hostname: os.hostname(),
2199
2200
  accountDeps: {
2200
2201
  probe: modelDeps.probe,
2201
2202
  env: modelDeps.env,
@@ -2203,6 +2204,7 @@ export function applyOutpostAccountPickup(hqRoot, laneId, modelDeps = {}) {
2203
2204
  });
2204
2205
  const status = accountStatus(lane.provider, resolved.id, config, {
2205
2206
  existsSync: (p) => fs.existsSync(p),
2207
+ hostname: os.hostname(),
2206
2208
  probe: modelDeps.probe,
2207
2209
  env: modelDeps.env,
2208
2210
  });
@@ -2449,6 +2451,11 @@ async function runCreate(ctx, opts) {
2449
2451
  const profileEffort = profileDefaultsApply ? profile?.meta.effort : undefined;
2450
2452
  const profileAccount = profileDefaultsApply ? profile?.meta.account : undefined;
2451
2453
  const profileAccounts = profileDefaultsApply ? profile?.meta.accounts : undefined;
2454
+ const keepAlive = opts.keepAlive === true ||
2455
+ (profileDefaultsApply && profile?.meta.keep_alive === true);
2456
+ if (keepAlive && !providerSupportsSessionResume(provider)) {
2457
+ throw new LanesError("keep_alive_provider_unsupported", `Provider ${provider} cannot resume recorded sessions, so keep-alive is unavailable`, { provider });
2458
+ }
2452
2459
  const briefAbs = resolveCallerPath(ctx.callerCwd(), briefFile);
2453
2460
  if (!ctx.existsSync(briefAbs)) {
2454
2461
  throw new LanesError("brief_not_found", `Brief file not found: ${briefAbs}`, {
@@ -2589,6 +2596,7 @@ async function runCreate(ctx, opts) {
2589
2596
  }
2590
2597
  const resolved = resolveAccount(provider, requested, config, {
2591
2598
  hqRoot,
2599
+ hostname: ctx.hostname(),
2592
2600
  cliModel,
2593
2601
  model: cliModel ?? profileModel,
2594
2602
  });
@@ -2597,6 +2605,7 @@ async function runCreate(ctx, opts) {
2597
2605
  else if (profileAccount !== undefined && profileAccounts === undefined) {
2598
2606
  const resolved = resolveAccount(provider, profileAccount, config, {
2599
2607
  hqRoot,
2608
+ hostname: ctx.hostname(),
2600
2609
  cliModel,
2601
2610
  model: cliModel ?? profileModel,
2602
2611
  });
@@ -2650,6 +2659,7 @@ async function runCreate(ctx, opts) {
2650
2659
  const candidate = pinnedAccount ??
2651
2660
  resolveAccount(provider, undefined, config, {
2652
2661
  hqRoot,
2662
+ hostname: ctx.hostname(),
2653
2663
  existsSync: ctx.existsSync,
2654
2664
  accountDeps: { probe: ctx.probeProvider, env: ctx.env },
2655
2665
  cliModel,
@@ -2669,6 +2679,7 @@ async function runCreate(ctx, opts) {
2669
2679
  (profileAccounts === undefined || profileAccounts.includes(id)) &&
2670
2680
  accountStatus(provider, id, config, {
2671
2681
  existsSync: ctx.existsSync,
2682
+ hostname: ctx.hostname(),
2672
2683
  probe: ctx.probeProvider,
2673
2684
  env: ctx.env,
2674
2685
  }).logged_in === true)
@@ -2679,7 +2690,10 @@ async function runCreate(ctx, opts) {
2679
2690
  const quotaBlocked = [];
2680
2691
  const quota = loadQuotaState(hqRoot);
2681
2692
  for (const id of toValidate) {
2682
- const home = accounts[id]?.home;
2693
+ const account = accounts[id];
2694
+ const home = account === undefined
2695
+ ? undefined
2696
+ : effectiveAccountHome(account, ctx.hostname()).home;
2683
2697
  if (home === undefined)
2684
2698
  continue;
2685
2699
  checkedAccounts.push(id);
@@ -2782,6 +2796,7 @@ async function runCreate(ctx, opts) {
2782
2796
  existsSync: ctx.existsSync,
2783
2797
  probe: ctx.probeProvider,
2784
2798
  env: ctx.env,
2799
+ hostname: ctx.hostname(),
2785
2800
  now: ctx.now(),
2786
2801
  }),
2787
2802
  lanes: localLanes,
@@ -2805,6 +2820,7 @@ async function runCreate(ctx, opts) {
2805
2820
  pinnedAccount = resolveAccount(provider, undefined, config, {
2806
2821
  hqRoot,
2807
2822
  existsSync: ctx.existsSync,
2823
+ hostname: ctx.hostname(),
2808
2824
  accountDeps: { probe: ctx.probeProvider, env: ctx.env },
2809
2825
  cliModel,
2810
2826
  model: cliModel ?? profileModel,
@@ -2898,6 +2914,7 @@ async function runCreate(ctx, opts) {
2898
2914
  worker_profile_ignored: ignoredWorkerProfileFields(provider),
2899
2915
  }
2900
2916
  : {}),
2917
+ keep_alive: keepAlive,
2901
2918
  provider,
2902
2919
  account: pinnedAccount,
2903
2920
  ...(accountSelection !== undefined ? { account_selection: accountSelection } : {}),
@@ -3353,6 +3370,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3353
3370
  model: options.model,
3354
3371
  effort: options.effort,
3355
3372
  worker: options.worker,
3373
+ keepAlive: options.keepAlive,
3356
3374
  hire: options.hire,
3357
3375
  grantContext,
3358
3376
  compact: options.compact,
@@ -4029,6 +4047,7 @@ async function runCapacity(ctx) {
4029
4047
  existsSync: ctx.existsSync,
4030
4048
  probe: ctx.probeProvider,
4031
4049
  env: ctx.env,
4050
+ hostname: ctx.hostname(),
4032
4051
  now: ctx.now(),
4033
4052
  });
4034
4053
  printJson(snapshot);
@@ -4478,6 +4497,13 @@ async function runShow(ctx, laneId) {
4478
4497
  effort_source: lane.effort_source ?? null,
4479
4498
  terminal_reason: lane.terminal_reason ?? null,
4480
4499
  terminal_details: lane.terminal_details ?? null,
4500
+ keep_alive_state: {
4501
+ enabled: lane.keep_alive === true,
4502
+ consecutive_auto_resumes: lane.keep_alive_resume_count ?? 0,
4503
+ last_auto_resume_at: lane.keep_alive_last_resume_at ?? null,
4504
+ next_eligible_at: lane.keep_alive_next_eligible_at ?? null,
4505
+ exhausted_at: lane.keep_alive_exhausted_at ?? null,
4506
+ },
4481
4507
  last_message_at: lane.last_message_at ?? null,
4482
4508
  last_compaction: lane.last_compaction
4483
4509
  ? { ...lane.last_compaction, after: compactionAfterUsage(storedLane) }
@@ -4873,6 +4899,7 @@ function validateResumeProfileModel(ctx, lane, profile) {
4873
4899
  const account = resolveAccount(lane.provider, lane.account, config, {
4874
4900
  hqRoot,
4875
4901
  existsSync: ctx.existsSync,
4902
+ hostname: ctx.hostname(),
4876
4903
  model: profile.meta.model,
4877
4904
  });
4878
4905
  validateProfileLaneModel(lane.provider, profile.meta.model, account.home, profile.id, {
@@ -4891,6 +4918,9 @@ function applyResumeProfileAndEffort(current, next, input) {
4891
4918
  next.worker_profile_hash = profile.contentHash;
4892
4919
  next.worker_profile_ignored = ignoredWorkerProfileFields(current.provider);
4893
4920
  next.worker_policy_delivery = undefined;
4921
+ if (profile.meta.keep_alive !== undefined) {
4922
+ next.keep_alive = profile.meta.keep_alive;
4923
+ }
4894
4924
  if (next.model_source !== "cli") {
4895
4925
  if (profile.meta.provider === current.provider && profile.meta.model !== undefined) {
4896
4926
  next.model = profile.meta.model;
@@ -4906,6 +4936,8 @@ function applyResumeProfileAndEffort(current, next, input) {
4906
4936
  next.effort = input.effort.effort;
4907
4937
  next.effort_source = input.effort.source;
4908
4938
  }
4939
+ if (input.keepAlive !== undefined)
4940
+ next.keep_alive = input.keepAlive;
4909
4941
  }
4910
4942
  function recordResumeSetup(ctx, lane, input) {
4911
4943
  if (input.cliModel !== undefined) {
@@ -4919,7 +4951,7 @@ function recordResumeSetup(ctx, lane, input) {
4919
4951
  if (account === undefined) {
4920
4952
  throw new AccountResolutionError("unknown_account", `Lane ${lane.lane_id} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`, { provider: lane.provider, account: lane.account ?? "", enabled: [] });
4921
4953
  }
4922
- validateLaneModel(lane.provider, model, account.home, {
4954
+ validateLaneModel(lane.provider, model, effectiveAccountHome(account, ctx.hostname()).home, {
4923
4955
  readFileSync: ctx.readFileSync,
4924
4956
  probe: ctx.probeProvider,
4925
4957
  env: ctx.env,
@@ -4927,6 +4959,7 @@ function recordResumeSetup(ctx, lane, input) {
4927
4959
  }
4928
4960
  if (input.profile === undefined &&
4929
4961
  input.effort === undefined &&
4962
+ input.keepAlive === undefined &&
4930
4963
  input.cliModel === undefined &&
4931
4964
  input.meshCompany === undefined)
4932
4965
  return lane;
@@ -4949,6 +4982,23 @@ function recordResumeSetup(ctx, lane, input) {
4949
4982
  return next;
4950
4983
  });
4951
4984
  }
4985
+ function hasKeepAliveProgress(lane) {
4986
+ return (lane.keep_alive_resume_count ?? 0) !== 0 ||
4987
+ lane.keep_alive_last_resume_at !== undefined ||
4988
+ lane.keep_alive_next_eligible_at !== undefined ||
4989
+ lane.keep_alive_exhausted_at !== undefined;
4990
+ }
4991
+ function resetKeepAliveProgress(lane) {
4992
+ if (!hasKeepAliveProgress(lane))
4993
+ return lane;
4994
+ return {
4995
+ ...lane,
4996
+ keep_alive_resume_count: 0,
4997
+ keep_alive_last_resume_at: undefined,
4998
+ keep_alive_next_eligible_at: undefined,
4999
+ keep_alive_exhausted_at: undefined,
5000
+ };
5001
+ }
4952
5002
  /** Resolve an explicit resume override or newly attached profile like create does. */
4953
5003
  function resolveResumeEffort(ctx, lane, input) {
4954
5004
  if (input.cliEffort === undefined && input.profile === undefined)
@@ -5039,11 +5089,13 @@ async function prepareAccountMove(ctx, lane, input) {
5039
5089
  if (targetCfg === undefined) {
5040
5090
  throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(target)} for ${lane.provider}. Configured: ${Object.keys(providerCfg.accounts).join(", ") || "(none)"}`, { provider: lane.provider, account: target, enabled: Object.keys(providerCfg.accounts).filter((id) => providerCfg.accounts[id]?.enabled) });
5041
5091
  }
5092
+ const targetHome = effectiveAccountHome(targetCfg, ctx.hostname()).home;
5042
5093
  if (!targetCfg.enabled) {
5043
5094
  throw new AccountResolutionError("account_disabled", `Account ${JSON.stringify(target)} is disabled${targetCfg.disabled_reason ? `: ${targetCfg.disabled_reason}` : ""}`, { provider: lane.provider, account: target, reason: targetCfg.disabled_reason ?? "" });
5044
5095
  }
5045
5096
  if (accountStatus(lane.provider, target, config, {
5046
5097
  existsSync: ctx.existsSync,
5098
+ hostname: ctx.hostname(),
5047
5099
  probe: ctx.probeProvider,
5048
5100
  env: ctx.env,
5049
5101
  }).logged_in !== true) {
@@ -5060,7 +5112,7 @@ async function prepareAccountMove(ctx, lane, input) {
5060
5112
  : undefined;
5061
5113
  const profileModel = profileDefaultsApply ? profile?.meta.model : undefined;
5062
5114
  if (input.cliModel === undefined && profileDefaultsApply && profileModel !== undefined && pinnedModel === undefined) {
5063
- validateProfileLaneModel(lane.provider, profileModel, targetCfg.home, profile.id, {
5115
+ validateProfileLaneModel(lane.provider, profileModel, targetHome, profile.id, {
5064
5116
  readFileSync: ctx.readFileSync,
5065
5117
  probe: ctx.probeProvider,
5066
5118
  env: ctx.env,
@@ -5086,7 +5138,7 @@ async function prepareAccountMove(ctx, lane, input) {
5086
5138
  throw new LanesError("account_quota_held", `Account ${target} for ${lane.provider} is held${block.quota_reset_at ? ` until ${block.quota_reset_at}` : ""}${block.marker ? ` (${block.marker})` : ""}`, { provider: lane.provider, account: target, quota_reset_at: block.quota_reset_at ?? null, marker: block.marker ?? null, model: model ?? null });
5087
5139
  }
5088
5140
  if (model !== undefined) {
5089
- validateLaneModel(lane.provider, model, targetCfg.home, {
5141
+ validateLaneModel(lane.provider, model, targetHome, {
5090
5142
  readFileSync: ctx.readFileSync,
5091
5143
  probe: ctx.probeProvider,
5092
5144
  env: ctx.env,
@@ -5096,14 +5148,15 @@ async function prepareAccountMove(ctx, lane, input) {
5096
5148
  if (sourceCfg === undefined) {
5097
5149
  throw new AccountResolutionError("unknown_account", `Lane ${laneId} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`, { provider: lane.provider, account: lane.account ?? "", enabled: [] });
5098
5150
  }
5151
+ const sourceHome = effectiveAccountHome(sourceCfg, ctx.hostname()).home;
5099
5152
  // Checks done. Copy the session files, then write the record.
5100
5153
  const copy = ensureSessionHome({
5101
5154
  provider: lane.provider,
5102
5155
  sessionId,
5103
5156
  cwd: hqRoot,
5104
- targetHome: targetCfg.home,
5105
- registeredHomes: Object.values(providerCfg.accounts).map((account) => account.home),
5106
- providerHomeAtSpawn: lane.provider_home_at_spawn ?? sourceCfg.home,
5157
+ targetHome,
5158
+ registeredHomes: Object.values(providerCfg.accounts).map((account) => effectiveAccountHome(account, ctx.hostname()).home),
5159
+ providerHomeAtSpawn: lane.provider_home_at_spawn ?? sourceHome,
5107
5160
  });
5108
5161
  const at = now.toISOString();
5109
5162
  const from = lane.account ?? null;
@@ -5186,6 +5239,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
5186
5239
  }
5187
5240
  if (accountStatus("claude", lane.account, config, {
5188
5241
  existsSync: ctx.existsSync,
5242
+ hostname: ctx.hostname(),
5189
5243
  probe: ctx.probeProvider,
5190
5244
  env: ctx.env,
5191
5245
  }).logged_in === false) {
@@ -5212,6 +5266,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
5212
5266
  ? false
5213
5267
  : accountStatus("claude", lane.account, config, {
5214
5268
  existsSync: ctx.existsSync,
5269
+ hostname: ctx.hostname(),
5215
5270
  probe: ctx.probeProvider,
5216
5271
  env: ctx.env,
5217
5272
  }).logged_in;
@@ -5274,6 +5329,7 @@ function assertCurrentResumeAccountAvailable(ctx, lane) {
5274
5329
  }
5275
5330
  if (accountStatus(lane.provider, lane.account, config, {
5276
5331
  existsSync: ctx.existsSync,
5332
+ hostname: ctx.hostname(),
5277
5333
  probe: ctx.probeProvider,
5278
5334
  env: ctx.env,
5279
5335
  }).logged_in !== true) {
@@ -5538,7 +5594,7 @@ async function runMessage(ctx, laneId, opts) {
5538
5594
  }
5539
5595
  // Every accepted message moves the compaction reference forward.
5540
5596
  let next = casUpdateLane(hqRoot, laneId, (current) => ({
5541
- ...current,
5597
+ ...resetKeepAliveProgress(current),
5542
5598
  last_message_at: at,
5543
5599
  }));
5544
5600
  if (projectId !== lane.project_id || storyId !== lane.story_id) {
@@ -5876,6 +5932,9 @@ async function runResume(ctx, laneId, opts) {
5876
5932
  const resumeProfile = opts.worker !== undefined
5877
5933
  ? resolveResumeProfile(ctx, lane, opts.worker)
5878
5934
  : undefined;
5935
+ const keepAlive = opts.keepAlive === true
5936
+ ? true
5937
+ : resumeProfile?.profile.meta.keep_alive;
5879
5938
  const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
5880
5939
  if (keepCurrentAccount)
5881
5940
  assertCurrentResumeAccountAvailable(ctx, lane);
@@ -5933,6 +5992,7 @@ async function runResume(ctx, laneId, opts) {
5933
5992
  lane = recordResumeSetup(ctx, lane, {
5934
5993
  ...(move === null && resumeProfile !== undefined ? { profile: resumeProfile } : {}),
5935
5994
  ...(move === null && resumeEffort !== undefined ? { effort: resumeEffort } : {}),
5995
+ ...(keepAlive !== undefined ? { keepAlive } : {}),
5936
5996
  ...(move === null && keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
5937
5997
  ...(requestedMeshCompany !== undefined
5938
5998
  ? { meshCompany: requestedMeshCompany, threadId: meshThreadId }
@@ -6028,7 +6088,7 @@ async function runResume(ctx, laneId, opts) {
6028
6088
  if (text !== undefined) {
6029
6089
  // An operator text moves the compaction reference forward, as a message does.
6030
6090
  next = casUpdateLane(hqRoot, laneId, (current) => ({
6031
- ...current,
6091
+ ...resetKeepAliveProgress(current),
6032
6092
  last_message_at: at,
6033
6093
  }));
6034
6094
  }
@@ -6063,6 +6123,9 @@ async function runResume(ctx, laneId, opts) {
6063
6123
  };
6064
6124
  });
6065
6125
  }
6126
+ if (text === undefined && hasKeepAliveProgress(next)) {
6127
+ next = casUpdateLane(hqRoot, laneId, (current) => resetKeepAliveProgress(current));
6128
+ }
6066
6129
  printJson({
6067
6130
  ok: true,
6068
6131
  action: "resume",
@@ -6419,7 +6482,7 @@ function drainHookStatusFor(ctx, lane) {
6419
6482
  const account = config.providers[lane.provider].accounts[lane.account];
6420
6483
  if (account === undefined)
6421
6484
  return null;
6422
- const home = canonicalizeHome(account.home, {
6485
+ const home = canonicalizeHome(effectiveAccountHome(account, ctx.hostname()).home, {
6423
6486
  homedir: ctx.homedir,
6424
6487
  realpathSync: (p) => fs.realpathSync(p),
6425
6488
  existsSync: (p) => fs.existsSync(p),
@@ -7671,6 +7734,7 @@ async function runAccountsList(ctx, opts) {
7671
7734
  const quota = loadQuotaState(hqRoot);
7672
7735
  const accounts = providers.flatMap((provider) => listAccountStatuses(provider, config, {
7673
7736
  existsSync: ctx.existsSync,
7737
+ hostname: ctx.hostname(),
7674
7738
  probe: ctx.probeProvider,
7675
7739
  env: ctx.env,
7676
7740
  }).map((status) => {
@@ -7686,6 +7750,7 @@ async function runAccountsList(ctx, opts) {
7686
7750
  });
7687
7751
  return {
7688
7752
  ...status,
7753
+ effective_home: status.home,
7689
7754
  model: accountCfg?.model ?? null,
7690
7755
  effort: accountCfg?.effort ?? null,
7691
7756
  effective_model: model ?? null,
@@ -7849,9 +7914,23 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
7849
7914
  throw new LanesError("device_auth_unsupported", "--device-auth is supported only for codex account login", { provider });
7850
7915
  }
7851
7916
  const registeredAccount = config.providers[provider].accounts[id];
7852
- const homeRaw = opts.home?.trim() ||
7853
- registeredAccount?.home ||
7854
- (id === "a" ? DEFAULT_ACCOUNT_HOMES[provider] : `~/.${provider}-${id}`);
7917
+ const hostname = ctx.hostname().trim();
7918
+ if (!hostname) {
7919
+ throw new LanesError("host_identity_unknown", "Cannot resolve this host's hostname for account login", {});
7920
+ }
7921
+ const requestedHome = opts.home?.trim();
7922
+ if (opts.home !== undefined && !requestedHome) {
7923
+ throw new LanesError("invalid_account_home", "--home must be a non-empty path", { provider, account: id });
7924
+ }
7925
+ const hostScopedWriterEnabled = registeredAccount !== undefined && requestedHome !== undefined
7926
+ ? await hostScopedAccountHomesEnabled(ctx)
7927
+ : false;
7928
+ const homeRaw = requestedHome ??
7929
+ (registeredAccount !== undefined
7930
+ ? effectiveAccountHome(registeredAccount, hostname).home
7931
+ : id === "a"
7932
+ ? DEFAULT_ACCOUNT_HOMES[provider]
7933
+ : `~/.${provider}-${id}`);
7855
7934
  const homeResolved = homeRaw.startsWith("~") || path.isAbsolute(homeRaw)
7856
7935
  ? homeRaw
7857
7936
  : resolveCallerPath(callerCwd, homeRaw);
@@ -7860,20 +7939,43 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
7860
7939
  realpathSync: (p) => fs.realpathSync(p),
7861
7940
  existsSync: (p) => fs.existsSync(p),
7862
7941
  });
7863
- for (const [existingId, account] of Object.entries(config.providers[provider].accounts)) {
7864
- if (existingId === id)
7865
- continue;
7866
- if (account.home === home) {
7867
- throw new LanesError("duplicate_account_home", `Home ${home} is already registered as ${provider} account ${existingId}`, { provider, accounts: [existingId, id], home });
7942
+ const nextAccount = registeredAccount === undefined
7943
+ ? { home, enabled: true }
7944
+ : { ...registeredAccount, enabled: true };
7945
+ if (registeredAccount !== undefined) {
7946
+ if (hostScopedWriterEnabled && requestedHome !== undefined) {
7947
+ if (home === registeredAccount.home) {
7948
+ if (nextAccount.hosts !== undefined) {
7949
+ delete nextAccount.hosts[hostname];
7950
+ if (Object.keys(nextAccount.hosts).length === 0)
7951
+ delete nextAccount.hosts;
7952
+ }
7953
+ }
7954
+ else {
7955
+ nextAccount.hosts = {
7956
+ ...(registeredAccount.hosts ?? {}),
7957
+ [hostname]: { home },
7958
+ };
7959
+ }
7960
+ }
7961
+ else if (requestedHome !== undefined) {
7962
+ // With the rollout off, retain the legacy shared-home write shape.
7963
+ nextAccount.home = home;
7868
7964
  }
7869
7965
  }
7966
+ delete nextAccount.disabled_reason;
7967
+ assertUniqueAccountHomes(provider, {
7968
+ ...config.providers[provider].accounts,
7969
+ [id]: nextAccount,
7970
+ });
7870
7971
  ensureDir(home);
7871
7972
  const defaultId = config.providers[provider].default_account;
7872
- const defaultHome = (defaultId !== undefined
7873
- ? config.providers[provider].accounts[defaultId]?.home
7973
+ const defaultAccount = defaultId !== undefined
7974
+ ? config.providers[provider].accounts[defaultId]
7975
+ : Object.values(config.providers[provider].accounts).find((account) => account.enabled);
7976
+ const defaultHome = (defaultAccount !== undefined
7977
+ ? effectiveAccountHome(defaultAccount, hostname).home
7874
7978
  : undefined) ??
7875
- Object.values(config.providers[provider].accounts).find((account) => account.enabled)
7876
- ?.home ??
7877
7979
  canonicalizeHome(DEFAULT_ACCOUNT_HOMES[provider], {
7878
7980
  homedir: ctx.homedir,
7879
7981
  realpathSync: (p) => fs.realpathSync(p),
@@ -7899,12 +8001,10 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
7899
8001
  if ((grokNeedsRails || copied) && !railsPresent(home)) {
7900
8002
  throw new LanesError("rails_missing", `Hook rails are missing under ${home}; account not enabled`, { provider, account: id, home });
7901
8003
  }
7902
- config.providers[provider].accounts[id] = {
7903
- home,
7904
- enabled: true,
7905
- };
8004
+ config.providers[provider].accounts[id] = nextAccount;
7906
8005
  const status = accountStatus(provider, id, config, {
7907
8006
  existsSync: ctx.existsSync,
8007
+ hostname,
7908
8008
  probe: ctx.probeProvider,
7909
8009
  env: ctx.env,
7910
8010
  });
@@ -7916,6 +8016,93 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
7916
8016
  writeLanesConfig(hqRoot, config);
7917
8017
  printJson({ ok: true, action: "accounts.login", account: status });
7918
8018
  }
8019
+ async function runAccountsSetHome(ctx, providerRaw, accountIdRaw, homeRaw, opts) {
8020
+ if (!(await hostScopedAccountHomesEnabled(ctx))) {
8021
+ 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 });
8022
+ }
8023
+ const callerCwd = ctx.callerCwd();
8024
+ const hqRoot = ctx.hqRoot();
8025
+ const provider = parseLaneProvider(providerRaw);
8026
+ const id = accountIdRaw.trim();
8027
+ if (!id) {
8028
+ throw new AccountResolutionError("unknown_account", "account-id must be non-empty", {
8029
+ provider,
8030
+ account: accountIdRaw,
8031
+ enabled: [],
8032
+ });
8033
+ }
8034
+ const hostname = opts.host === undefined ? ctx.hostname().trim() : opts.host.trim();
8035
+ if (!hostname || (opts.host !== undefined && hostname !== opts.host)) {
8036
+ throw new LanesError("invalid_host", "--host must be a non-empty hostname without surrounding whitespace", {
8037
+ host: opts.host ?? "",
8038
+ });
8039
+ }
8040
+ if (!homeRaw.trim()) {
8041
+ throw new LanesError("invalid_account_home", "home path must be a non-empty path", {
8042
+ provider,
8043
+ account: id,
8044
+ });
8045
+ }
8046
+ const config = loadLanesConfig(hqRoot);
8047
+ const account = config.providers[provider].accounts[id];
8048
+ if (account === undefined) {
8049
+ throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(id)} for ${provider}`, {
8050
+ provider,
8051
+ account: id,
8052
+ enabled: Object.keys(config.providers[provider].accounts).filter((key) => config.providers[provider].accounts[key]?.enabled),
8053
+ });
8054
+ }
8055
+ const homePath = homeRaw.trim();
8056
+ const isCurrentHost = hostname === ctx.hostname().trim();
8057
+ let home;
8058
+ if (isCurrentHost) {
8059
+ const homeResolved = homePath.startsWith("~") || path.isAbsolute(homePath)
8060
+ ? homePath
8061
+ : resolveCallerPath(callerCwd, homePath);
8062
+ home = canonicalizeHome(homeResolved, {
8063
+ homedir: ctx.homedir,
8064
+ realpathSync: (p) => fs.realpathSync(p),
8065
+ existsSync: (p) => fs.existsSync(p),
8066
+ });
8067
+ }
8068
+ else if (homePath === "~" || homePath.startsWith("~/")) {
8069
+ home = homePath;
8070
+ }
8071
+ else if (path.isAbsolute(homePath)) {
8072
+ home = path.normalize(homePath);
8073
+ }
8074
+ else {
8075
+ 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 });
8076
+ }
8077
+ for (const [otherId, other] of Object.entries(config.providers[provider].accounts)) {
8078
+ if (otherId !== id && effectiveAccountHome(other, hostname).home === home) {
8079
+ throw new LanesError("duplicate_account_home", `Home ${home} is already registered as ${provider} account ${otherId} on host ${hostname}`, { provider, accounts: [otherId, id], home, hostname });
8080
+ }
8081
+ }
8082
+ if (home === account.home) {
8083
+ if (account.hosts !== undefined) {
8084
+ delete account.hosts[hostname];
8085
+ if (Object.keys(account.hosts).length === 0)
8086
+ delete account.hosts;
8087
+ }
8088
+ }
8089
+ else {
8090
+ const hosts = Object.assign(Object.create(null), account.hosts ?? {});
8091
+ hosts[hostname] = { home };
8092
+ account.hosts = hosts;
8093
+ }
8094
+ assertUniqueAccountHomes(provider, config.providers[provider].accounts);
8095
+ writeLanesConfig(hqRoot, config);
8096
+ printJson({
8097
+ ok: true,
8098
+ action: "accounts.set-home",
8099
+ provider,
8100
+ account: id,
8101
+ hostname,
8102
+ home,
8103
+ home_source: home === account.home ? "default" : "host",
8104
+ });
8105
+ }
7919
8106
  async function runAccountsEnable(ctx, providerRaw, accountId) {
7920
8107
  const hqRoot = ctx.hqRoot();
7921
8108
  const provider = parseLaneProvider(providerRaw);
@@ -7932,6 +8119,7 @@ async function runAccountsEnable(ctx, providerRaw, accountId) {
7932
8119
  }
7933
8120
  const status = accountStatus(provider, accountId, config, {
7934
8121
  existsSync: ctx.existsSync,
8122
+ hostname: ctx.hostname(),
7935
8123
  probe: ctx.probeProvider,
7936
8124
  env: ctx.env,
7937
8125
  });
@@ -7948,6 +8136,7 @@ async function runAccountsEnable(ctx, providerRaw, accountId) {
7948
8136
  action: "accounts.enable",
7949
8137
  account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
7950
8138
  existsSync: ctx.existsSync,
8139
+ hostname: ctx.hostname(),
7951
8140
  probe: ctx.probeProvider,
7952
8141
  env: ctx.env,
7953
8142
  }),
@@ -7976,6 +8165,7 @@ async function runAccountsDisable(ctx, providerRaw, accountId, opts) {
7976
8165
  action: "accounts.disable",
7977
8166
  account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
7978
8167
  existsSync: ctx.existsSync,
8168
+ hostname: ctx.hostname(),
7979
8169
  probe: ctx.probeProvider,
7980
8170
  env: ctx.env,
7981
8171
  }),
@@ -8145,6 +8335,7 @@ export function registerLanesCommand(parent, deps = {}) {
8145
8335
  .option("--effort <level>", "Effort level; claude: low|medium|high|xhigh|max, codex: minimal|low|medium|high|xhigh|max|ultra, grok: low|medium|high|xhigh")
8146
8336
  .option("--senior <ref>", "session:<id> | lane:<id> | user")
8147
8337
  .option("--hire-grant <n>", "Maximum alive direct child lanes assigned to this lane; set by its senior")
8338
+ .option("--keep-alive", "Automatically resume done lead lanes while their story remains open")
8148
8339
  .option("--timeout <secs>", "Deadline in seconds (warns, does not kill)")
8149
8340
  .option("--priority <n>", "Queue priority; higher runs first", "1")
8150
8341
  .option("--outpost <id>", "Run on this Outpost instead of the local host")
@@ -8382,6 +8573,7 @@ export function registerLanesCommand(parent, deps = {}) {
8382
8573
  .option("--model <id>", "Model for the moved lane (with --account only)")
8383
8574
  .option("--effort <level>", "Reasoning effort for the resumed provider turn")
8384
8575
  .option("--worker <profile>", "Attach a personal worker profile for the resumed turn")
8576
+ .option("--keep-alive", "Automatically resume done lead lanes while their story remains open")
8385
8577
  .option("--mesh-company <slug>", "Attach Work Mesh attribution after verifying company membership")
8386
8578
  .option("--no-compact", "Skip the resume-time compaction check for this resume")
8387
8579
  .action(wrap(async (laneId, opts) => {
@@ -8490,6 +8682,17 @@ export function registerLanesCommand(parent, deps = {}) {
8490
8682
  .action(wrap(async (opts) => {
8491
8683
  await runAccountsBalance(ctx, opts);
8492
8684
  }));
8685
+ accounts
8686
+ .command("set-home")
8687
+ .description("Set the host-specific home for an existing provider account")
8688
+ .argument("<provider>", "claude | codex | grok")
8689
+ .argument("<account-id>", "Existing account id")
8690
+ .argument("<path>", "Home directory for this host")
8691
+ .option("--host <hostname>", "Host key (defaults to this host's hostname)")
8692
+ .option("--json", "JSON on stdout (always)")
8693
+ .action(wrap(async (provider, accountId, home, opts) => {
8694
+ await runAccountsSetHome(ctx, provider, accountId, home, opts);
8695
+ }));
8493
8696
  accounts
8494
8697
  .command("login")
8495
8698
  .description("Create a home, run the vendor login, and register the account")