@indigoai-us/hq-cli 5.269.0 → 5.271.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.
@@ -28,7 +28,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
28
28
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
29
29
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
30
30
  import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
31
- import { resolveProcessFlagGate } from "../lib/flag-registry.js";
31
+ import { resolveProcessFlagCompanyContext } from "../lib/flag-registry.js";
32
32
  import { defaultTrackerActor, LANES_TRACKER_FLAG, registerLaneTrackerCommands, } from "./lanes-tracker.js";
33
33
  import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
34
34
  import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } from "../lib/lanes/outpost-profile-repin.js";
@@ -50,7 +50,7 @@ import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, lan
50
50
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
51
51
  import { listLaneReviews } from "../lib/lanes/reviews.js";
52
52
  import { effectiveAccountHome } from "../lib/lanes/account-home.js";
53
- import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
53
+ import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
54
54
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
55
55
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
56
56
  import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
@@ -76,12 +76,48 @@ export const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
76
76
  /** The lane → mesh seam. Defined in lib/lanes/mesh-events.ts; the watcher shares it. */
77
77
  export { emitLaneDecisionEvents };
78
78
  export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
79
- function defaultLanePlacementEnabled() {
80
- return resolveProcessFlagGate(LANE_OUTPOST_LOAD_BALANCING_FLAG, { fallback: false }, () => false);
79
+ function lanePlacementFlagErrorCode(error) {
80
+ if (error && typeof error === "object") {
81
+ for (const key of ["code", "error", "name"]) {
82
+ const value = error[key];
83
+ if (typeof value === "string" && value.trim())
84
+ return value;
85
+ }
86
+ }
87
+ return "UnknownError";
88
+ }
89
+ function reportLanePlacementFlagReadFailure(error) {
90
+ process.stderr.write(`${JSON.stringify({
91
+ event: "lanes.outpost_load_balancing.flag_read_failed",
92
+ flag: LANE_OUTPOST_LOAD_BALANCING_FLAG,
93
+ error: lanePlacementFlagErrorCode(error),
94
+ fallback: false,
95
+ })}\n`);
96
+ }
97
+ function defaultLanePlacementEnabled(company) {
98
+ return readLanesRegistryFlag(LANE_OUTPOST_LOAD_BALANCING_FLAG, {
99
+ company,
100
+ reportFailure: reportLanePlacementFlagReadFailure,
101
+ });
102
+ }
103
+ async function readLanePlacementEnabled(ctx, company) {
104
+ try {
105
+ return await ctx.lanePlacementEnabled(company);
106
+ }
107
+ catch (error) {
108
+ reportLanePlacementFlagReadFailure(error);
109
+ return false;
110
+ }
81
111
  }
82
112
  function defaultLaneTrackerEnabled(company) {
83
113
  return readLanesRegistryFlag(LANES_TRACKER_FLAG, { company });
84
114
  }
115
+ function processLanePlacementCompany(ctx) {
116
+ const context = resolveProcessFlagCompanyContext(ctx.env);
117
+ if (!context.companySlug)
118
+ return undefined;
119
+ return { slug: context.companySlug, uid: context.companyUid ?? "" };
120
+ }
85
121
  function withoutProviderHome(value) {
86
122
  if (Array.isArray(value))
87
123
  return value.map(withoutProviderHome);
@@ -1059,6 +1095,66 @@ async function readOutpostLaneRecords(ctx, outpostId) {
1059
1095
  }
1060
1096
  return lanes;
1061
1097
  }
1098
+ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1099
+ try {
1100
+ return { lane: readLane(ctx.hqRoot(), laneId), remoteOnly: false };
1101
+ }
1102
+ catch (err) {
1103
+ if (!(err instanceof LanesError) || err.error !== "lane_not_found")
1104
+ throw err;
1105
+ }
1106
+ const separator = laneId.lastIndexOf("_");
1107
+ const hostSuffix = separator < 0 ? "" : laneId.slice(separator + 1);
1108
+ if (!hostSuffix || sanitizeHostComponent(hostSuffix) !== hostSuffix) {
1109
+ throw commandLaneNotFound(laneId, options.outpostHint === true);
1110
+ }
1111
+ let rawOutpostIds;
1112
+ try {
1113
+ rawOutpostIds = await ctx.listOutpostIds();
1114
+ }
1115
+ catch (err) {
1116
+ throw new LanesError("outpost_list_unavailable", `Could not resolve registered Outposts for lane ${laneId}: ${dispatchErrorMessage(err)}`, { lane_id: laneId });
1117
+ }
1118
+ if (!Array.isArray(rawOutpostIds)) {
1119
+ throw new LanesError("outpost_list_invalid", "Registered Outpost list is not an array", { lane_id: laneId });
1120
+ }
1121
+ const outpostIds = rawOutpostIds.map((id) => typeof id === "string" ? id.trim() : "");
1122
+ if (outpostIds.length > MAX_PLACEMENT_OUTPOSTS ||
1123
+ outpostIds.some((id) => id === "") ||
1124
+ new Set(outpostIds).size !== outpostIds.length) {
1125
+ throw new LanesError("outpost_list_invalid", "Registered Outpost list is malformed or exceeds the lane lookup limit", { lane_id: laneId, count: outpostIds.length, maximum: MAX_PLACEMENT_OUTPOSTS });
1126
+ }
1127
+ const matches = [];
1128
+ for (const outpostId of outpostIds) {
1129
+ const ownerLanes = await readOutpostLaneRecords(ctx, outpostId);
1130
+ for (const lane of ownerLanes) {
1131
+ if (lane.lane_id === laneId &&
1132
+ lane.host.kind === "local" &&
1133
+ sanitizeHostComponent(lane.host.id) === hostSuffix) {
1134
+ matches.push({ outpostId, lane });
1135
+ }
1136
+ }
1137
+ }
1138
+ if (matches.length === 0) {
1139
+ throw commandLaneNotFound(laneId, options.outpostHint === true);
1140
+ }
1141
+ if (matches.length !== 1) {
1142
+ throw new LanesError("lane_outpost_ambiguous", `More than one registered Outpost reported lane ${laneId} with the same recorded host identity`, { lane_id: laneId, outpost_ids: matches.map((match) => match.outpostId) });
1143
+ }
1144
+ const match = matches[0];
1145
+ return {
1146
+ lane: { ...match.lane, host: { kind: "outpost", id: match.outpostId } },
1147
+ remoteOnly: true,
1148
+ outpostId: match.outpostId,
1149
+ ownerHost: match.lane.host,
1150
+ };
1151
+ }
1152
+ function commandLaneNotFound(laneId, outpostHint) {
1153
+ const hint = outpostHint
1154
+ ? "; pass --outpost <id> to confirm a registered Outpost"
1155
+ : "";
1156
+ return new LanesError("lane_not_found", `Lane ${laneId} was not found locally or on a registered Outpost with a matching host suffix${hint}`, { lane_id: laneId });
1157
+ }
1062
1158
  async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
