@indigoai-us/hq-cli 5.273.0-alpha.0 → 5.274.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 (91) hide show
  1. package/CHANGELOG.md +65 -2
  2. package/README.md +32 -1
  3. package/dist/command-catalog.generated.d.ts +83 -536
  4. package/dist/command-catalog.generated.js +129 -714
  5. package/dist/command-registration-plan.d.ts +12 -0
  6. package/dist/command-registration-plan.js +2 -0
  7. package/dist/commands/cloud.js +40 -0
  8. package/dist/commands/core-checkpoint.js +3 -1
  9. package/dist/commands/core-command-catalog.d.ts +32 -0
  10. package/dist/commands/core-command-catalog.js +58 -0
  11. package/dist/commands/core-ontology.js +9 -4
  12. package/dist/commands/core.d.ts +4 -0
  13. package/dist/commands/core.js +79 -75
  14. package/dist/commands/daemon.d.ts +0 -3
  15. package/dist/commands/daemon.js +1 -173
  16. package/dist/commands/deploy-comments.d.ts +3 -0
  17. package/dist/commands/deploy-comments.js +159 -0
  18. package/dist/commands/lanes-checks.d.ts +0 -1
  19. package/dist/commands/lanes-checks.js +217 -54
  20. package/dist/commands/lanes-hold.js +40 -2
  21. package/dist/commands/lanes-message.js +98 -14
  22. package/dist/commands/lanes-move.d.ts +12 -0
  23. package/dist/commands/lanes-move.js +342 -0
  24. package/dist/commands/lanes-resume.js +3 -1
  25. package/dist/commands/lanes-shared.d.ts +39 -3
  26. package/dist/commands/lanes-shared.js +59 -0
  27. package/dist/commands/lanes.d.ts +3 -5
  28. package/dist/commands/lanes.js +206 -478
  29. package/dist/commands/report.js +2 -14
  30. package/dist/commands/sync-mode.d.ts +0 -11
  31. package/dist/commands/sync-mode.js +0 -30
  32. package/dist/fast-core.js +1 -1
  33. package/dist/{commands/scaffold-fast.d.ts → fast-scaffold.d.ts} +1 -1
  34. package/dist/{commands/scaffold-fast.js → fast-scaffold.js} +1 -1
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.js +2 -2
  37. package/dist/lib/checkpoint-ontology/apply.d.ts +8 -0
  38. package/dist/lib/checkpoint-ontology/apply.js +33 -0
  39. package/dist/lib/checkpoint-ontology/plan.d.ts +15 -0
  40. package/dist/lib/checkpoint-ontology/plan.js +15 -0
  41. package/dist/lib/daemon/catalog.d.ts +2 -18
  42. package/dist/lib/daemon/catalog.js +2 -18
  43. package/dist/lib/daemon/config.d.ts +0 -6
  44. package/dist/lib/daemon/config.js +2 -15
  45. package/dist/lib/daemon/requests.d.ts +0 -4
  46. package/dist/lib/daemon/requests.js +0 -2
  47. package/dist/lib/daemon/run.js +19 -78
  48. package/dist/lib/daemon/supervisor.d.ts +0 -8
  49. package/dist/lib/daemon/supervisor.js +1 -101
  50. package/dist/lib/deploy-api.d.ts +6 -0
  51. package/dist/lib/deploy-api.js +21 -0
  52. package/dist/lib/doctor/checks/scaffold-cli-floor.d.ts +7 -0
  53. package/dist/lib/doctor/checks/scaffold-cli-floor.js +73 -0
  54. package/dist/lib/doctor/fix/apply.d.ts +17 -2
  55. package/dist/lib/doctor/fix/apply.js +111 -57
  56. package/dist/lib/doctor/fix/json-output.d.ts +9 -1
  57. package/dist/lib/doctor/fix/json-output.js +11 -0
  58. package/dist/lib/doctor/registry.js +2 -0
  59. package/dist/lib/first-sync-next-step.d.ts +37 -0
  60. package/dist/lib/first-sync-next-step.js +189 -0
  61. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  62. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  63. package/dist/lib/lanes/hire-grant-dispatch.d.ts +6 -1
  64. package/dist/lib/lanes/hire-grant-dispatch.js +78 -3
  65. package/dist/lib/lanes/keep-alive.d.ts +3 -2
  66. package/dist/lib/lanes/keep-alive.js +2 -2
  67. package/dist/lib/lanes/merge-command.d.ts +1 -0
  68. package/dist/lib/lanes/merge-command.js +299 -33
  69. package/dist/lib/lanes/orphans.d.ts +7 -0
  70. package/dist/lib/lanes/orphans.js +39 -4
  71. package/dist/lib/lanes/tracker-store.d.ts +2 -18
  72. package/dist/lib/lanes/tracker-store.js +1 -172
  73. package/dist/lib/scaffold-cli-floor.d.ts +9 -0
  74. package/dist/lib/scaffold-cli-floor.js +50 -0
  75. package/dist/main.js +4 -1
  76. package/dist/utils/cli-telemetry.d.ts +7 -1
  77. package/dist/utils/cli-telemetry.js +84 -8
  78. package/dist/utils/contribution-table.d.ts +1 -1
  79. package/dist/utils/scaffold-fast-import-guard.d.ts +3 -0
  80. package/dist/utils/scaffold-fast-import-guard.js +28 -0
  81. package/dist/utils/scaffold-packlist-guard.d.ts +7 -0
  82. package/dist/utils/scaffold-packlist-guard.js +23 -0
  83. package/dist/utils/scaffold-sibling-references.d.ts +21 -0
  84. package/dist/utils/scaffold-sibling-references.js +156 -0
  85. package/package.json +2 -2
  86. package/dist/commands/lanes-import.d.ts +0 -14
  87. package/dist/commands/lanes-import.js +0 -1210
  88. package/dist/commands/lanes-tracker.d.ts +0 -18
  89. package/dist/commands/lanes-tracker.js +0 -452
  90. package/dist/lib/daemon/sync-runner.d.ts +0 -58
  91. package/dist/lib/daemon/sync-runner.js +0 -277
