@indigoai-us/hq-cli 5.202.1 → 5.204.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 (42) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +24 -0
  3. package/assets/lanes-hooks/hq-lanes-drain.mjs +36 -15
  4. package/dist/command-catalog.generated.d.ts +195 -0
  5. package/dist/command-catalog.generated.js +249 -0
  6. package/dist/command-registration-plan.d.ts +6 -0
  7. package/dist/command-registration-plan.js +1 -0
  8. package/dist/commands/lanes.d.ts +4 -0
  9. package/dist/commands/lanes.js +350 -108
  10. package/dist/commands/monitor.d.ts +58 -0
  11. package/dist/commands/monitor.js +965 -0
  12. package/dist/lib/lanes/account-home.d.ts +8 -0
  13. package/dist/lib/lanes/account-home.js +10 -0
  14. package/dist/lib/lanes/accounts.d.ts +4 -1
  15. package/dist/lib/lanes/accounts.js +15 -10
  16. package/dist/lib/lanes/claude-account-balance.d.ts +3 -0
  17. package/dist/lib/lanes/claude-account-balance.js +19 -4
  18. package/dist/lib/lanes/config.d.ts +2 -1
  19. package/dist/lib/lanes/config.js +55 -13
  20. package/dist/lib/lanes/delivery-core.d.ts +15 -0
  21. package/dist/lib/lanes/delivery-core.js +21 -0
  22. package/dist/lib/lanes/delivery.d.ts +21 -5
  23. package/dist/lib/lanes/delivery.js +71 -7
  24. package/dist/lib/lanes/dropbox.d.ts +13 -1
  25. package/dist/lib/lanes/dropbox.js +61 -5
  26. package/dist/lib/lanes/host-capacity.d.ts +4 -0
  27. package/dist/lib/lanes/host-capacity.js +8 -1
  28. package/dist/lib/lanes/index.d.ts +3 -1
  29. package/dist/lib/lanes/index.js +2 -1
  30. package/dist/lib/lanes/liveness.d.ts +1 -0
  31. package/dist/lib/lanes/liveness.js +3 -1
  32. package/dist/lib/lanes/move-account.js +1 -1
  33. package/dist/lib/lanes/reconcile.js +20 -9
  34. package/dist/lib/lanes/spawn.d.ts +7 -3
  35. package/dist/lib/lanes/spawn.js +71 -22
  36. package/dist/lib/lanes/types.d.ts +8 -0
  37. package/dist/lib/monitor.d.ts +153 -0
  38. package/dist/lib/monitor.js +519 -0
  39. package/dist/main.js +40 -28
  40. package/dist/utils/cli-network-lifecycle.d.ts +4 -0
  41. package/dist/utils/cli-network-lifecycle.js +11 -0
  42. 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,13 +33,16 @@ 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, 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, 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";
44
+ import { dispatchLaneDelivery } from "../lib/lanes/delivery-core.js";
45
+ import { formatMonitorEvent } from "../lib/lanes/dropbox.js";
42
46
  import { accountQuotaView, loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
43
47
  import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
44
48
  import { ensureSessionHome } from "../lib/lanes/session-relink.js";
@@ -57,6 +61,7 @@ const VENDOR_LOGIN = {
57
61
  export { emitLaneDecisionEvents };
58
62
  export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
59
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";
60
65
  function defaultLanePlacementEnabled() {
61
66
  return resolveProcessFlagGate(LANE_OUTPOST_LOAD_BALANCING_FLAG, { fallback: false }, () => false);
62
67
  }
@@ -80,6 +85,15 @@ function defaultLaneOutpostQuestionVisibilityEnabled(company, env) {
80
85
  env,
81
86
  });
82
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
+ }
83
97
  function withoutProviderHome(value) {
84
98
  if (Array.isArray(value))
85
99
  return value.map(withoutProviderHome);
@@ -235,6 +249,7 @@ function resolveDeps(deps = {}) {
235
249
  throttleDeps: deps.throttleDeps ?? defaultThrottleDeps(env),
236
250
  existsSync: deps.existsSync ?? ((p) => fs.existsSync(p)),
237
251
  readFileSync: deps.readFileSync ?? ((p, encoding) => fs.readFileSync(p, encoding)),
252
+ readStdin: deps.readStdin ?? (() => fs.readFileSync(0, "utf8")),
238
253
  homedir: deps.homedir ?? (() => os.homedir()),
239
254
  mesh: deps.mesh ?? createDefaultMesh(),
240
255
  sleep: deps.sleep ?? defaultSleep,
@@ -283,6 +298,8 @@ function resolveDeps(deps = {}) {
283
298
  ((company) => defaultLaneOutpostQuestionVisibilityEnabled(company, env)),
284
299
  laneTrackerEnabled: deps.laneTrackerEnabled ?? defaultLaneTrackerEnabled,
285
300
  laneTrackerMeshEnabled: deps.laneTrackerMeshEnabled ?? defaultLaneTrackerMeshEnabled,
301
+ laneHostScopedAccountHomesEnabled: deps.laneHostScopedAccountHomesEnabled ??
302
+ ((company) => defaultLaneHostScopedAccountHomesEnabled(company, env)),
286
303
  trackerActor: deps.trackerActor ?? defaultTrackerActor,
287
304
  };
288
305
  }
@@ -295,6 +312,7 @@ function livenessDepsFromCtx(ctx) {
295
312
  env: ctx.env,
296
313
  existsSync: ctx.existsSync,
297
314
  readFileSync: ctx.readFileSync,
315
+ hostname: ctx.hostname,
298
316
  };
299
317
  }