1063
1159
  const localHost = laneHostFromCtx(ctx);
1064
1160
  const localLanes = loadAllLanes(hqRoot);
@@ -1196,8 +1292,8 @@ function parsePlacementPolicy(raw) {
1196
1292
  return { kind: "outpost", id };
1197
1293
  throw new LanesError("lane_placement_invalid", `Invalid placement ${JSON.stringify(raw)}. Accepted: local, auto, outpost:<id>`, { value: raw, accepted: ["local", "auto", "outpost:<id>"] });
1198
1294
  }
1199
- function assertPlacementEnabled(ctx) {
1200
- if (ctx.lanePlacementEnabled())
1295
+ async function assertPlacementEnabled(ctx, company, readPlacementEnabled = () => readLanePlacementEnabled(ctx, company)) {
1296
+ if (await readPlacementEnabled())
1201
1297
  return;
1202
1298
  throw new LanesError("lane_placement_disabled", `Lane placement options require hq-flags gate ${LANE_OUTPOST_LOAD_BALANCING_FLAG}`, { flag: LANE_OUTPOST_LOAD_BALANCING_FLAG });
1203
1299
  }
@@ -2171,14 +2267,6 @@ async function runCreate(ctx, opts) {
2171
2267
  if (opts.outpost !== undefined && requestedPlacement !== undefined) {
2172
2268
  throw new LanesError("lane_placement_conflict", "Pass either --outpost or --placement, not both", {});
2173
2269
  }
2174
- let placementGate;
2175
- const placementEnabled = () => {
2176
- placementGate ??= ctx.lanePlacementEnabled();
2177
- return placementGate;
2178
- };
2179
- if ((requestedPlacement !== undefined || defaultPlacement !== undefined) && !placementEnabled()) {
2180
- assertPlacementEnabled(ctx);
2181
- }
2182
2270
  const companyFlag = opts.company?.trim();
2183
2271
  const meshCompanyFlag = opts.meshCompany?.trim();
2184
2272
  const project = opts.project?.trim();
@@ -2266,6 +2354,29 @@ async function runCreate(ctx, opts) {
2266
2354
  }
2267
2355
  meshCompany = resolvedMeshCompany;
2268
2356
  }
2357
+ let placementCompanyResolved = false;
2358
+ let placementCompany;
2359
+ let placementGate;
2360
+ const resolvePlacementCompany = () => {
2361
+ if (!placementCompanyResolved) {
2362
+ const projectCompany = company === null
2363
+ ? resolveProjectQuestionCompany(hqRoot, {
2364
+ lane_id: "create",
2365
+ project_id: project,
2366
+ })
2367
+ : null;
2368
+ placementCompany = company ?? projectCompany ?? meshCompany ?? undefined;
2369
+ placementCompanyResolved = true;
2370
+ }
2371
+ return placementCompany;
2372
+ };
2373
+ const placementEnabled = () => {
2374
+ placementGate ??= readLanePlacementEnabled(ctx, resolvePlacementCompany());
2375
+ return placementGate;
2376
+ };
2377
+ if (requestedPlacement !== undefined || defaultPlacement !== undefined) {
2378
+ await assertPlacementEnabled(ctx, resolvePlacementCompany(), placementEnabled);
2379
+ }
2269
2380
  const seniorCompany = companyOfSenior(hqRoot, senior, ctx);
2270
2381
  assertSameTenant(company, senior, seniorCompany);
2271
2382
  const createStoryWarnings = [];
@@ -2316,15 +2427,17 @@ async function runCreate(ctx, opts) {
2316
2427
  const callerLaneId = ctx.env.HQ_LANE_ID?.trim() || undefined;
2317
2428
  if (requestedPlacement === undefined &&
2318
2429
  outpostId === undefined &&
2319
- callerLaneId !== undefined &&
2320
- placementEnabled()) {
2430
+ callerLaneId !== undefined) {
2321
2431
  const callerLane = tryReadLane(hqRoot, callerLaneId);
2322
2432
  if (!callerLane) {
2323
- throw new LanesError("lane_not_found", `Caller lane ${callerLaneId} not found`, {
2324
- lane_id: callerLaneId,
2325
- });
2433
+ if (await placementEnabled()) {
2434
+ throw new LanesError("lane_not_found", `Caller lane ${callerLaneId} not found`, {
2435
+ lane_id: callerLaneId,
2436
+ });
2437
+ }
2326
2438
  }
2327
- if (callerLane.placement_default !== undefined) {
2439
+ else if (callerLane.placement_default !== undefined &&
2440
+ await placementEnabled()) {
2328
2441
  if (!sameLaneCompany(company, callerLane.company)) {
2329
2442
  throw new LanesError("lane_placement_tenant_mismatch", "Cannot inherit placement from a manager lane in another tenant", { caller_lane_id: callerLaneId });
2330
2443
  }
@@ -2456,7 +2569,7 @@ async function runCreate(ctx, opts) {
2456
2569
  }
2457
2570
  if (outpostId !== undefined &&
2458
2571
  profile !== undefined &&
2459
- !placementEnabled()) {
2572
+ !(await placementEnabled())) {
2460
2573
  throw new LanesError("worker_profile_outpost_unsupported", `Worker profile ${JSON.stringify(profile.id)} cannot be used with --outpost; personal profiles are local-only`, {
2461
2574
  worker: profile.id,
2462
2575
  outpost: outpostId,
@@ -3352,7 +3465,7 @@ function assertLaneRunsHereForOperatorMutation(ctx, lane, action) {
3352
3465
  return;
3353
3466
  throw new LanesError("lane_remote", `Cannot ${action} lane ${lane.lane_id} from this host; its state is owned by ${lane.host.kind}:${lane.host.id}`, { lane_id: lane.lane_id, host: lane.host, action });
3354
3467
  }
3355
- async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3468
+ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior = {}) {
3356
3469
  if (lane.host.kind !== "outpost") {
3357
3470
  throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch to`, { lane_id: lane.lane_id });
3358
3471
  }
@@ -3550,7 +3663,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3550
3663
  });
3551
3664
  }
3552
3665
  if (action === "grant") {
3553
- const ownerLane = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, remote.lane, undefined, { allowNonTerminal: true });
3666
+ const ownerLane = dispatchedOwnerLane(ctx, lane.lane_id, outpostId, remote.lane, behavior);
3554
3667
  printJson({
3555
3668
  ...remote,
3556
3669
  lane_id: ownerLane.lane_id,
@@ -3566,7 +3679,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3566
3679
  (remote.delivery_status === "worker_spawned" ||
3567
3680
  remote.delivery_status === "queued_on_target_account" ||
3568
3681
  remote.delivery_status === "queued_on_admission")) {
3569
- const ownerLane = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, remote.lane, undefined, { allowNonTerminal: true });
3682
+ const ownerLane = dispatchedOwnerLane(ctx, lane.lane_id, outpostId, remote.lane, behavior);
3570
3683
  printJson({
3571
3684
  ...remote,
3572
3685
  lane_id: ownerLane.lane_id,
@@ -3578,7 +3691,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3578
3691
  return;
3579
3692
  }
3580
3693
  if (action === "approve" || action === "reparent" || action === "resume") {
3581
- const ownerLane = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, remote.lane, undefined, { allowNonTerminal: true });
3694
+ const ownerLane = dispatchedOwnerLane(ctx, lane.lane_id, outpostId, remote.lane, behavior);
3582
3695
  const descendants = [];
3583
3696
  if (action === "reparent") {
3584
3697
  for (const descendantId of changedDescendantIds) {
@@ -3606,6 +3719,32 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3606
3719
  outpost_transport: "outposts_exec",
3607
3720
  });
3608
3721
  }
3722
+ function dispatchedOwnerLane(ctx, laneId, outpostId, rawLane, behavior) {
3723
+ if (behavior.syncPrimary !== false) {
3724
+ return syncLaneRecordFromOwner(ctx.hqRoot(), laneId, outpostId, rawLane, undefined, { allowNonTerminal: true });
3725
+ }
3726
+ let ownerLane;
3727
+ try {
3728
+ ownerLane = parseLaneRecord(rawLane);
3729
+ }
3730
+ catch (err) {
3731
+ throw new LanesError("outpost_lane_sync_invalid", `Outpost ${outpostId} returned an invalid lane snapshot for ${laneId}`, { lane_id: laneId, outpost_id: outpostId, cause: dispatchErrorMessage(err) });
3732
+ }
3733
+ const expectedHost = behavior.expectedOwnerHost;
3734
+ if (!expectedHost ||
3735
+ ownerLane.lane_id !== laneId ||
3736
+ ownerLane.host.kind !== expectedHost.kind ||
3737
+ ownerLane.host.id !== expectedHost.id) {
3738
+ throw new LanesError("outpost_lane_sync_invalid", `Outpost ${outpostId} returned a lane snapshot that does not match its recorded owner identity`, {
3739
+ lane_id: laneId,
3740
+ outpost_id: outpostId,
3741
+ owner_lane_id: ownerLane.lane_id,
3742
+ owner_host: ownerLane.host,
3743
+ expected_host: expectedHost ?? null,
3744
+ });
3745
+ }
3746
+ return ownerLane;
3747
+ }
3609
3748
  function parseReparentDescendantIds(value, laneId, outpostId) {
3610
3749
  if (!Array.isArray(value) || value.some((id) => typeof id !== "string" || id.trim() === "")) {
3611
3750
  throw new LanesError("outpost_reparent_result_invalid", `Outpost ${outpostId} returned an invalid descendant list for lane ${laneId}`, { lane_id: laneId, outpost_id: outpostId });
@@ -3992,7 +4131,8 @@ async function runList(ctx, opts) {
3992
4131
  : false,
3993
4132
  };
3994
4133
  });
3995
- const host_capacity = ctx.lanePlacementEnabled()
4134
+ const placementCompany = company ?? processLanePlacementCompany(ctx);
4135
+ const host_capacity = await readLanePlacementEnabled(ctx, placementCompany)
3996
4136
  ? (await loadHostCapacitySnapshots(ctx, hqRoot)).entries
3997
4137
  : undefined;
3998
4138
  const result = {
@@ -4194,10 +4334,16 @@ function readLaneTimeout(hqRoot, laneId) {
4194
4334
  return { timeout_secs: record.timeout_secs, deadline_at: record.deadline_at };
4195
4335
  }
4196
4336
  async function runMove(ctx, laneId, opts) {
4197
- assertPlacementEnabled(ctx);
4198
4337
  const hqRoot = ctx.hqRoot();
4199
4338
  const policy = parsePlacementPolicy(opts.placement ?? "");
4200
4339
  const source = readLane(hqRoot, laneId);
4340
+ const placementCompany = resolveLaneCompany(hqRoot, source).company ?? undefined;
4341
+ let placementGate;
4342
+ const placementEnabled = () => {
4343
+ placementGate ??= readLanePlacementEnabled(ctx, placementCompany);
4344
+ return placementGate;
4345
+ };
4346
+ await assertPlacementEnabled(ctx, placementCompany, placementEnabled);
4201
4347
  if (!laneCanMoveBeforeStart(source)) {
4202
4348
  throw new LanesError(source.state === "queued" ? "lane_move_started" : "lane_not_queued", `Lane ${laneId} cannot move after it has started; current state is ${source.state}`, { lane_id: laneId, state: source.state });
4203
4349
  }
@@ -4222,7 +4368,7 @@ async function runMove(ctx, laneId, opts) {
4222
4368
  });
4223
4369
  let moved;
4224
4370
  if (laneRunsHere(source, reconcileHostFromCtx(ctx))) {
4225
- moved = await commitLaneMove(ctx, laneId, choice.host, choice.account);
4371
+ moved = await commitLaneMove(ctx, laneId, choice.host, choice.account, placementEnabled);
4226
4372
  }
4227
4373
  else if (source.host.kind === "outpost") {
4228
4374
  moved = await dispatchRemoteLaneMove(ctx, source, choice.host, choice.account);
@@ -4240,11 +4386,11 @@ async function runMove(ctx, laneId, opts) {
4240
4386
  lane: moved,
4241
4387
  });
4242
4388
  }
4243
- async function commitLaneMove(ctx, laneId, target, account) {
4244
- assertPlacementEnabled(ctx);
4389
+ async function commitLaneMove(ctx, laneId, target, account, readPlacementEnabled) {
4245
4390
  const hqRoot = ctx.hqRoot();
4246
4391
  const before = readLane(hqRoot, laneId);
4247
4392
  assertLaneRunsHereForOperatorMutation(ctx, before, "move-target");
4393
+ await assertPlacementEnabled(ctx, resolveLaneCompany(hqRoot, before).company ?? undefined, readPlacementEnabled);
4248
4394
  if (before.host.kind === target.kind && before.host.id === target.id) {
4249
4395
  throw new LanesError("lane_move_same_host", `Lane ${laneId} is already on ${target.kind}:${target.id}`, {
4250
4396
  lane_id: laneId,
@@ -4653,6 +4799,12 @@ function callerMatchesSenior(ctx, seniorRef) {
4653
4799
  (senior !== null && typeof senior === "object" && !Array.isArray(senior) &&
4654
4800
  senior.kind === "user");
4655
4801
  }
4802
+ function laneGrantTargetHostedHere(ctx, lane) {
4803
+ if (lane.host.kind === "local") {
4804
+ return laneRunsHere(lane, reconcileHostFromCtx(ctx));
4805
+ }
4806
+ return lane.host.kind === "outpost" && laneIsHostedHere(ctx, lane);
4807
+ }
4656
4808
  function forceAdmissionAuthorized(ctx, lane) {
4657
4809
  return callerMatchesSenior(ctx, lane.senior);
4658
4810
  }
@@ -4663,7 +4815,7 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
4663
4815
  throw new LanesError("hire_grant_unauthorized", `Only senior ${seniorKey(initial.senior)} may change lane ${laneId}'s hire grant`, { lane_id: laneId, senior: initial.senior });
4664
4816
  }
4665
4817
  if (authorization === "ssm-owner-dispatch" &&
4666
- (initial.host.kind !== "outpost" || !laneIsHostedHere(ctx, initial))) {
4818
+ !laneGrantTargetHostedHere(ctx, initial)) {
4667
4819
  throw new LanesError("lane_remote", `Outpost grant dispatch cannot mutate lane ${laneId} on this host`, { lane_id: laneId, host: initial.host });
4668
4820
  }
4669
4821
  if (authorization === "ssm-owner-dispatch" &&
@@ -4682,7 +4834,7 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
4682
4834
  throw new LanesError("hire_grant_unauthorized", `Only senior ${seniorKey(current.senior)} may change lane ${laneId}'s hire grant`, { lane_id: laneId, senior: current.senior });
4683
4835
  }
4684
4836
  if (authorization === "ssm-owner-dispatch" &&
4685
- (current.host.kind !== "outpost" || !laneIsHostedHere(ctx, current))) {
4837
+ !laneGrantTargetHostedHere(ctx, current)) {
4686
4838
  throw new LanesError("lane_remote", `Outpost grant dispatch cannot mutate lane ${laneId} on this host`, { lane_id: laneId, host: current.host });
4687
4839
  }
4688
4840
  if (authorization === "ssm-owner-dispatch" &&
@@ -4808,10 +4960,34 @@ async function runGrant(ctx, laneId, opts) {
4808
4960
  }
4809
4961
  const hireGrant = parseHireGrant(opts.hire);
4810
4962
  const hqRoot = ctx.hqRoot();
4811
- const initial = readLane(hqRoot, laneId);
4963
+ const target = await resolveCommandLaneTarget(ctx, laneId, { outpostHint: true });
4964
+ const initial = target.lane;
4812
4965
  if (!callerMatchesSenior(ctx, initial.senior)) {
4813
4966
  throw new LanesError("hire_grant_unauthorized", `Only senior ${seniorKey(initial.senior)} may change lane ${laneId}'s hire grant`, { lane_id: laneId, senior: initial.senior });
4814
4967
  }
4968
+ if (opts.outpost !== undefined) {
4969
+ const requestedOutpost = opts.outpost.trim();
4970
+ const recordedOutpost = target.remoteOnly
4971
+ ? target.outpostId
4972
+ : initial.host.kind === "outpost"
4973
+ ? initial.host.id
4974
+ : undefined;
4975
+ if (!requestedOutpost || requestedOutpost !== recordedOutpost) {
4976
+ throw new LanesError("lane_outpost_mismatch", `--outpost ${requestedOutpost || "<empty>"} does not match lane ${laneId}'s recorded host ${initial.host.kind}:${initial.host.id}`, {
4977
+ lane_id: laneId,
4978
+ requested_outpost_id: requestedOutpost || null,
4979
+ recorded_host: target.remoteOnly ? target.ownerHost : initial.host,
4980
+ matched_outpost_id: recordedOutpost ?? null,
4981
+ });
4982
+ }
4983
+ }
4984
+ if (target.remoteOnly) {
4985
+ await dispatchOutpostLaneCommand(ctx, initial, "grant", { hire: String(hireGrant) }, {
4986
+ syncPrimary: false,
4987
+ expectedOwnerHost: target.ownerHost,
4988
+ });
4989
+ return;
4990
+ }
4815
4991
  if (initial.host.kind === "outpost" && !laneRunsHere(initial, reconcileHostFromCtx(ctx))) {
4816
4992
  await dispatchOutpostLaneCommand(ctx, initial, "grant", { hire: String(hireGrant) });
4817
4993
  return;
@@ -6865,6 +7041,7 @@ export function registerLanesCommand(parent, deps = {}) {
6865
7041
  persistClaudeAccountSelection,
6866
7042
  printJson,
6867
7043
  recordResumeSetup,
7044
+ resolveCommandLane: (commandCtx, laneId) => resolveCommandLaneTarget(commandCtx, laneId),
6868
7045
  resolveActiveQuestionCompany,
6869
7046
  resolveResumeProfile,
6870
7047
  selectAutomaticClaudeResumeAccount,
@@ -6889,6 +7066,7 @@ export function registerLanesCommand(parent, deps = {}) {
6889
7066
  persistClaudeAccountSelection,
6890
7067
  printJson,
6891
7068
  recordResumeSetup,
7069
+ resolveCommandLane: (commandCtx, laneId) => resolveCommandLaneTarget(commandCtx, laneId),
6892
7070
  resolveResumeProfile,
6893
7071
  selectAutomaticClaudeResumeAccount,
6894
7072
  validateResumeProfileModel,
@@ -7002,7 +7180,7 @@ export function registerLanesCommand(parent, deps = {}) {
7002
7180
  .command("grant-target", { hidden: true })
7003
7181
  .argument("<lane-id>", "Lane id")
7004
7182
  .option("--hire <n>", "Non-negative safe integer hire grant")
7005
- .option("--grant-context <base64url>", "Primary-authorized hire hierarchy and gate decision")
7183
+ .option("--grant-context <base64url>", "Primary-authorized hire hierarchy context")
7006
7184
  .action(wrap(async (laneId, opts) => {
7007
7185
  await runGrantOwnerTarget(ctx, laneId, opts);
7008
7186
  }));
@@ -7022,14 +7200,15 @@ export function registerLanesCommand(parent, deps = {}) {
7022
7200
  .description("Set a lane's alive child-lane grant as its senior")
7023
7201
  .argument("<lane-id>", "Lane id")
7024
7202
  .requiredOption("--hire <n>", "Non-negative safe integer hire grant")
7203
+ .option("--outpost <id>", "Confirm the registered Outpost hosting a native lane")
7025
7204
  .action(wrap(async (laneId, opts) => {
7026
7205
  await runGrant(ctx, laneId, opts);
7027
7206
  }));
7028
7207
  lanes
7029
7208
  .command("backfill-grants")
7030
- .description("Preview or apply missing hire grants for alive manager lanes")
7209
+ .description("Preview missing hire grants for alive manager lanes; writes are disabled")
7031
7210
  .option("--dry-run", "Print proposed values without writing (default)")
7032
- .option("--apply", "Write missing hire grants; only run before enforcement is enabled")
7211
+ .option("--apply", "Retained for compatibility; writes are disabled")
7033
7212
  .action(wrap(async (opts) => {
7034
7213
  await runBackfillGrants(ctx, opts);
7035
7214
  }));
@@ -1,14 +1,11 @@
1
- export declare const PLAN_LIMIT_UPGRADE_FALLBACK_FLAG = "cli.plan-limit-upgrade-fallback";
2
- export declare const PLAN_LIMIT_UPGRADE_FALLBACK_DEFAULT = false;
3
1
  export interface PlanLimitUpgradeUrlChoices {
4
2
  topLevelUrl?: string | null;
5
3
  nestedUrl?: string | null;
6
4
  legacyUrl: string;
7
5
  }
8
- export declare function isPlanLimitUpgradeFallbackEnabled(): boolean;
9
6
  /** Resolve and attribute the final selected company billing destination. */
10
7
  export declare function selectPlanLimitUpgradeUrl(companySlug: string | undefined, choices: PlanLimitUpgradeUrlChoices): string;
11
8
  export declare function withCliLimitEntrySurface(value: string): string;
12
- /** Keep the existing destination and flag behavior while labeling company limit links. */
9
+ /** Select the standard upgrade destination and label company limit links. */
13
10
  export declare function planLimitUpgradeFallbackUrl(companySlug: string | undefined, previousFallbackUrl: string): string;
14
11
  //# sourceMappingURL=plan-limit-upgrade-url.d.ts.map
@@ -1,11 +1,5 @@
1
- import { resolveProcessFlagGate } from "../flag-registry.js";
2
1
  import { DEFAULT_VAULT_API_URL } from "../../utils/cognito-session.js";
3
2
  import { consoleOrigin } from "../../utils/console-origin.js";
4
- export const PLAN_LIMIT_UPGRADE_FALLBACK_FLAG = "cli.plan-limit-upgrade-fallback";
5
- export const PLAN_LIMIT_UPGRADE_FALLBACK_DEFAULT = false;
6
- export function isPlanLimitUpgradeFallbackEnabled() {
7
- return resolveProcessFlagGate(PLAN_LIMIT_UPGRADE_FALLBACK_FLAG, { fallback: PLAN_LIMIT_UPGRADE_FALLBACK_DEFAULT }, () => PLAN_LIMIT_UPGRADE_FALLBACK_DEFAULT);
8
- }
9
3
  function companyBillingUrl(companySlug) {
10
4
  const controlPlaneUrl = process.env.HQ_VAULT_API_URL?.trim() || DEFAULT_VAULT_API_URL;
11
5
  const origin = consoleOrigin(controlPlaneUrl);
@@ -19,20 +13,15 @@ function companyBillingUrl(companySlug) {
19
13
  }
20
14
  /** Resolve and attribute the final selected company billing destination. */
21
15
  export function selectPlanLimitUpgradeUrl(companySlug, choices) {
16
+ const topLevelUrl = choices.topLevelUrl?.trim() || undefined;
17
+ const nestedUrl = choices.nestedUrl?.trim() || undefined;
18
+ const generatedUrl = companyBillingUrl(companySlug);
22
19
  let selected;
23
- if (!isPlanLimitUpgradeFallbackEnabled()) {
24
- selected = choices.legacyUrl;
20
+ if (companySlug?.trim()) {
21
+ selected = generatedUrl ?? topLevelUrl ?? nestedUrl ?? choices.legacyUrl;
25
22
  }
26
23
  else {
27
- const topLevelUrl = choices.topLevelUrl?.trim() || undefined;
28
- const nestedUrl = choices.nestedUrl?.trim() || undefined;
29
- const generatedUrl = companyBillingUrl(companySlug);
30
- if (companySlug?.trim()) {
31
- selected = generatedUrl ?? topLevelUrl ?? nestedUrl ?? choices.legacyUrl;
32
- }
33
- else {
34
- selected = topLevelUrl ?? nestedUrl ?? generatedUrl ?? choices.legacyUrl;
35
- }
24
+ selected = topLevelUrl ?? nestedUrl ?? generatedUrl ?? choices.legacyUrl;
36
25
  }
37
26
  return withCliLimitEntrySurface(selected);
38
27
  }
@@ -68,7 +57,7 @@ export function withCliLimitEntrySurface(value) {
68
57
  }
69
58
  return value;
70
59
  }
71
- /** Keep the existing destination and flag behavior while labeling company limit links. */
60
+ /** Select the standard upgrade destination and label company limit links. */
72
61
  export function planLimitUpgradeFallbackUrl(companySlug, previousFallbackUrl) {
73
62
  return selectPlanLimitUpgradeUrl(companySlug, {
74
63
  legacyUrl: previousFallbackUrl,
@@ -7,5 +7,5 @@ export interface SyncRuntimeOptions {
7
7
  /** Classify the last hour of sync log lines into privacy-safe results. */
8
8
  export declare function analyzeSyncRuntimeLog(lines: readonly string[], options: SyncRuntimeOptions): CheckResult[];
9
9
  /** Read the machine's sync logs and client health file. */
10
- export declare function defaultSyncRuntimeResults(now: Date, hqHome?: string): CheckResult[];
10
+ export declare function defaultSyncRuntimeResults(now: Date, hqHome?: string, env?: NodeJS.ProcessEnv): CheckResult[];
11
11
  //# sourceMappingURL=sync-runtime.d.ts.map