@@ -0,0 +1,342 @@
1
+ import * as path from "node:path";
2
+ import { chooseHostForLane } from "../lib/lanes/host-capacity.js";
3
+ import { laneRunsHere, loadLanesConfig, loadWorkerProfile, readLane, updateLane, withLaneAdmissionLock, } from "../lib/lanes/index.js";
4
+ import { LanesError, isLanesError } from "../lib/lanes/errors.js";
5
+ import { buildOutpostMoveTargetCommand, dispatchErrorMessage, } from "../lib/lanes/outpost-dispatch.js";
6
+ import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
7
+ import { assertPlacementEnabled, parsePlacementPolicy, readLanePlacementEnabled, } from "./lanes-shared.js";
8
+ export function createLaneMoveHandlers(deps) {
9
+ const { assertKnownOutpost, assertLaneRunsHereForOperatorMutation, dispatchCreatedOutpostLane, loadHostCapacitySnapshots, ownerErrorFromOutput, parseRemoteJson, printJson, readJsonFile, reconcileHostFromCtx, runDir, timeoutPath, } = deps;
10
+ function laneCanMoveBeforeStart(lane) {
11
+ return lane.state === "queued" &&
12
+ lane.pickup_validation_pending !== true &&
13
+ lane.admission_reserved_at === undefined &&
14
+ lane.provider_session_id === undefined &&
15
+ lane.provider_session_id_absent === undefined &&
16
+ lane.provider_home_at_spawn === undefined &&
17
+ lane.last_resume === undefined &&
18
+ lane.last_run_usage === undefined &&
19
+ lane.last_host_report_at === undefined &&
20
+ lane.pending_resume === undefined;
21
+ }
22
+ function readLaneTimeout(hqRoot, laneId) {
23
+ const raw = readJsonFile(timeoutPath(hqRoot, laneId));
24
+ if (raw === null)
25
+ return undefined;
26
+ if (typeof raw !== "object" || Array.isArray(raw) ||
27
+ typeof raw.timeout_secs !== "number" ||
28
+ typeof raw.deadline_at !== "string" ||
29
+ !Number.isSafeInteger(raw.timeout_secs) ||
30
+ raw.timeout_secs <= 0 ||
31
+ !Number.isFinite(Date.parse(raw.deadline_at))) {
32
+ throw new LanesError("lane_timeout_invalid", `Lane ${laneId} has a malformed timeout record`, {
33
+ lane_id: laneId,
34
+ });
35
+ }
36
+ const record = raw;
37
+ return { timeout_secs: record.timeout_secs, deadline_at: record.deadline_at };
38
+ }
39
+ async function runMove(ctx, laneId, opts) {
40
+ const hqRoot = ctx.hqRoot();
41
+ const policy = parsePlacementPolicy(opts.placement ?? "");
42
+ const source = readLane(hqRoot, laneId);
43
+ const placementCompany = resolveLaneCompany(hqRoot, source).company ?? undefined;
44
+ let placementGate;
45
+ const placementEnabled = () => {
46
+ placementGate ??= readLanePlacementEnabled(ctx, placementCompany);
47
+ return placementGate;
48
+ };
49
+ await assertPlacementEnabled(ctx, placementCompany, placementEnabled);
50
+ if (!laneCanMoveBeforeStart(source)) {
51
+ 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 });
52
+ }
53
+ const sourceHost = source.host;
54
+ const config = loadLanesConfig(hqRoot);
55
+ const capacity = await loadHostCapacitySnapshots(ctx, hqRoot, config);
56
+ let targetSnapshots = capacity.snapshots.filter((snapshot) => snapshot.host.kind !== sourceHost.kind || snapshot.host.id !== sourceHost.id);
57
+ if (policy.kind === "outpost") {
58
+ targetSnapshots = targetSnapshots.filter((snapshot) => snapshot.host.kind === "outpost" && snapshot.host.id === policy.id);
59
+ }
60
+ else if (policy.kind === "local") {
61
+ targetSnapshots = targetSnapshots.filter((snapshot) => snapshot.host.kind === "local");
62
+ }
63
+ const profileAccounts = source.worker_profile === undefined
64
+ ? undefined
65
+ : loadWorkerProfile(hqRoot, source.worker_profile).meta.accounts;
66
+ const allowedAccounts = source.admission_accounts ?? profileAccounts;
67
+ const choice = chooseHostForLane(targetSnapshots, {
68
+ provider: source.provider,
69
+ ...(source.account !== undefined ? { account: source.account } : {}),
70
+ ...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
71
+ });
72
+ let moved;
73
+ if (laneRunsHere(source, reconcileHostFromCtx(ctx))) {
74
+ moved = await commitLaneMove(ctx, laneId, choice.host, choice.account, placementEnabled);
75
+ }
76
+ else if (source.host.kind === "outpost") {
77
+ moved = await dispatchRemoteLaneMove(ctx, source, choice.host, choice.account);
78
+ }
79
+ else {
80
+ throw new LanesError("lane_remote", `Cannot move lane ${laneId} from this host; its state is owned by ${source.host.kind}:${source.host.id}`, { lane_id: laneId, host: source.host, action: "move" });
81
+ }
82
+ printJson({
83
+ ok: true,
84
+ action: "move",
85
+ lane_id: laneId,
86
+ state: moved.state,
87
+ host: moved.host,
88
+ account: moved.account ?? null,
89
+ lane: moved,
90
+ });
91
+ }
92
+ async function commitLaneMove(ctx, laneId, target, account, readPlacementEnabled) {
93
+ const hqRoot = ctx.hqRoot();
94
+ const before = readLane(hqRoot, laneId);
95
+ assertLaneRunsHereForOperatorMutation(ctx, before, "move-target");
96
+ await assertPlacementEnabled(ctx, resolveLaneCompany(hqRoot, before).company ?? undefined, readPlacementEnabled);
97
+ if (before.host.kind === target.kind && before.host.id === target.id) {
98
+ throw new LanesError("lane_move_same_host", `Lane ${laneId} is already on ${target.kind}:${target.id}`, {
99
+ lane_id: laneId,
100
+ host: target,
101
+ });
102
+ }
103
+ if (!laneCanMoveBeforeStart(before)) {
104
+ throw new LanesError(before.state === "queued" ? "lane_move_started" : "lane_not_queued", `Lane ${laneId} cannot move after it has started; current state is ${before.state}`, { lane_id: laneId, state: before.state });
105
+ }
106
+ if (target.kind === "outpost") {
107
+ let known;
108
+ try {
109
+ known = await ctx.listOutpostIds();
110
+ }
111
+ catch (err) {
112
+ throw new LanesError("outpost_list_unavailable", `Failed to list Outpost ids: ${dispatchErrorMessage(err)}`, {});
113
+ }
114
+ assertKnownOutpost(target.id, known);
115
+ }
116
+ const accountConfig = loadLanesConfig(hqRoot).providers[before.provider].accounts[account];
117
+ if (accountConfig === undefined || !accountConfig.enabled) {
118
+ throw new LanesError("lane_move_account_invalid", `Account ${account} is unavailable for ${before.provider}`, {
119
+ lane_id: laneId,
120
+ provider: before.provider,
121
+ account,
122
+ });
123
+ }
124
+ const profileAccounts = before.worker_profile === undefined
125
+ ? undefined
126
+ : loadWorkerProfile(hqRoot, before.worker_profile).meta.accounts;
127
+ const allowedAccounts = before.admission_accounts ?? profileAccounts;
128
+ if (allowedAccounts !== undefined &&
129
+ !allowedAccounts.includes(account)) {
130
+ throw new LanesError("lane_move_account_invalid", `Account ${account} is not allowed by lane ${laneId}`, {
131
+ lane_id: laneId,
132
+ account,
133
+ allowed_accounts: allowedAccounts,
134
+ });
135
+ }
136
+ const briefText = target.kind === "outpost"
137
+ ? (() => {
138
+ const briefPath = path.join(runDir(hqRoot, laneId), "brief.md");
139
+ if (!ctx.existsSync(briefPath)) {
140
+ throw new LanesError("lane_move_brief_missing", `Lane ${laneId} has no stored brief to dispatch`, {
141
+ lane_id: laneId,
142
+ });
143
+ }
144
+ return ctx.readFileSync(briefPath, "utf8");
145
+ })()
146
+ : undefined;
147
+ const timeout = target.kind === "outpost" ? readLaneTimeout(hqRoot, laneId) : undefined;
148
+ const moved = await withLaneAdmissionLock(hqRoot, before.provider, async () => {
149
+ const current = readLane(hqRoot, laneId);
150
+ assertLaneRunsHereForOperatorMutation(ctx, current, "move-target");
151
+ if (!laneCanMoveBeforeStart(current)) {
152
+ throw new LanesError(current.state === "queued" ? "lane_move_started" : "lane_not_queued", `Lane ${laneId} started before the move could commit`, { lane_id: laneId, state: current.state });
153
+ }
154
+ return updateLane(hqRoot, laneId, current.version, "command", (live) => ({
155
+ ...live,
156
+ host: target,
157
+ account,
158
+ queued_reason: undefined,
159
+ admission_reserved_at: undefined,
160
+ }));
161
+ }, ctx.sleep);
162
+ if (target.kind !== "outpost")
163
+ return moved;
164
+ try {
165
+ return await dispatchCreatedOutpostLane(ctx, {
166
+ hqRoot,
167
+ lane: moved,
168
+ outpostId: target.id,
169
+ briefText: briefText ?? "",
170
+ timeout,
171
+ preserveQueuedOnFailure: true,
172
+ });
173
+ }
174
+ catch (err) {
175
+ const dispatchDetails = isLanesError(err) ? err.details : {};
176
+ const commandId = typeof dispatchDetails.command_id === "string" &&
177
+ dispatchDetails.command_id.trim() !== ""
178
+ ? dispatchDetails.command_id
179
+ : undefined;
180
+ const definitelyNotSubmitted = commandId === undefined &&
181
+ dispatchDetails.outcome_unknown !== true &&
182
+ dispatchDetails.done !== false;
183
+ const knownNoPersistence = (isLanesError(err) && err.error === "outpost_self_target") ||
184
+ (dispatchDetails.done === true && dispatchDetails.pickup_persisted === false);
185
+ if (!definitelyNotSubmitted && !knownNoPersistence) {
186
+ const checkCommand = `hq lanes show ${laneId}`;
187
+ const message = `Failed to dispatch lane ${laneId} to Outpost ${target.id}; outcome is unknown and the lane remains assigned to Outpost ${target.id}. ` +
188
+ `Check the target copy by running \`${checkCommand}\` on Outpost ${target.id}. ` +
189
+ dispatchErrorMessage(err);
190
+ throw new LanesError("lane_move_dispatch_failed", message, {
191
+ lane_id: laneId,
192
+ message,
193
+ source_host: before.host,
194
+ target_host: target,
195
+ source_restored: false,
196
+ outcome: "unknown",
197
+ ...(commandId !== undefined ? { command_id: commandId } : {}),
198
+ check_command: checkCommand,
199
+ ...(dispatchDetails.owner_error !== undefined
200
+ ? { owner_error: dispatchDetails.owner_error }
201
+ : {}),
202
+ });
203
+ }
204
+ let restored = false;
205
+ let rollbackError;
206
+ try {
207
+ restored = await withLaneAdmissionLock(hqRoot, before.provider, async () => {
208
+ const current = readLane(hqRoot, laneId);
209
+ if (current.version !== moved.version ||
210
+ current.state !== "queued" ||
211
+ current.host.kind !== target.kind ||
212
+ current.host.id !== target.id ||
213
+ current.account !== account) {
214
+ return false;
215
+ }
216
+ updateLane(hqRoot, laneId, current.version, "command", (live) => ({
217
+ ...live,
218
+ host: before.host,
219
+ account: before.account,
220
+ queued_reason: before.queued_reason,
221
+ admission_reserved_at: before.admission_reserved_at,
222
+ }));
223
+ return true;
224
+ }, ctx.sleep);
225
+ }
226
+ catch (restoreErr) {
227
+ rollbackError = restoreErr;
228
+ }
229
+ const dispatchMessage = dispatchErrorMessage(err);
230
+ const outcome = restored
231
+ ? "the lane stayed queued on its source"
232
+ : rollbackError === undefined
233
+ ? "the lane changed after the move and was not overwritten"
234
+ : `source restoration failed: ${dispatchErrorMessage(rollbackError)}`;
235
+ const message = `Failed to dispatch lane ${laneId} to Outpost ${target.id}; ${outcome}: ${dispatchMessage}`;
236
+ throw new LanesError("lane_move_dispatch_failed", message, {
237
+ lane_id: laneId,
238
+ message,
239
+ source_host: before.host,
240
+ target_host: target,
241
+ source_restored: restored,
242
+ ...(dispatchDetails.owner_error !== undefined
243
+ ? { owner_error: dispatchDetails.owner_error }
244
+ : {}),
245
+ ...(rollbackError !== undefined
246
+ ? { rollback_error: dispatchErrorMessage(rollbackError) }
247
+ : {}),
248
+ });
249
+ }
250
+ }
251
+ async function dispatchRemoteLaneMove(ctx, source, target, account) {
252
+ if (source.host.kind !== "outpost") {
253
+ throw new LanesError("lane_move_wrong_host", `Lane ${source.lane_id} is not Outpost-hosted`, {
254
+ lane_id: source.lane_id,
255
+ host: source.host,
256
+ });
257
+ }
258
+ let dispatched;
259
+ try {
260
+ dispatched = await ctx.dispatchOutpostExec({
261
+ outpostId: source.host.id,
262
+ command: buildOutpostMoveTargetCommand({
263
+ laneId: source.lane_id,
264
+ host: target,
265
+ account,
266
+ }),
267
+ timeoutSeconds: 120,
268
+ waitMs: 120_000,
269
+ });
270
+ }
271
+ catch (err) {
272
+ throw new LanesError("lane_move_dispatch_failed", `Could not ask Outpost ${source.host.id} to move lane ${source.lane_id}: ${dispatchErrorMessage(err)}`, { lane_id: source.lane_id, source_outpost: source.host.id });
273
+ }
274
+ const ownerError = ownerErrorFromOutput(dispatched.stdout);
275
+ if (ownerError) {
276
+ throw new LanesError("outpost_dispatch_failed", `Outpost ${source.host.id} rejected the lane move: ${ownerError.message}`, {
277
+ lane_id: source.lane_id,
278
+ source_outpost: source.host.id,
279
+ command_id: dispatched.commandId,
280
+ status: dispatched.status,
281
+ done: dispatched.done ?? false,
282
+ exit_code: dispatched.exitCode,
283
+ owner_error: ownerError,
284
+ });
285
+ }
286
+ if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
287
+ throw new LanesError("lane_move_dispatch_failed", `Outpost ${source.host.id} did not confirm the lane move`, {
288
+ lane_id: source.lane_id,
289
+ source_outpost: source.host.id,
290
+ command_id: dispatched.commandId,
291
+ status: dispatched.status,
292
+ done: dispatched.done ?? false,
293
+ exit_code: dispatched.exitCode,
294
+ });
295
+ }
296
+ const remote = parseRemoteJson(dispatched.stdout, "Outpost lane move");
297
+ const remoteHost = remote?.host;
298
+ if (remote?.ok !== true ||
299
+ remote.action !== "move-target" ||
300
+ remote.lane_id !== source.lane_id ||
301
+ remote.state !== "queued" ||
302
+ remote.account !== account ||
303
+ remoteHost === null || typeof remoteHost !== "object" || Array.isArray(remoteHost) ||
304
+ remoteHost.kind !== target.kind ||
305
+ remoteHost.id !== target.id) {
306
+ throw new LanesError("lane_move_unconfirmed", `Outpost ${source.host.id} returned an invalid lane move result`, {
307
+ lane_id: source.lane_id,
308
+ source_outpost: source.host.id,
309
+ target,
310
+ });
311
+ }
312
+ return {
313
+ ...source,
314
+ state: "queued",
315
+ host: target,
316
+ account,
317
+ };
318
+ }
319
+ async function runMoveTarget(ctx, laneId, opts) {
320
+ const kind = opts.hostKind?.trim();
321
+ const id = opts.hostId?.trim();
322
+ const account = opts.account?.trim();
323
+ if ((kind !== "local" && kind !== "outpost") || !id || !account) {
324
+ throw new LanesError("lane_move_target_invalid", "Move target requires a valid host kind, host id, and account", {
325
+ host_kind: kind ?? null,
326
+ host_id: id ?? null,
327
+ account: account ?? null,
328
+ });
329
+ }
330
+ const moved = await commitLaneMove(ctx, laneId, { kind, id }, account);
331
+ printJson({
332
+ ok: true,
333
+ action: "move-target",
334
+ lane_id: moved.lane_id,
335
+ state: moved.state,
336
+ host: moved.host,
337
+ account: moved.account ?? null,
338
+ });
339
+ }
340
+ return { runMove, runMoveTarget };
341
+ }
342
+ //# sourceMappingURL=lanes-move.js.map
@@ -124,7 +124,9 @@ export function createLaneResumeHandlers(deps) {
124
124
  throw new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is only valid on an Outpost owner dispatch", { lane_id: laneId });
125
125
  }
