@indigoai-us/hq-cli 5.270.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.
- package/CHANGELOG.md +11 -0
- package/dist/command-catalog.generated.d.ts +3 -0
- package/dist/command-catalog.generated.js +4 -0
- package/dist/commands/lanes-message.js +6 -3
- package/dist/commands/lanes-resume.js +6 -3
- package/dist/commands/lanes-shared.d.ts +20 -4
- package/dist/commands/lanes.d.ts +1 -1
- package/dist/commands/lanes.js +212 -33
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.271.0] — 2026-09-28
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `hq lanes create --outpost` reads `lanes.outpost-load-balancing` for the
|
|
10
|
+
resolved company, so enabled personal worker profiles can run on an Outpost.
|
|
11
|
+
- Resolve host-suffixed lanes created on a registered Outpost for grant,
|
|
12
|
+
message, and resume commands while keeping senior authorization on the primary.
|
|
13
|
+
- `hq lanes move-target` keeps the company placement gate, and unfiltered
|
|
14
|
+
`hq lanes list` resolves the active company for host capacity.
|
|
15
|
+
|
|
5
16
|
## [5.270.0] — 2026-09-28
|
|
6
17
|
|
|
7
18
|
### Fixed
|
|
@@ -7800,6 +7800,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
7800
7800
|
readonly options: readonly [{
|
|
7801
7801
|
readonly flags: "--hire <n>";
|
|
7802
7802
|
readonly description: "Non-negative safe integer hire grant";
|
|
7803
|
+
}, {
|
|
7804
|
+
readonly flags: "--outpost <id>";
|
|
7805
|
+
readonly description: "Confirm the registered Outpost hosting a native lane";
|
|
7803
7806
|
}, {
|
|
7804
7807
|
readonly flags: "--json";
|
|
7805
7808
|
readonly description: "JSON on stdout (always)";
|
|
@@ -10086,6 +10086,10 @@ export const COMMAND_CATALOG = [
|
|
|
10086
10086
|
"flags": "--hire <n>",
|
|
10087
10087
|
"description": "Non-negative safe integer hire grant"
|
|
10088
10088
|
},
|
|
10089
|
+
{
|
|
10090
|
+
"flags": "--outpost <id>",
|
|
10091
|
+
"description": "Confirm the registered Outpost hosting a native lane"
|
|
10092
|
+
},
|
|
10089
10093
|
{
|
|
10090
10094
|
"flags": "--json",
|
|
10091
10095
|
"description": "JSON on stdout (always)"
|
|
@@ -65,9 +65,12 @@ export function createLaneMessageHandlers(deps) {
|
|
|
65
65
|
if (!text) {
|
|
66
66
|
throw new LanesError("missing_text", "--text is required", {});
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
const resolution = await deps.resolveCommandLane(ctx, laneId);
|
|
69
|
+
let lane = resolution.lane;
|
|
70
|
+
if (resolution.remoteOnly || (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane))) {
|
|
71
|
+
await dispatchOutpostLaneCommand(ctx, lane, "message", opts, resolution.remoteOnly
|
|
72
|
+
? { syncPrimary: false, expectedOwnerHost: resolution.ownerHost }
|
|
73
|
+
: undefined);
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
73
76
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "message");
|
|
@@ -86,7 +86,8 @@ export function createLaneResumeHandlers(deps) {
|
|
|
86
86
|
*/
|
|
87
87
|
async function runResume(ctx, laneId, opts) {
|
|
88
88
|
const hqRoot = ctx.hqRoot();
|
|
89
|
-
|
|
89
|
+
const resolution = await deps.resolveCommandLane(ctx, laneId);
|
|
90
|
+
let lane = resolution.lane;
|
|
90
91
|
const failedResumeReason = opts.reason?.trim();
|
|
91
92
|
if (opts.reason !== undefined && opts.fromFailed !== true) {
|
|
92
93
|
throw new LanesError("failed_resume_reason_requires_flag", "--reason on hq lanes resume requires --from-failed", { lane_id: laneId });
|
|
@@ -122,9 +123,11 @@ export function createLaneResumeHandlers(deps) {
|
|
|
122
123
|
if (opts.profileRepinContext !== undefined && !ctx.isSsmOwnerDispatch()) {
|
|
123
124
|
throw new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is only valid on an Outpost owner dispatch", { lane_id: laneId });
|
|
124
125
|
}
|
|
125
|
-
if (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane)) {
|
|
126
|
+
if (resolution.remoteOnly || (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane))) {
|
|
126
127
|
await withResumeHireAdmission(ctx, lane, () => undefined);
|
|
127
|
-
await dispatchOutpostLaneCommand(ctx, lane, "resume", opts
|
|
128
|
+
await dispatchOutpostLaneCommand(ctx, lane, "resume", opts, resolution.remoteOnly
|
|
129
|
+
? { syncPrimary: false, expectedOwnerHost: resolution.ownerHost }
|
|
130
|
+
: undefined);
|
|
128
131
|
return;
|
|
129
132
|
}
|
|
130
133
|
// Fail before provider/session work if a terminal child would consume a
|
|
@@ -115,6 +115,21 @@ export declare function resolveResumeEffort(ctx: Ctx, lane: LaneRecord, input: {
|
|
|
115
115
|
model?: string;
|
|
116
116
|
}): ResolvedEffort | undefined;
|
|
117
117
|
export declare function recordedResumeEffort(lane: LaneRecord): ResolvedEffort;
|
|
118
|
+
export type CommandLaneResolution = {
|
|
119
|
+
lane: LaneRecord;
|
|
120
|
+
remoteOnly: false;
|
|
121
|
+
} | {
|
|
122
|
+
lane: LaneRecord;
|
|
123
|
+
remoteOnly: true;
|
|
124
|
+
outpostId: string;
|
|
125
|
+
ownerHost: LaneRecord["host"];
|
|
126
|
+
};
|
|
127
|
+
export interface OutpostLaneDispatchBehavior {
|
|
128
|
+
/** Keep remote-only lanes out of the primary lane store. */
|
|
129
|
+
syncPrimary?: boolean;
|
|
130
|
+
/** Host identity returned by the remote record lookup for a remote-only lane. */
|
|
131
|
+
expectedOwnerHost?: LaneRecord["host"];
|
|
132
|
+
}
|
|
118
133
|
export interface LaneMessageHandlerDeps {
|
|
119
134
|
archiveResumeMessage(hqRoot: string, laneId: string, message: {
|
|
120
135
|
message_id: string;
|
|
@@ -128,7 +143,7 @@ export interface LaneMessageHandlerDeps {
|
|
|
128
143
|
alreadyQueued?: boolean;
|
|
129
144
|
}): Promise<AdmissionResult>;
|
|
130
145
|
casUpdateLane(hqRoot: string, laneId: string, updater: (current: LaneRecord) => LaneRecord): LaneRecord;
|
|
131
|
-
dispatchOutpostLaneCommand(ctx: Ctx, lane: LaneRecord, action: "message" | "resume", options: LaneMessageOptions | LaneResumeOptions): Promise<void>;
|
|
146
|
+
dispatchOutpostLaneCommand(ctx: Ctx, lane: LaneRecord, action: "message" | "resume", options: LaneMessageOptions | LaneResumeOptions, behavior?: OutpostLaneDispatchBehavior): Promise<void>;
|
|
132
147
|
drainHookStatusFor(ctx: Ctx, lane: LaneRecord): {
|
|
133
148
|
registered: boolean;
|
|
134
149
|
kind: string;
|
|
@@ -142,6 +157,7 @@ export interface LaneMessageHandlerDeps {
|
|
|
142
157
|
persistClaudeAccountSelection(hqRoot: string, lane: LaneRecord, selection: LaneAccountSelection): LaneRecord;
|
|
143
158
|
printJson(value: unknown): void;
|
|
144
159
|
recordResumeSetup(ctx: Ctx, lane: LaneRecord, input: RecordResumeSetupInput): LaneRecord;
|
|
160
|
+
resolveCommandLane(ctx: Ctx, laneId: string): Promise<CommandLaneResolution>;
|
|
145
161
|
resolveActiveQuestionCompany(ctx: Ctx, message?: string): Promise<LaneCompanyRef>;
|
|
146
162
|
resolveResumeProfile(ctx: Ctx, lane: LaneRecord, workerId: string): ResolvedResumeProfile;
|
|
147
163
|
selectAutomaticClaudeResumeAccount(ctx: Ctx, lane: LaneRecord, workerProfile?: ResolvedResumeProfile, options?: {
|
|
@@ -154,8 +170,8 @@ export interface LaneMessageHandlerDeps {
|
|
|
154
170
|
validateResumeProfileModel(ctx: Ctx, lane: LaneRecord, profile: WorkerProfile): void;
|
|
155
171
|
withResumeHireAdmission<T>(ctx: Ctx, lane: LaneRecord, fn: () => T, grantContext?: OutpostHireGrantContext): Promise<T>;
|
|
156
172
|
}
|
|
157
|
-
export interface LaneResumeHandlerDeps extends Pick<LaneMessageHandlerDeps, "archiveResumeMessage" | "assertLaneRunsHereForAdmission" | "assertLaneRunsHereForOperatorMutation" | "assessLaneAdmission" | "casUpdateLane" | "laneIsHostedHere" | "laneWithRecoveredProviderSession" | "mintMessageId" | "prepareAccountMove" | "persistClaudeAccountSelection" | "printJson" | "recordResumeSetup" | "resolveResumeProfile" | "selectAutomaticClaudeResumeAccount" | "validateResumeProfileModel" | "withResumeHireAdmission"> {
|
|
158
|
-
dispatchOutpostLaneCommand(ctx: Ctx, lane: LaneRecord, action: "resume", options: LaneResumeOptions): Promise<void>;
|
|
173
|
+
export interface LaneResumeHandlerDeps extends Pick<LaneMessageHandlerDeps, "archiveResumeMessage" | "assertLaneRunsHereForAdmission" | "assertLaneRunsHereForOperatorMutation" | "assessLaneAdmission" | "casUpdateLane" | "laneIsHostedHere" | "laneWithRecoveredProviderSession" | "mintMessageId" | "prepareAccountMove" | "persistClaudeAccountSelection" | "printJson" | "recordResumeSetup" | "resolveCommandLane" | "resolveResumeProfile" | "selectAutomaticClaudeResumeAccount" | "validateResumeProfileModel" | "withResumeHireAdmission"> {
|
|
174
|
+
dispatchOutpostLaneCommand(ctx: Ctx, lane: LaneRecord, action: "resume", options: LaneResumeOptions, behavior?: OutpostLaneDispatchBehavior): Promise<void>;
|
|
159
175
|
authorizeFailedResume(ctx: Ctx, lane: LaneRecord, encodedContext?: string): string;
|
|
160
176
|
authorizeResumeProfileRepin(ctx: Ctx, lane: LaneRecord, profile: ResolvedResumeProfile | undefined, encodedContext: string | undefined): ResumeProfileRepinAuthorization | undefined;
|
|
161
177
|
automaticBalanceMoveHistoryReason(ctx: Ctx, lane: LaneRecord): AccountHistoryEntry["reason"];
|
|
@@ -217,7 +233,7 @@ export interface Ctx {
|
|
|
217
233
|
}) => Promise<OutpostDispatchResult>;
|
|
218
234
|
backfillOutpostIdentity: (expectedId: string) => "created" | "unchanged";
|
|
219
235
|
fetchText: (url: string) => Promise<string>;
|
|
220
|
-
lanePlacementEnabled: () => boolean
|
|
236
|
+
lanePlacementEnabled: (company: LaneCompanyRef | undefined) => Promise<boolean>;
|
|
221
237
|
storyLedgerEnabled: () => boolean;
|
|
222
238
|
isSsmOwnerDispatch: () => boolean;
|
|
223
239
|
withGrantHireLocks: typeof withLaneHireLocks;
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -83,7 +83,7 @@ export interface LanesCommandDeps {
|
|
|
83
83
|
locateTranscript?: (home: string, sessionId: string) => string | null;
|
|
84
84
|
monitorEvidence?: MonitorEvidenceReader;
|
|
85
85
|
/** Test seam for the default-off outpost placement rollout flag. */
|
|
86
|
-
lanePlacementEnabled?: () => boolean
|
|
86
|
+
lanePlacementEnabled?: (company: LaneCompanyRef | undefined) => Promise<boolean>;
|
|
87
87
|
/** Test seam for the default-off story ledger flag. */
|
|
88
88
|
storyLedgerEnabled?: () => boolean;
|
|
89
89
|
/** Test seam for the SSM-only Outpost owner grant path. */
|
package/dist/commands/lanes.js
CHANGED
|
@@ -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 {
|
|
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
|
|
80
|
-
|
|
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 (
|
|
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
|
-
|
|
2324
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
@@ -7022,6 +7200,7 @@ 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
|
}));
|