300
318
  function reconcileHostFromCtx(ctx) {
@@ -1001,6 +1019,7 @@ async function loadHostCapacitySnapshots(ctx, hqRoot, config = loadLanesConfig(h
1001
1019
  lanes: loadAllLanes(hqRoot),
1002
1020
  config,
1003
1021
  existsSync: ctx.existsSync,
1022
+ hostname: ctx.hostname(),
1004
1023
  now: ctx.now(),
1005
1024
  });
1006
1025
  const remote = await Promise.all(remoteIds.map(async (id) => {
@@ -1115,6 +1134,7 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
1115
1134
  existsSync: ctx.existsSync,
1116
1135
  probe: ctx.probeProvider,
1117
1136
  env: ctx.env,
1137
+ hostname: ctx.hostname(),
1118
1138
  now: ctx.now(),
1119
1139
  });
1120
1140
  const snapshots = [{
@@ -1137,6 +1157,7 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
1137
1157
  existsSync: ctx.existsSync,
1138
1158
  probe: ctx.probeProvider,
1139
1159
  env: ctx.env,
1160
+ hostname: ctx.hostname(),
1140
1161
  now: ctx.now(),
1141
1162
  }),
1142
1163
  lanes: actualLocalLanes,
@@ -2193,6 +2214,7 @@ export function applyOutpostAccountPickup(hqRoot, laneId, modelDeps = {}) {
2193
2214
  try {
2194
2215
  const resolved = resolveAccount(lane.provider, lane.account, config, {
2195
2216
  hqRoot,
2217
+ hostname: os.hostname(),
2196
2218
  accountDeps: {
2197
2219
  probe: modelDeps.probe,
2198
2220
  env: modelDeps.env,
@@ -2200,6 +2222,7 @@ export function applyOutpostAccountPickup(hqRoot, laneId, modelDeps = {}) {
2200
2222
  });
2201
2223
  const status = accountStatus(lane.provider, resolved.id, config, {
2202
2224
  existsSync: (p) => fs.existsSync(p),
2225
+ hostname: os.hostname(),
2203
2226
  probe: modelDeps.probe,
2204
2227
  env: modelDeps.env,
2205
2228
  });
@@ -2586,6 +2609,7 @@ async function runCreate(ctx, opts) {
2586
2609
  }
2587
2610
  const resolved = resolveAccount(provider, requested, config, {
2588
2611
  hqRoot,
2612
+ hostname: ctx.hostname(),
2589
2613
  cliModel,
2590
2614
  model: cliModel ?? profileModel,
2591
2615
  });
@@ -2594,6 +2618,7 @@ async function runCreate(ctx, opts) {
2594
2618
  else if (profileAccount !== undefined && profileAccounts === undefined) {
2595
2619
  const resolved = resolveAccount(provider, profileAccount, config, {
2596
2620
  hqRoot,
2621
+ hostname: ctx.hostname(),
2597
2622
  cliModel,
2598
2623
  model: cliModel ?? profileModel,
2599
2624
  });
@@ -2647,6 +2672,7 @@ async function runCreate(ctx, opts) {
2647
2672
  const candidate = pinnedAccount ??
2648
2673
  resolveAccount(provider, undefined, config, {
2649
2674
  hqRoot,
2675
+ hostname: ctx.hostname(),
2650
2676
  existsSync: ctx.existsSync,
2651
2677
  accountDeps: { probe: ctx.probeProvider, env: ctx.env },
2652
2678
  cliModel,
@@ -2666,6 +2692,7 @@ async function runCreate(ctx, opts) {
2666
2692
  (profileAccounts === undefined || profileAccounts.includes(id)) &&
2667
2693
  accountStatus(provider, id, config, {
2668
2694
  existsSync: ctx.existsSync,
2695
+ hostname: ctx.hostname(),
2669
2696
  probe: ctx.probeProvider,
2670
2697
  env: ctx.env,
2671
2698
  }).logged_in === true)
@@ -2676,7 +2703,10 @@ async function runCreate(ctx, opts) {
2676
2703
  const quotaBlocked = [];
2677
2704
  const quota = loadQuotaState(hqRoot);
2678
2705
  for (const id of toValidate) {
2679
- const home = accounts[id]?.home;
2706
+ const account = accounts[id];
2707
+ const home = account === undefined
2708
+ ? undefined
2709
+ : effectiveAccountHome(account, ctx.hostname()).home;
2680
2710
  if (home === undefined)
2681
2711
  continue;
2682
2712
  checkedAccounts.push(id);
@@ -2779,6 +2809,7 @@ async function runCreate(ctx, opts) {
2779
2809
  existsSync: ctx.existsSync,
2780
2810
  probe: ctx.probeProvider,
2781
2811
  env: ctx.env,
2812
+ hostname: ctx.hostname(),
2782
2813
  now: ctx.now(),
2783
2814
  }),
2784
2815
  lanes: localLanes,
@@ -2802,6 +2833,7 @@ async function runCreate(ctx, opts) {
2802
2833
  pinnedAccount = resolveAccount(provider, undefined, config, {
2803
2834
  hqRoot,
2804
2835
  existsSync: ctx.existsSync,
2836
+ hostname: ctx.hostname(),
2805
2837
  accountDeps: { probe: ctx.probeProvider, env: ctx.env },
2806
2838
  cliModel,
2807
2839
  model: cliModel ?? profileModel,
@@ -4026,6 +4058,7 @@ async function runCapacity(ctx) {
4026
4058
  existsSync: ctx.existsSync,
4027
4059
  probe: ctx.probeProvider,
4028
4060
  env: ctx.env,
4061
+ hostname: ctx.hostname(),
4029
4062
  now: ctx.now(),
4030
4063
  });
4031
4064
  printJson(snapshot);
@@ -4870,6 +4903,7 @@ function validateResumeProfileModel(ctx, lane, profile) {
4870
4903
  const account = resolveAccount(lane.provider, lane.account, config, {
4871
4904
  hqRoot,
4872
4905
  existsSync: ctx.existsSync,
4906
+ hostname: ctx.hostname(),
4873
4907
  model: profile.meta.model,
4874
4908
  });
4875
4909
  validateProfileLaneModel(lane.provider, profile.meta.model, account.home, profile.id, {
@@ -4916,7 +4950,7 @@ function recordResumeSetup(ctx, lane, input) {
4916
4950
  if (account === undefined) {
4917
4951
  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: [] });
4918
4952
  }
4919
- validateLaneModel(lane.provider, model, account.home, {
4953
+ validateLaneModel(lane.provider, model, effectiveAccountHome(account, ctx.hostname()).home, {
4920
4954
  readFileSync: ctx.readFileSync,
4921
4955
  probe: ctx.probeProvider,
4922
4956
  env: ctx.env,
@@ -5021,11 +5055,10 @@ async function prepareAccountMove(ctx, lane, input) {
5021
5055
  account: target,
5022
5056
  });
5023
5057
  }
5024
- if (!providerSupportsSessionResume(lane.provider)) {
5025
- throw new LanesError("provider_session_not_portable", `Provider ${lane.provider} has no session resume, so a lane cannot move between its accounts`, { lane_id: laneId, provider: lane.provider });
5026
- }
5027
- if (lane.provider === "grok") {
5028
- throw new LanesError("provider_session_not_portable", `Provider ${lane.provider} has no session resume, so a lane cannot move between its accounts`, { lane_id: laneId, provider: lane.provider });
5058
+ // The grok comparison repeats PROVIDER_SESSION_PORTABLE so TypeScript narrows
5059
+ // the provider for ensureSessionHome below.
5060
+ if (!providerSessionPortable(lane.provider) || lane.provider === "grok") {
5061
+ throw new LanesError("provider_session_not_portable", sessionNotPortableMessage(lane.provider), { lane_id: laneId, provider: lane.provider });
5029
5062
  }
5030
5063
  if (!hasRecordedSessionId(lane)) {
5031
5064
  throw new LanesError("no_provider_session", `Lane ${laneId} has no recorded provider session id, so there is no session to move`, { lane_id: laneId, provider: lane.provider });
@@ -5037,11 +5070,13 @@ async function prepareAccountMove(ctx, lane, input) {
5037
5070
  if (targetCfg === undefined) {
5038
5071
  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) });
5039
5072
  }
5073
+ const targetHome = effectiveAccountHome(targetCfg, ctx.hostname()).home;
5040
5074
  if (!targetCfg.enabled) {
5041
5075
  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 ?? "" });
5042
5076
  }
5043
5077
  if (accountStatus(lane.provider, target, config, {
5044
5078
  existsSync: ctx.existsSync,
5079
+ hostname: ctx.hostname(),
5045
5080
  probe: ctx.probeProvider,
5046
5081
  env: ctx.env,
5047
5082
  }).logged_in !== true) {
@@ -5058,7 +5093,7 @@ async function prepareAccountMove(ctx, lane, input) {
5058
5093
  : undefined;
5059
5094
  const profileModel = profileDefaultsApply ? profile?.meta.model : undefined;
5060
5095
  if (input.cliModel === undefined && profileDefaultsApply && profileModel !== undefined && pinnedModel === undefined) {
5061
- validateProfileLaneModel(lane.provider, profileModel, targetCfg.home, profile.id, {
5096
+ validateProfileLaneModel(lane.provider, profileModel, targetHome, profile.id, {
5062
5097
  readFileSync: ctx.readFileSync,
5063
5098
  probe: ctx.probeProvider,
5064
5099
  env: ctx.env,
@@ -5084,7 +5119,7 @@ async function prepareAccountMove(ctx, lane, input) {
5084
5119
  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 });
5085
5120
  }
5086
5121
  if (model !== undefined) {
5087
- validateLaneModel(lane.provider, model, targetCfg.home, {
5122
+ validateLaneModel(lane.provider, model, targetHome, {
5088
5123
  readFileSync: ctx.readFileSync,
5089
5124
  probe: ctx.probeProvider,
5090
5125
  env: ctx.env,
@@ -5094,14 +5129,15 @@ async function prepareAccountMove(ctx, lane, input) {
5094
5129
  if (sourceCfg === undefined) {
5095
5130
  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: [] });
5096
5131
  }
5132
+ const sourceHome = effectiveAccountHome(sourceCfg, ctx.hostname()).home;
5097
5133
  // Checks done. Copy the session files, then write the record.
5098
5134
  const copy = ensureSessionHome({
5099
5135
  provider: lane.provider,
5100
5136
  sessionId,
5101
5137
  cwd: hqRoot,
5102
- targetHome: targetCfg.home,
5103
- registeredHomes: Object.values(providerCfg.accounts).map((account) => account.home),
5104
- providerHomeAtSpawn: lane.provider_home_at_spawn ?? sourceCfg.home,
5138
+ targetHome,
5139
+ registeredHomes: Object.values(providerCfg.accounts).map((account) => effectiveAccountHome(account, ctx.hostname()).home),
5140
+ providerHomeAtSpawn: lane.provider_home_at_spawn ?? sourceHome,
5105
5141
  });
5106
5142
  const at = now.toISOString();
5107
5143
  const from = lane.account ?? null;
@@ -5184,6 +5220,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
5184
5220
  }
5185
5221
  if (accountStatus("claude", lane.account, config, {
5186
5222
  existsSync: ctx.existsSync,
5223
+ hostname: ctx.hostname(),
5187
5224
  probe: ctx.probeProvider,
5188
5225
  env: ctx.env,
5189
5226
  }).logged_in === false) {
@@ -5210,6 +5247,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
5210
5247
  ? false
5211
5248
  : accountStatus("claude", lane.account, config, {
5212
5249
  existsSync: ctx.existsSync,
5250
+ hostname: ctx.hostname(),
5213
5251
  probe: ctx.probeProvider,
5214
5252
  env: ctx.env,
5215
5253
  }).logged_in;
@@ -5272,6 +5310,7 @@ function assertCurrentResumeAccountAvailable(ctx, lane) {
5272
5310
  }
5273
5311
  if (accountStatus(lane.provider, lane.account, config, {
5274
5312
  existsSync: ctx.existsSync,
5313
+ hostname: ctx.hostname(),
5275
5314
  probe: ctx.probeProvider,
5276
5315
  env: ctx.env,
5277
5316
  }).logged_in !== true) {
@@ -5310,8 +5349,8 @@ async function runMessage(ctx, laneId, opts) {
5310
5349
  state: lane.state,
5311
5350
  });
5312
5351
  }
5313
- if (opts.account !== undefined && !keepCurrentAccount && !providerSupportsSessionResume(lane.provider)) {
5314
- throw new LanesError("provider_session_not_portable", `Provider ${lane.provider} has no session resume, so a lane cannot move between its accounts`, { lane_id: laneId, provider: lane.provider });
5352
+ if (opts.account !== undefined && !keepCurrentAccount && !providerSessionPortable(lane.provider)) {
5353
+ throw new LanesError("provider_session_not_portable", sessionNotPortableMessage(lane.provider), { lane_id: laneId, provider: lane.provider });
5315
5354
  }
5316
5355
  // One decision point for how a follow-up reaches this lane (delivery.ts).
5317
5356
  // `none` refuses before anything is written: no inbox append, no spawn.
@@ -5359,12 +5398,27 @@ async function runMessage(ctx, laneId, opts) {
5359
5398
  if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
5360
5399
  validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
5361
5400
  }
5401
+ const framedMonitorEvent = opts.monitorEvent === undefined
5402
+ ? undefined
5403
+ : formatMonitorEvent({
5404
+ monitorId: opts.monitorEvent.monitor_id,
5405
+ description: opts.monitorEvent.description,
5406
+ sequence: opts.monitorEvent.sequence,
5407
+ text,
5408
+ });
5409
+ const deliveryText = framedMonitorEvent ?? text;
5362
5410
  const dropboxMessage = {
5363
5411
  at,
5364
5412
  message_id: messageId,
5365
5413
  text,
5366
5414
  project_id: projectId,
5367
5415
  story_id: storyId,
5416
+ ...(opts.monitorEvent === undefined ? {} : {
5417
+ kind: "monitor_event",
5418
+ monitor_id: opts.monitorEvent.monitor_id,
5419
+ description: opts.monitorEvent.description,
5420
+ sequence: opts.monitorEvent.sequence,
5421
+ }),
5368
5422
  };
5369
5423
  let resumed = null;
5370
5424
  let move = null;
@@ -5377,7 +5431,13 @@ async function runMessage(ctx, laneId, opts) {
5377
5431
  cliModel: opts.model?.trim() || undefined,
5378
5432
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
5379
5433
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
5380
- pending: { trigger: "message", text, message_id: messageId, compact: opts.compact !== false },
5434
+ pending: {
5435
+ trigger: "message",
5436
+ text: deliveryText,
5437
+ ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
5438
+ message_id: messageId,
5439
+ compact: opts.compact !== false,
5440
+ },
5381
5441
  });
5382
5442
  lane = move.lane;
5383
5443
  if (move.queued) {
@@ -5395,7 +5455,8 @@ async function runMessage(ctx, laneId, opts) {
5395
5455
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
5396
5456
  pending: {
5397
5457
  trigger: "message",
5398
- text,
5458
+ text: deliveryText,
5459
+ ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
5399
5460
  message_id: messageId,
5400
5461
  compact: opts.compact !== false,
5401
5462
  },
@@ -5423,81 +5484,94 @@ async function runMessage(ctx, laneId, opts) {
5423
5484
  if (move?.queued) {
5424
5485
  // Parked at the target's cap: reconcile resumes the session there.
5425
5486
  }
5426
- else if (mechanism === "dropbox") {
5427
- // Live (or about-to-be-live) process: the PreToolUse drain delivers it.
5428
- if (!laneAcceptsOperatorMessage(lane)) {
5429
- throw new LanesError("lane_not_accepting_messages", operatorMessageRefusalReason(lane.state), { lane_id: laneId, state: lane.state });
5430
- }
5431
- appendDropboxMessage(hqRoot, laneId, dropboxMessage);
5432
- }
5433
5487
  else {
5434
- // Exited worker with a recorded session: queue the message when capacity
5435
- // is occupied, otherwise start its next turn under the shared admission lock.
5436
- assertLaneRunsHereForAdmission(ctx, lane);
5437
- resumed = await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
5438
- const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
5439
- if (admission.action === "refuse" && admission.error)
5440
- throw admission.error;
5441
- return withResumeHireAdmission(ctx, lane, () => {
5442
- if (admission.action === "queue") {
5443
- const sessionId = lane.provider_session_id;
5444
- if (!sessionId) {
5445
- throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this message resume`, {
5446
- lane_id: laneId,
5447
- provider: lane.provider,
5448
- });
5449
- }
5450
- archiveResumeMessage(hqRoot, laneId, dropboxMessage);
5451
- queuedOnAdmission = true;
5452
- return casUpdateLane(hqRoot, laneId, (current) => {
5453
- if (current.version !== lane.version || current.state !== lane.state) {
5454
- throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while message admission was checked`, {
5455
- lane_id: laneId,
5456
- prepared_version: lane.version,
5457
- version: current.version,
5458
- state: current.state,
5488
+ const dispatched = await dispatchLaneDelivery(lane, {
5489
+ dropbox: () => {
5490
+ // Live (or about-to-be-live) process: the PreToolUse drain delivers it.
5491
+ if (!laneAcceptsOperatorMessage(lane)) {
5492
+ throw new LanesError("lane_not_accepting_messages", operatorMessageRefusalReason(lane.state), { lane_id: laneId, state: lane.state });
5493
+ }
5494
+ appendDropboxMessage(hqRoot, laneId, dropboxMessage);
5495
+ return null;
5496
+ },
5497
+ resume: async () => {
5498
+ // Exited worker with a recorded session: queue the message when capacity
5499
+ // is occupied, otherwise start its next turn under the shared admission lock.
5500
+ assertLaneRunsHereForAdmission(ctx, lane);
5501
+ return withLaneAdmissionLock(hqRoot, lane.provider, async () => {
5502
+ const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
5503
+ if (admission.action === "refuse" && admission.error)
5504
+ throw admission.error;
5505
+ return withResumeHireAdmission(ctx, lane, () => {
5506
+ if (admission.action === "queue") {
5507
+ const sessionId = lane.provider_session_id;
5508
+ if (!sessionId) {
5509
+ throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this message resume`, {
5510
+ lane_id: laneId,
5511
+ provider: lane.provider,
5512
+ });
5513
+ }
5514
+ archiveResumeMessage(hqRoot, laneId, dropboxMessage);
5515
+ queuedOnAdmission = true;
5516
+ return casUpdateLane(hqRoot, laneId, (current) => {
5517
+ if (current.version !== lane.version || current.state !== lane.state) {
5518
+ throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while message admission was checked`, {
5519
+ lane_id: laneId,
5520
+ prepared_version: lane.version,
5521
+ version: current.version,
5522
+ state: current.state,
5523
+ });
5524
+ }
5525
+ return {
5526
+ ...current,
5527
+ state: "queued",
5528
+ queued_reason: admission.reason ?? "throttle",
5529
+ queued_at: at,
5530
+ pending_resume: {
5531
+ session_id: sessionId,
5532
+ trigger: "message",
5533
+ text: deliveryText,
5534
+ ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
5535
+ message_id: messageId,
5536
+ compact: opts.compact !== false,
5537
+ at,
5538
+ },
5539
+ };
5459
5540
  });
5460
5541
  }
5461
- return {
5462
- ...current,
5463
- state: "queued",
5464
- queued_reason: admission.reason ?? "throttle",
5465
- queued_at: at,
5466
- pending_resume: {
5467
- session_id: sessionId,
5468
- trigger: "message",
5469
- text,
5542
+ const started = spawnLaneWorker(hqRoot, lane, {
5543
+ env: ctx.env,
5544
+ modelDeps: {
5545
+ readFileSync: ctx.readFileSync,
5546
+ probe: ctx.probeProvider,
5547
+ env: ctx.env,
5548
+ },
5549
+ existsSync: ctx.existsSync,
5550
+ spawnProcess: ctx.spawnProcess,
5551
+ proveDetached: ctx.proveDetached,
5552
+ killProcess: ctx.killProcess,
5553
+ now: ctx.now,
5554
+ resolveMachineIdentity: ctx.resolveMachineIdentity,
5555
+ runCompaction: ctx.runCompaction,
5556
+ locateTranscript: ctx.locateTranscript,
5557
+ }, {
5558
+ trigger: {
5559
+ kind: "message",
5560
+ text: deliveryText,
5470
5561
  message_id: messageId,
5471
- compact: opts.compact !== false,
5472
- at,
5562
+ ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
5473
5563
  },
5474
- };
5564
+ compact: opts.compact !== false,
5565
+ effort: resumeEffort ?? recordedResumeEffort(lane),
5566
+ });
5567
+ archiveResumeMessage(hqRoot, laneId, dropboxMessage);
5568
+ return started;
5475
5569
  });
5476
- }
5477
- const started = spawnLaneWorker(hqRoot, lane, {
5478
- env: ctx.env,
5479
- modelDeps: {
5480
- readFileSync: ctx.readFileSync,
5481
- probe: ctx.probeProvider,
5482
- env: ctx.env,
5483
- },
5484
- existsSync: ctx.existsSync,
5485
- spawnProcess: ctx.spawnProcess,
5486
- proveDetached: ctx.proveDetached,
5487
- killProcess: ctx.killProcess,
5488
- now: ctx.now,
5489
- resolveMachineIdentity: ctx.resolveMachineIdentity,
5490
- runCompaction: ctx.runCompaction,
5491
- locateTranscript: ctx.locateTranscript,
5492
- }, {
5493
- trigger: { kind: "message", text, message_id: messageId },
5494
- compact: opts.compact !== false,
5495
- effort: resumeEffort ?? recordedResumeEffort(lane),
5496
- });
5497
- archiveResumeMessage(hqRoot, laneId, dropboxMessage);
5498
- return started;
5499
- });
5500
- }, ctx.sleep);
5570
+ }, ctx.sleep);
5571
+ },
5572
+ }, mechanism);
5573
+ if (dispatched.mechanism === "resume")
5574
+ resumed = dispatched.value;
5501
5575
  }
5502
5576
  // Every accepted message moves the compaction reference forward.
5503
5577
  let next = casUpdateLane(hqRoot, laneId, (current) => ({
@@ -5736,8 +5810,6 @@ async function runClaudeBalanceMoveTarget(ctx, laneId, opts) {
5736
5810
  });
5737
5811
  }
5738
5812
  function laneWithRecoveredProviderSession(hqRoot, lane) {
5739
- if (lane.provider === "grok")
5740
- return lane;
5741
5813
  const manifest = readSpawnManifest(laneRunDir(hqRoot, lane.lane_id));
5742
5814
  const sessionId = (typeof lane.provider_session_id === "string" && lane.provider_session_id.trim()
5743
5815
  ? lane.provider_session_id
@@ -5795,8 +5867,8 @@ async function runResume(ctx, laneId, opts) {
5795
5867
  state: lane.state,
5796
5868
  });
5797
5869
  }
5798
- if (opts.account !== undefined && !providerSupportsSessionResume(lane.provider)) {
5799
- throw new LanesError("provider_session_not_portable", `Provider ${lane.provider} has no session resume, so a lane cannot move between its accounts`, { lane_id: laneId, provider: lane.provider });
5870
+ if (opts.account !== undefined && !providerSessionPortable(lane.provider)) {
5871
+ throw new LanesError("provider_session_not_portable", sessionNotPortableMessage(lane.provider), { lane_id: laneId, provider: lane.provider });
5800
5872
  }
5801
5873
  if (lane.state === "queued") {
5802
5874
  throw new LanesError("lane_not_resumable", `Lane ${laneId} is queued; use hq lanes admit ${laneId} to request admission`, { lane_id: laneId, state: lane.state, admission_command: `hq lanes admit ${laneId}` });
@@ -6384,7 +6456,7 @@ function drainHookStatusFor(ctx, lane) {
6384
6456
  const account = config.providers[lane.provider].accounts[lane.account];
6385
6457
  if (account === undefined)
6386
6458
  return null;
6387
- const home = canonicalizeHome(account.home, {
6459
+ const home = canonicalizeHome(effectiveAccountHome(account, ctx.hostname()).home, {
6388
6460
  homedir: ctx.homedir,
6389
6461
  realpathSync: (p) => fs.realpathSync(p),
6390
6462
  existsSync: (p) => fs.existsSync(p),
@@ -7636,6 +7708,7 @@ async function runAccountsList(ctx, opts) {
7636
7708
  const quota = loadQuotaState(hqRoot);
7637
7709
  const accounts = providers.flatMap((provider) => listAccountStatuses(provider, config, {
7638
7710
  existsSync: ctx.existsSync,
7711
+ hostname: ctx.hostname(),
7639
7712
  probe: ctx.probeProvider,
7640
7713
  env: ctx.env,
7641
7714
  }).map((status) => {
@@ -7651,6 +7724,7 @@ async function runAccountsList(ctx, opts) {
7651
7724
  });
7652
7725
  return {
7653
7726
  ...status,
7727
+ effective_home: status.home,
7654
7728
  model: accountCfg?.model ?? null,
7655
7729
  effort: accountCfg?.effort ?? null,
7656
7730
  effective_model: model ?? null,
@@ -7814,9 +7888,23 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
7814
7888
  throw new LanesError("device_auth_unsupported", "--device-auth is supported only for codex account login", { provider });
7815
7889
  }
7816
7890
  const registeredAccount = config.providers[provider].accounts[id];
7817
- const homeRaw = opts.home?.trim() ||
7818
- registeredAccount?.home ||
7819
- (id === "a" ? DEFAULT_ACCOUNT_HOMES[provider] : `~/.${provider}-${id}`);
7891
+ const hostname = ctx.hostname().trim();
7892
+ if (!hostname) {
7893
+ throw new LanesError("host_identity_unknown", "Cannot resolve this host's hostname for account login", {});
7894
+ }
7895
+ const requestedHome = opts.home?.trim();
7896
+ if (opts.home !== undefined && !requestedHome) {
7897
+ throw new LanesError("invalid_account_home", "--home must be a non-empty path", { provider, account: id });
7898
+ }
7899
+ const hostScopedWriterEnabled = registeredAccount !== undefined && requestedHome !== undefined
7900
+ ? await hostScopedAccountHomesEnabled(ctx)
7901
+ : false;
7902
+ const homeRaw = requestedHome ??
7903
+ (registeredAccount !== undefined
7904
+ ? effectiveAccountHome(registeredAccount, hostname).home
7905
+ : id === "a"
7906
+ ? DEFAULT_ACCOUNT_HOMES[provider]
7907
+ : `~/.${provider}-${id}`);
7820
7908
  const homeResolved = homeRaw.startsWith("~") || path.isAbsolute(homeRaw)
7821
7909
  ? homeRaw
7822
7910
  : resolveCallerPath(callerCwd, homeRaw);
@@ -7825,20 +7913,43 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
7825
7913
  realpathSync: (p) => fs.realpathSync(p),
7826
7914
  existsSync: (p) => fs.existsSync(p),
7827
7915
  });
7828
- for (const [existingId, account] of Object.entries(config.providers[provider].accounts)) {
7829
- if (existingId === id)
7830
- continue;
7831
- if (account.home === home) {
7832
- throw new LanesError("duplicate_account_home", `Home ${home} is already registered as ${provider} account ${existingId}`, { provider, accounts: [existingId, id], home });
7916
+ const nextAccount = registeredAccount === undefined
7917
+ ? { home, enabled: true }
7918
+ : { ...registeredAccount, enabled: true };
7919
+ if (registeredAccount !== undefined) {
7920
+ if (hostScopedWriterEnabled && requestedHome !== undefined) {
7921
+ if (home === registeredAccount.home) {
7922
+ if (nextAccount.hosts !== undefined) {
7923
+ delete nextAccount.hosts[hostname];
7924
+ if (Object.keys(nextAccount.hosts).length === 0)
7925
+ delete nextAccount.hosts;
7926
+ }
7927
+ }
7928
+ else {
7929
+ nextAccount.hosts = {
7930
+ ...(registeredAccount.hosts ?? {}),
7931
+ [hostname]: { home },
7932
+ };
7933
+ }
7934
+ }
7935
+ else if (requestedHome !== undefined) {
7936
+ // With the rollout off, retain the legacy shared-home write shape.
7937
+ nextAccount.home = home;
7833
7938
  }
7834
7939
  }
7940
+ delete nextAccount.disabled_reason;
7941
+ assertUniqueAccountHomes(provider, {
7942
+ ...config.providers[provider].accounts,
7943
+ [id]: nextAccount,
7944
+ });
7835
7945
  ensureDir(home);
7836
7946
  const defaultId = config.providers[provider].default_account;
7837
- const defaultHome = (defaultId !== undefined
7838
- ? config.providers[provider].accounts[defaultId]?.home
7947
+ const defaultAccount = defaultId !== undefined
7948
+ ? config.providers[provider].accounts[defaultId]
7949
+ : Object.values(config.providers[provider].accounts).find((account) => account.enabled);
7950
+ const defaultHome = (defaultAccount !== undefined
7951
+ ? effectiveAccountHome(defaultAccount, hostname).home
7839
7952
  : undefined) ??
7840
- Object.values(config.providers[provider].accounts).find((account) => account.enabled)
7841
- ?.home ??
7842
7953
  canonicalizeHome(DEFAULT_ACCOUNT_HOMES[provider], {
7843
7954
  homedir: ctx.homedir,
7844
7955
  realpathSync: (p) => fs.realpathSync(p),
@@ -7864,12 +7975,10 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
7864
7975
  if ((grokNeedsRails || copied) && !railsPresent(home)) {
7865
7976
  throw new LanesError("rails_missing", `Hook rails are missing under ${home}; account not enabled`, { provider, account: id, home });
7866
7977
  }
7867
- config.providers[provider].accounts[id] = {
7868
- home,
7869
- enabled: true,
7870
- };
7978
+ config.providers[provider].accounts[id] = nextAccount;
7871
7979
  const status = accountStatus(provider, id, config, {
7872
7980
  existsSync: ctx.existsSync,
7981
+ hostname,
7873
7982
  probe: ctx.probeProvider,
7874
7983
  env: ctx.env,
7875
7984
  });
@@ -7881,6 +7990,93 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
7881
7990
  writeLanesConfig(hqRoot, config);
7882
7991
  printJson({ ok: true, action: "accounts.login", account: status });
7883
7992
  }
7993
+ async function runAccountsSetHome(ctx, providerRaw, accountIdRaw, homeRaw, opts) {
7994
+ if (!(await hostScopedAccountHomesEnabled(ctx))) {
7995
+ 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 });
7996
+ }
7997
+ const callerCwd = ctx.callerCwd();
7998
+ const hqRoot = ctx.hqRoot();
7999
+ const provider = parseLaneProvider(providerRaw);
8000
+ const id = accountIdRaw.trim();
8001
+ if (!id) {
8002
+ throw new AccountResolutionError("unknown_account", "account-id must be non-empty", {
8003
+ provider,
8004
+ account: accountIdRaw,
8005
+ enabled: [],
8006
+ });
8007
+ }
8008
+ const hostname = opts.host === undefined ? ctx.hostname().trim() : opts.host.trim();
8009
+ if (!hostname || (opts.host !== undefined && hostname !== opts.host)) {
8010
+ throw new LanesError("invalid_host", "--host must be a non-empty hostname without surrounding whitespace", {
8011
+ host: opts.host ?? "",
8012
+ });
8013
+ }
8014
+ if (!homeRaw.trim()) {
8015
+ throw new LanesError("invalid_account_home", "home path must be a non-empty path", {
8016
+ provider,
8017
+ account: id,
8018
+ });
8019
+ }
8020
+ const config = loadLanesConfig(hqRoot);
8021
+ const account = config.providers[provider].accounts[id];
8022
+ if (account === undefined) {
8023
+ throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(id)} for ${provider}`, {
8024
+ provider,
8025
+ account: id,
8026
+ enabled: Object.keys(config.providers[provider].accounts).filter((key) => config.providers[provider].accounts[key]?.enabled),
8027
+ });
8028
+ }
8029
+ const homePath = homeRaw.trim();
8030
+ const isCurrentHost = hostname === ctx.hostname().trim();
8031
+ let home;
8032
+ if (isCurrentHost) {
8033
+ const homeResolved = homePath.startsWith("~") || path.isAbsolute(homePath)
8034
+ ? homePath
8035
+ : resolveCallerPath(callerCwd, homePath);
8036
+ home = canonicalizeHome(homeResolved, {
8037
+ homedir: ctx.homedir,
8038
+ realpathSync: (p) => fs.realpathSync(p),
8039
+ existsSync: (p) => fs.existsSync(p),
8040
+ });
8041
+ }
8042
+ else if (homePath === "~" || homePath.startsWith("~/")) {
8043
+ home = homePath;
8044
+ }
8045
+ else if (path.isAbsolute(homePath)) {
8046
+ home = path.normalize(homePath);
8047
+ }
8048
+ else {
8049
+ 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 });
8050
+ }
8051
+ for (const [otherId, other] of Object.entries(config.providers[provider].accounts)) {
8052
+ if (otherId !== id && effectiveAccountHome(other, hostname).home === home) {
8053
+ throw new LanesError("duplicate_account_home", `Home ${home} is already registered as ${provider} account ${otherId} on host ${hostname}`, { provider, accounts: [otherId, id], home, hostname });
8054
+ }
8055
+ }
8056
+ if (home === account.home) {
8057
+ if (account.hosts !== undefined) {
8058
+ delete account.hosts[hostname];
8059
+ if (Object.keys(account.hosts).length === 0)
8060
+ delete account.hosts;
8061
+ }
8062
+ }
8063
+ else {
8064
+ const hosts = Object.assign(Object.create(null), account.hosts ?? {});
8065
+ hosts[hostname] = { home };
8066
+ account.hosts = hosts;
8067
+ }
8068
+ assertUniqueAccountHomes(provider, config.providers[provider].accounts);
8069
+ writeLanesConfig(hqRoot, config);
8070
+ printJson({
8071
+ ok: true,
8072
+ action: "accounts.set-home",
8073
+ provider,
8074
+ account: id,
8075
+ hostname,
8076
+ home,
8077
+ home_source: home === account.home ? "default" : "host",
8078
+ });
8079
+ }
7884
8080
  async function runAccountsEnable(ctx, providerRaw, accountId) {
7885
8081
  const hqRoot = ctx.hqRoot();
7886
8082
  const provider = parseLaneProvider(providerRaw);
@@ -7897,6 +8093,7 @@ async function runAccountsEnable(ctx, providerRaw, accountId) {
7897
8093
  }
7898
8094
  const status = accountStatus(provider, accountId, config, {
7899
8095
  existsSync: ctx.existsSync,
8096
+ hostname: ctx.hostname(),
7900
8097
  probe: ctx.probeProvider,
7901
8098
  env: ctx.env,
7902
8099
  });
@@ -7913,6 +8110,7 @@ async function runAccountsEnable(ctx, providerRaw, accountId) {
7913
8110
  action: "accounts.enable",
7914
8111
  account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
7915
8112
  existsSync: ctx.existsSync,
8113
+ hostname: ctx.hostname(),
7916
8114
  probe: ctx.probeProvider,
7917
8115
  env: ctx.env,
7918
8116
  }),
@@ -7941,6 +8139,7 @@ async function runAccountsDisable(ctx, providerRaw, accountId, opts) {
7941
8139
  action: "accounts.disable",
7942
8140
  account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
7943
8141
  existsSync: ctx.existsSync,
8142
+ hostname: ctx.hostname(),
7944
8143
  probe: ctx.probeProvider,
7945
8144
  env: ctx.env,
7946
8145
  }),
@@ -8304,6 +8503,38 @@ export function registerLanesCommand(parent, deps = {}) {
8304
8503
  .action(wrap(async (laneId, opts) => {
8305
8504
  await runMessage(ctx, laneId, opts);
8306
8505
  }));
8506
+ lanes
8507
+ .command("_monitor-message", { hidden: true })
8508
+ .argument("<lane-id>", "Lane id")
8509
+ .action(wrap(async (laneId) => {
8510
+ const input = ctx.readStdin();
8511
+ let payload;
8512
+ try {
8513
+ payload = JSON.parse(input);
8514
+ }
8515
+ catch (err) {
8516
+ throw new LanesError("monitor_lane_payload_invalid", `Monitor lane delivery payload is invalid JSON: ${err instanceof Error ? err.message : String(err)}.`, {});
8517
+ }
8518
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
8519
+ throw new LanesError("monitor_lane_payload_invalid", "Monitor lane delivery payload must be an object.", {});
8520
+ }
8521
+ const value = payload;
8522
+ if (typeof value.text !== "string" ||
8523
+ typeof value.monitor_id !== "string" ||
8524
+ typeof value.description !== "string" ||
8525
+ !Number.isInteger(value.sequence) ||
8526
+ Number(value.sequence) < 1) {
8527
+ throw new LanesError("monitor_lane_payload_invalid", "Monitor lane delivery payload is missing text, monitor metadata, or sequence.", {});
8528
+ }
8529
+ await runMessage(ctx, laneId, {
8530
+ text: value.text,
8531
+ monitorEvent: {
8532
+ monitor_id: value.monitor_id,
8533
+ description: value.description,
8534
+ sequence: Number(value.sequence),
8535
+ },
8536
+ });
8537
+ }));
8307
8538
  lanes
8308
8539
  .command("resume")
8309
8540
  .description("Resume an interrupted, done or blocked lane on its recorded provider session")
@@ -8423,6 +8654,17 @@ export function registerLanesCommand(parent, deps = {}) {
8423
8654
  .action(wrap(async (opts) => {
8424
8655
  await runAccountsBalance(ctx, opts);
8425
8656
  }));
8657
+ accounts
8658
+ .command("set-home")
8659
+ .description("Set the host-specific home for an existing provider account")
8660
+ .argument("<provider>", "claude | codex | grok")
8661
+ .argument("<account-id>", "Existing account id")
8662
+ .argument("<path>", "Home directory for this host")
8663
+ .option("--host <hostname>", "Host key (defaults to this host's hostname)")
8664
+ .option("--json", "JSON on stdout (always)")
8665
+ .action(wrap(async (provider, accountId, home, opts) => {
8666
+ await runAccountsSetHome(ctx, provider, accountId, home, opts);
8667
+ }));
8426
8668
  accounts
8427
8669
  .command("login")
8428
8670
  .description("Create a home, run the vendor login, and register the account")