126
126
  if (resolution.remoteOnly || (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane))) {
127
- await withResumeHireAdmission(ctx, lane, () => undefined);
127
+ if (!resolution.remoteOnly) {
128
+ await withResumeHireAdmission(ctx, lane, () => undefined);
129
+ }
128
130
  await dispatchOutpostLaneCommand(ctx, lane, "resume", opts, resolution.remoteOnly
129
131
  ? { syncPrimary: false, expectedOwnerHost: resolution.ownerHost }
130
132
  : undefined);
@@ -1,4 +1,4 @@
1
- import type { LaneCompanyRef, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LaneAccountSelection, PendingResume, QueuedReason, ResolvedLaneWorker, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneHireLocks } from "../lib/lanes/index.js";
1
+ import type { LaneCompanyRef, LanePlacementPolicy, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LaneAccountSelection, PendingResume, QueuedReason, ResolvedLaneWorker, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneHireLocks } from "../lib/lanes/index.js";
2
2
  import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
3
3
  import type { OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
4
4
  import type { ProbeCommand } from "../lib/lanes/models.js";
@@ -41,6 +41,8 @@ export interface LaneMessageOptions {
41
41
  effort?: string;
42
42
  worker?: string;
43
43
  grantContext?: string;
44
+ /** Primary-verified caller identity for a superseded Outpost message. */
45
+ supersededMessageCallerId?: string;
44
46
  monitorEvent?: {
45
47
  monitor_id: string;
46
48
  description: string;
@@ -237,10 +239,15 @@ export interface Ctx {
237
239
  storyLedgerEnabled: () => boolean;
238
240
  isSsmOwnerDispatch: () => boolean;
239
241
  withGrantHireLocks: typeof withLaneHireLocks;
240
- laneTrackerEnabled: (company: LaneCompanyRef) => boolean | Promise<boolean>;
242
+ mergeHoldEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
241
243
  mergeReviewEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
242
- trackerActor: () => string | null;
243
244
  }
245
+ export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
246
+ export declare function defaultLanePlacementEnabled(company: LaneCompanyRef | undefined): Promise<boolean>;
247
+ export declare function readLanePlacementEnabled(ctx: Ctx, company: LaneCompanyRef | undefined): Promise<boolean>;
248
+ export declare function processLanePlacementCompany(ctx: Ctx): LaneCompanyRef | undefined;
249
+ export declare function parsePlacementPolicy(raw: string): LanePlacementPolicy;
250
+ export declare function assertPlacementEnabled(ctx: Ctx, company: LaneCompanyRef | undefined, readPlacementEnabled?: () => Promise<boolean>): Promise<void>;
244
251
  export interface LaneWaitCommandOptions {
245
252
  any?: string[];
246
253
  for?: string[];
@@ -267,6 +274,35 @@ export interface LaneWatchHandlerDeps {
267
274
  printJson(value: unknown): void;
268
275
  readOutpostCapacity(ctx: Ctx, outpostId: string): Promise<HostCapacitySnapshot>;
269
276
  }
277
+ export interface LaneMoveHandlerDeps {
278
+ assertKnownOutpost(requested: string, known: string[]): void;
279
+ assertLaneRunsHereForOperatorMutation(ctx: Ctx, lane: LaneRecord, action: string): void;
280
+ dispatchCreatedOutpostLane(ctx: Ctx, input: {
281
+ hqRoot: string;
282
+ lane: LaneRecord;
283
+ outpostId: string;
284
+ briefText: string;
285
+ timeout?: {
286
+ timeout_secs: number;
287
+ deadline_at: string;
288
+ };
289
+ preserveQueuedOnFailure?: boolean;
290
+ }): Promise<LaneRecord>;
291
+ loadHostCapacitySnapshots(ctx: Ctx, hqRoot: string, config: LanesConfig): Promise<{
292
+ snapshots: HostCapacitySnapshot[];
293
+ }>;
294
+ ownerErrorFromOutput(stdout: string): {
295
+ error: string;
296
+ message: string;
297
+ details: Record<string, unknown>;
298
+ } | undefined;
299
+ parseRemoteJson(stdout: string, surface: string): Record<string, unknown> | null;
300
+ printJson(value: unknown): void;
301
+ readJsonFile(filePath: string): unknown | null;
302
+ reconcileHostFromCtx(ctx: Ctx): ReconcileHost;
303
+ runDir(hqRoot: string, laneId: string): string;
304
+ timeoutPath(hqRoot: string, laneId: string): string;
305
+ }
270
306
  export interface PrimaryOwnedOutpostQuestion {
271
307
  question: QuestionRecord;
272
308
  lane: LaneRecord;
@@ -1,3 +1,5 @@
1
+ import { resolveProcessFlagCompanyContext } from "../lib/flag-registry.js";
2
+ import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
1
3
  import { resolveLaneEffort } from "../lib/lanes/effort.js";
2
4
  import { LanesError } from "../lib/lanes/errors.js";
3
5
  import { AccountResolutionError, accountStatus, loadLanesConfig } from "../lib/lanes/index.js";
@@ -92,4 +94,61 @@ export function recordedResumeEffort(lane) {
92
94
  source: lane.effort_source ?? "none",
93
95
  };
94
96
  }
97
+ export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
98
+ function lanePlacementFlagErrorCode(error) {
99
+ if (error && typeof error === "object") {
100
+ for (const key of ["code", "error", "name"]) {
101
+ const value = error[key];
102
+ if (typeof value === "string" && value.trim())
103
+ return value;
104
+ }
105
+ }
106
+ return "UnknownError";
107
+ }
108
+ function reportLanePlacementFlagReadFailure(error) {
109
+ process.stderr.write(`${JSON.stringify({
110
+ event: "lanes.outpost_load_balancing.flag_read_failed",
111
+ flag: LANE_OUTPOST_LOAD_BALANCING_FLAG,
112
+ error: lanePlacementFlagErrorCode(error),
113
+ fallback: false,
114
+ })}\n`);
115
+ }
116
+ export function defaultLanePlacementEnabled(company) {
117
+ return readLanesRegistryFlag(LANE_OUTPOST_LOAD_BALANCING_FLAG, {
118
+ company,
119
+ reportFailure: reportLanePlacementFlagReadFailure,
120
+ });
121
+ }
122
+ export async function readLanePlacementEnabled(ctx, company) {
123
+ try {
124
+ return await ctx.lanePlacementEnabled(company);
125
+ }
126
+ catch (error) {
127
+ reportLanePlacementFlagReadFailure(error);
128
+ return false;
129
+ }
130
+ }
131
+ export function processLanePlacementCompany(ctx) {
132
+ const context = resolveProcessFlagCompanyContext(ctx.env);
133
+ if (!context.companySlug)
134
+ return undefined;
135
+ return { slug: context.companySlug, uid: context.companyUid ?? "" };
136
+ }
137
+ export function parsePlacementPolicy(raw) {
138
+ const value = raw.trim();
139
+ if (value === "local")
140
+ return { kind: "local" };
141
+ if (value === "auto")
142
+ return { kind: "auto" };
143
+ const match = /^outpost:(.+)$/.exec(value);
144
+ const id = match?.[1]?.trim();
145
+ if (id)
146
+ return { kind: "outpost", id };
147
+ throw new LanesError("lane_placement_invalid", `Invalid placement ${JSON.stringify(raw)}. Accepted: local, auto, outpost:<id>`, { value: raw, accepted: ["local", "auto", "outpost:<id>"] });
148
+ }
149
+ export async function assertPlacementEnabled(ctx, company, readPlacementEnabled = () => readLanePlacementEnabled(ctx, company)) {
150
+ if (await readPlacementEnabled())
151
+ return;
152
+ throw new LanesError("lane_placement_disabled", `Lane placement options require hq-flags gate ${LANE_OUTPOST_LOAD_BALANCING_FLAG}`, { flag: LANE_OUTPOST_LOAD_BALANCING_FLAG });
153
+ }
95
154
  //# sourceMappingURL=lanes-shared.js.map
@@ -8,6 +8,7 @@
8
8
  import { Command } from "commander";
9
9
  import { type LanesMesh, type SpawnLoginResult } from "./lanes-shared.js";
10
10
  export type { LanesMesh, SpawnLoginResult } from "./lanes-shared.js";
11
+ export { LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lanes-shared.js";
11
12
  export { resolveAnswerChoice } from "./lanes-questions.js";
12
13
  import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
13
14
  export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
@@ -90,14 +91,11 @@ export interface LanesCommandDeps {
90
91
  isSsmOwnerDispatch?: () => boolean;
91
92
  /** Test seam for changing a lane between pre-lock and locked grant reads. */
92
93
  withGrantHireLocks?: typeof withLaneHireLocks;
93
- /** Test seam for the hq-flags gated tracker commands. */
94
- laneTrackerEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
95
94
  /** Test seam for the review-before-merge registry flag. */
96
95
  mergeReviewEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
97
- /** Test seam for recording approvals against the active identity. */
98
- trackerActor?: () => string | null;
96
+ /** Test seam for the repository-merge-hold registry flag. */
97
+ mergeHoldEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
99
98
  }
100
- export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
101
99
  export declare function parseSeniorRef(raw: string): SeniorRef;
102
100
  export declare function parseQuestionBody(raw: unknown): QuestionBody | null;
103
101
  /**