@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
@@ -13,29 +13,29 @@ import * as path from "node:path";
13
13
  import { Option } from "commander";
14
14
  import { createLaneAccountsHandlers } from "./lanes-accounts.js";
15
15
  import { createLaneMessageHandlers } from "./lanes-message.js";
16
+ import { createLaneMoveHandlers } from "./lanes-move.js";
16
17
  import { createLaneQuestionsHandlers } from "./lanes-questions.js";
17
18
  import { createLaneResumeHandlers } from "./lanes-resume.js";
18
19
  import { createLaneWatchHandlers } from "./lanes-watch.js";
20
+ import { assertPlacementEnabled, defaultLanePlacementEnabled, parsePlacementPolicy, processLanePlacementCompany, readLanePlacementEnabled, } from "./lanes-shared.js";
21
+ export { LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lanes-shared.js";
19
22
  export { resolveAnswerChoice } from "./lanes-questions.js";
20
23
  import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
21
24
  import { registerLaneKpisCommand } from "./lanes-kpis.js";
22
25
  import { registerLaneChecksCommand } from "./lanes-checks.js";
23
26
  import { registerLaneReportCommands } from "./lanes-report.js";
24
- import { registerLaneTrackerImportCommand } from "./lanes-import.js";
25
27
  import { registerLaneRepoMergeHoldCommand } from "./lanes-hold.js";
26
28
  import { registerLaneReviewCommand } from "./lanes-review.js";
27
29
  import { ensureCognitoToken } from "../utils/cognito-session.js";
28
30
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
29
31
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
30
32
  import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
31
- import { resolveProcessFlagCompanyContext } from "../lib/flag-registry.js";
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";
35
35
  import { createOutpostRegistryWorkerApprovalContext, parseOutpostRegistryWorkerApprovalContext, } from "../lib/lanes/outpost-registry-worker-approval.js";
36
36
  import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
37
37
  export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
38
- import { buildOutpostCapacityCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostQuestionListCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
38
+ import { buildOutpostCapacityCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostQuestionListCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
39
39
  import { outpostHireSummaryCacheKey, readOutpostHireSummaryCache, writeOutpostHireSummaryCache, } from "../lib/lanes/outpost-hire-cache.js";
40
40
  import { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "../lib/lanes/host-capacity.js";
41
41
  import { BALANCE_MOVABLE_STATES, selectClaudeAccount, } from "../lib/lanes/claude-balance.js";
@@ -75,49 +75,6 @@ export { MAX_LANE_DEPTH };
75
75
  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
- export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
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
- }
111
- }
112
- function defaultLaneTrackerEnabled(company) {
113
- return readLanesRegistryFlag(LANES_TRACKER_FLAG, { company });
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
- }
121
78
  function withoutProviderHome(value) {
122
79
  if (Array.isArray(value))
123
80
  return value.map(withoutProviderHome);
@@ -328,9 +285,8 @@ function resolveDeps(deps = {}) {
328
285
  storyLedgerEnabled: deps.storyLedgerEnabled ?? laneStoryLedgerEnabled,
329
286
  isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
330
287
  withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
331
- laneTrackerEnabled: deps.laneTrackerEnabled ?? defaultLaneTrackerEnabled,
332
288
  mergeReviewEnabled: deps.mergeReviewEnabled,
333
- trackerActor: deps.trackerActor ?? defaultTrackerActor,
289
+ mergeHoldEnabled: deps.mergeHoldEnabled,
334
290
  };
335
291
  }
336
292
  function livenessDepsFromCtx(ctx) {
@@ -513,6 +469,12 @@ function mergeOwnerLaneHierarchy(supplied, ownerLanes) {
513
469
  byId.set(lane.lane_id, lane);
514
470
  return [...byId.values()];
515
471
  }
472
+ function mergeOwnerLaneRecords(supplied, ownerLanes) {
473
+ const byId = new Map(supplied.map((lane) => [lane.lane_id, lane]));
474
+ for (const lane of ownerLanes)
475
+ byId.set(lane.lane_id, lane);
476
+ return [...byId.values()];
477
+ }
516
478
  function outpostCliOverride(ctx, outpostId) {
517
479
  const envKey = `HQ_LANES_OUTPOST_CLI_${outpostId.toUpperCase().replace(/[^A-Z0-9_]/g, "_")}`;
518
480
  const configured = ctx.env[envKey];
@@ -863,8 +825,7 @@ async function mapWithConcurrency(values, concurrency, map) {
863
825
  async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
864
826
  const hqRoot = ctx.hqRoot();
865
827
  if (grantContext !== undefined && (!ctx.isSsmOwnerDispatch() ||
866
- lane.host.kind !== "outpost" ||
867
- !laneIsHostedHere(ctx, lane) ||
828
+ !laneGrantTargetHostedHere(ctx, lane) ||
868
829
  lane.senior.kind !== "lane" ||
869
830
  !sameHireGrantSenior(lane.senior, grantContext.expected_senior))) {
870
831
  throw new LanesError("hire_grant_senior_changed", `Lane ${lane.lane_id}'s senior changed after the primary authorized its resume`, { lane_id: lane.lane_id, expected_senior: grantContext.expected_senior, current_senior: lane.senior });
@@ -1124,9 +1085,27 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1124
1085
  new Set(outpostIds).size !== outpostIds.length) {
1125
1086
  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
1087
  }
1088
+ const requestedOutpostId = options.outpostId?.trim();
1089
+ if (options.outpostId !== undefined && !requestedOutpostId) {
1090
+ throw new LanesError("outpost_id_invalid", "--outpost must be a registered Outpost id", { lane_id: laneId });
1091
+ }
1092
+ if (requestedOutpostId !== undefined && !outpostIds.includes(requestedOutpostId)) {
1093
+ throw new LanesError("outpost_not_registered", `Outpost ${requestedOutpostId} is not registered`, { lane_id: laneId, outpost_id: requestedOutpostId });
1094
+ }
1127
1095
  const matches = [];
1128
- for (const outpostId of outpostIds) {
1129
- const ownerLanes = await readOutpostLaneRecords(ctx, outpostId);
1096
+ const failedOutpostIds = [];
1097
+ const lookupOutpostIds = requestedOutpostId === undefined ? outpostIds : [requestedOutpostId];
1098
+ for (const outpostId of lookupOutpostIds) {
1099
+ let ownerLanes;
1100
+ try {
1101
+ ownerLanes = await readOutpostLaneRecords(ctx, outpostId);
1102
+ }
1103
+ catch (err) {
1104
+ if (requestedOutpostId !== undefined)
1105
+ throw err;
1106
+ failedOutpostIds.push(outpostId);
1107
+ continue;
1108
+ }
1130
1109
  for (const lane of ownerLanes) {
1131
1110
  if (lane.lane_id === laneId &&
1132
1111
  lane.host.kind === "local" &&
@@ -1136,7 +1115,7 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1136
1115
  }
1137
1116
  }
1138
1117
  if (matches.length === 0) {
1139
- throw commandLaneNotFound(laneId, options.outpostHint === true);
1118
+ throw commandLaneNotFound(laneId, options.outpostHint === true, failedOutpostIds.length > 0 ? { failed_outpost_ids: failedOutpostIds } : undefined);
1140
1119
  }
1141
1120
  if (matches.length !== 1) {
1142
1121
  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) });
@@ -1149,11 +1128,11 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1149
1128
  ownerHost: match.lane.host,
1150
1129
  };
1151
1130
  }
1152
- function commandLaneNotFound(laneId, outpostHint) {
1131
+ function commandLaneNotFound(laneId, outpostHint, details = {}) {
1153
1132
  const hint = outpostHint
1154
1133
  ? "; pass --outpost <id> to confirm a registered Outpost"
1155
1134
  : "";
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 });
1135
+ 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, ...details });
1157
1136
  }
1158
1137
  async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
1159
1138
  const localHost = laneHostFromCtx(ctx);
@@ -1280,29 +1259,18 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
1280
1259
  }
1281
1260
  return { snapshots, unavailableHosts };
1282
1261
  }
1283
- function parsePlacementPolicy(raw) {
1284
- const value = raw.trim();
1285
- if (value === "local")
1286
- return { kind: "local" };
1287
- if (value === "auto")
1288
- return { kind: "auto" };
1289
- const match = /^outpost:(.+)$/.exec(value);
1290
- const id = match?.[1]?.trim();
1291
- if (id)
1292
- return { kind: "outpost", id };
1293
- throw new LanesError("lane_placement_invalid", `Invalid placement ${JSON.stringify(raw)}. Accepted: local, auto, outpost:<id>`, { value: raw, accepted: ["local", "auto", "outpost:<id>"] });
1294
- }
1295
- async function assertPlacementEnabled(ctx, company, readPlacementEnabled = () => readLanePlacementEnabled(ctx, company)) {
1296
- if (await readPlacementEnabled())
1297
- return;
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 });
1299
- }
1300
1262
  function loadAllQuestions(hqRoot) {
1301
1263
  return listQuestionIds(hqRoot).map((id) => readQuestion(hqRoot, id));
1302
1264
  }
1303
1265
  function sessionMetaPath(hqRoot, sessionId) {
1304
1266
  return path.join(hqRoot, "workspace", "sessions", sessionId, "meta.yaml");
1305
1267
  }
1268
+ function isSessionMetaDocument(value) {
1269
+ if (value === null || typeof value !== "object" || Array.isArray(value))
1270
+ return false;
1271
+ const prototype = Object.getPrototypeOf(value);
1272
+ return prototype === Object.prototype || prototype === null;
1273
+ }
1306
1274
  function loadSessionMeta(hqRoot, sessionId, ctx) {
1307
1275
  const filePath = sessionMetaPath(hqRoot, sessionId);
1308
1276
  if (!ctx.existsSync(filePath))
@@ -1323,7 +1291,7 @@ function loadSessionMeta(hqRoot, sessionId, ctx) {
1323
1291
  process.stderr.write(`hq lanes: failed to parse session meta ${filePath}: ${err instanceof Error ? err.message : String(err)}\n`);
1324
1292
  return { status: "malformed" };
1325
1293
  }
1326
- if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
1294
+ if (!isSessionMetaDocument(doc)) {
1327
1295
  return { status: "malformed" };
1328
1296
  }
1329
1297
  return { status: "ok", doc: doc };
@@ -1334,11 +1302,21 @@ function readSessionMeta(hqRoot, sessionId, ctx) {
1334
1302
  return null;
1335
1303
  return loaded.doc;
1336
1304
  }
1305
+ /**
1306
+ * Apply the documented session senior default only when the key was never
1307
+ * written. `core/scripts/hq-session.sh` section 3.1 defaults new sessions to
1308
+ * `user` and says authority walkers apply that default at the walk site.
1309
+ */
1310
+ function sessionSeniorValue(meta) {
1311
+ return Object.prototype.hasOwnProperty.call(meta, "senior")
1312
+ ? meta.senior
1313
+ : "user";
1314
+ }
1337
1315
  function seniorOfSession(hqRoot, sessionId, ctx) {
1338
1316
  const meta = readSessionMeta(hqRoot, sessionId, ctx);
1339
1317
  if (!meta)
1340
1318
  return { kind: "user" };
1341
- const senior = meta.senior;
1319
+ const senior = sessionSeniorValue(meta);
1342
1320
  if (typeof senior === "string" && senior.trim()) {
1343
1321
  return parseSeniorRef(senior);
1344
1322
  }
@@ -2325,13 +2303,16 @@ async function runCreate(ctx, opts) {
2325
2303
  const company = companyFlag
2326
2304
  ? await ctx.resolveCompany(companyFlag)
2327
2305
  : null;
2306
+ const callerAuthorization = requestedHireGrant !== undefined || ctx.env.HQ_LANE_ID?.trim()
2307
+ ? callerSeniorAuthorization(ctx, senior)
2308
+ : { authorized: true };
2328
2309
  if (requestedHireGrant !== undefined &&
2329
- !callerMatchesSenior(ctx, senior)) {
2330
- throw new LanesError("hire_grant_unauthorized", `Only senior ${seniorKey(senior)} may set a lane's hire grant`, { senior, hire_grant: requestedHireGrant });
2310
+ !callerAuthorization.authorized) {
2311
+ throw new LanesError("hire_grant_unauthorized", appendCallerSeniorReason(`Only senior ${seniorKey(senior)} may set a lane's hire grant`, callerAuthorization), { senior, hire_grant: requestedHireGrant, reason: callerAuthorization.reason });
2331
2312
  }
2332
2313
  if (ctx.env.HQ_LANE_ID?.trim() &&
2333
- !callerMatchesSenior(ctx, senior)) {
2334
- throw new LanesError("hire_grant_unauthorized", `Only senior lane ${senior.id} may create a direct child lane`, { senior_lane_id: senior.id });
2314
+ !callerAuthorization.authorized) {
2315
+ throw new LanesError("hire_grant_unauthorized", appendCallerSeniorReason(`Only senior lane ${senior.id} may create a direct child lane`, callerAuthorization), { senior_lane_id: senior.id, reason: callerAuthorization.reason });
2335
2316
  }
2336
2317
  const hireGrant = requestedHireGrant ?? 0;
2337
2318
  const laneDepth = derivedLaneDepth(hqRoot, senior, ctx);
@@ -3355,7 +3336,7 @@ function authorizeFailedResume(ctx, lane, encodedContext) {
3355
3336
  throw new LanesError("failed_resume_authorization_invalid", "Failed-resume authorization context is only valid on an Outpost owner dispatch", { lane_id: lane.lane_id });
3356
3337
  }
3357
3338
  const context = parseOutpostFailedResumeContext(encodedContext, lane.lane_id);
3358
- if (lane.host.kind !== "outpost" ||
3339
+ if (!laneGrantTargetHostedHere(ctx, lane) ||
3359
3340
  !sameHireGrantSenior(lane.senior, context.expectedSenior)) {
3360
3341
  throw new LanesError("failed_resume_senior_changed", `Lane ${lane.lane_id}'s senior changed after its failed-resume request was authorized`, {
3361
3342
  lane_id: lane.lane_id,
@@ -3365,18 +3346,25 @@ function authorizeFailedResume(ctx, lane, encodedContext) {
3365
3346
  }
3366
3347
  return context.actor;
3367
3348
  }
3368
- if (!callerMatchesSenior(ctx, lane.senior)) {
3369
- throw new LanesError("failed_resume_unauthorized", "Only the lane's senior may resume a failed lane with --from-failed", { lane_id: lane.lane_id });
3349
+ const callerAuthorization = callerSeniorAuthorization(ctx, lane.senior);
3350
+ if (!callerAuthorization.authorized) {
3351
+ throw new LanesError("failed_resume_unauthorized", appendCallerSeniorReason("Only the lane's senior may resume a failed lane with --from-failed", callerAuthorization), { lane_id: lane.lane_id, reason: callerAuthorization.reason });
3370
3352
  }
3371
3353
  return resumeProfileRepinActor(ctx);
3372
3354
  }
3373
- function workerProfileHashMismatch(lane, profile) {
3374
- return new LanesError("worker_profile_hash_mismatch", `Worker profile ${JSON.stringify(profile.id)} on Outpost ${lane.host.id} does not match the hash recorded by the lane`, {
3355
+ function workerProfileHashMismatch(lane, profile, callerAuthorization) {
3356
+ const message = `Worker profile ${JSON.stringify(profile.id)} on Outpost ${lane.host.id} does not match the hash recorded by the lane`;
3357
+ return new LanesError("worker_profile_hash_mismatch", callerAuthorization && !callerAuthorization.authorized
3358
+ ? appendCallerSeniorReason(message, callerAuthorization)
3359
+ : message, {
3375
3360
  lane_id: lane.lane_id,
3376
3361
  worker: profile.id,
3377
3362
  outpost: lane.host.kind === "outpost" ? lane.host.id : null,
3378
3363
  expected_hash: lane.worker_profile_hash ?? null,
3379
3364
  actual_hash: profile.profile.contentHash,
3365
+ ...(callerAuthorization && !callerAuthorization.authorized
3366
+ ? { reason: callerAuthorization.reason }
3367
+ : {}),
3380
3368
  });
3381
3369
  }
3382
3370
  function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
@@ -3396,7 +3384,7 @@ function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
3396
3384
  ? new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is invalid", { lane_id: lane.lane_id })
3397
3385
  : workerProfileHashMismatch(lane, profile);
3398
3386
  }
3399
- if (lane.host.kind !== "outpost" ||
3387
+ if (!laneGrantTargetHostedHere(ctx, lane) ||
3400
3388
  !sameHireGrantSenior(lane.senior, context.expected_senior)) {
3401
3389
  throw profile === undefined
3402
3390
  ? new LanesError("worker_profile_hash_mismatch", "Lane senior changed after profile re-pin authorization", { lane_id: lane.lane_id })
@@ -3404,7 +3392,7 @@ function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
3404
3392
  }
3405
3393
  }
3406
3394
  if (profile === undefined ||
3407
- lane.host.kind !== "outpost" ||
3395
+ (lane.host.kind !== "outpost" && !(ssmOwnerDispatch && laneGrantTargetHostedHere(ctx, lane))) ||
3408
3396
  lane.worker_profile_hash === profile.profile.contentHash) {
3409
3397
  return undefined;
3410
3398
  }
@@ -3419,8 +3407,9 @@ function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
3419
3407
  newHash: profile.profile.contentHash,
3420
3408
  };
3421
3409
  }
3422
- if (context !== undefined || !callerMatchesSenior(ctx, lane.senior)) {
3423
- throw workerProfileHashMismatch(lane, profile);
3410
+ const callerAuthorization = callerSeniorAuthorization(ctx, lane.senior);
3411
+ if (context !== undefined || !callerAuthorization.authorized) {
3412
+ throw workerProfileHashMismatch(lane, profile, context === undefined ? callerAuthorization : undefined);
3424
3413
  }
3425
3414
  return {
3426
3415
  kind: "senior",
@@ -3431,18 +3420,26 @@ function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
3431
3420
  };
3432
3421
  }
3433
3422
  function assertResumeProfileRepinStillAuthorized(ctx, current, authorization) {
3434
- const authorized = current.host.kind === "outpost" &&
3435
- sameHireGrantSenior(current.senior, authorization.expectedSenior) &&
3423
+ const callerAuthorization = authorization.kind === "senior"
3424
+ ? callerSeniorAuthorization(ctx, current.senior)
3425
+ : undefined;
3426
+ const authorized = sameHireGrantSenior(current.senior, authorization.expectedSenior) &&
3436
3427
  (authorization.kind === "ssm-owner-dispatch"
3437
- ? ctx.isSsmOwnerDispatch()
3438
- : callerMatchesSenior(ctx, current.senior));
3428
+ ? ctx.isSsmOwnerDispatch() && laneGrantTargetHostedHere(ctx, current)
3429
+ : current.host.kind === "outpost" && callerAuthorization?.authorized === true);
3439
3430
  if (!authorized) {
3440
- throw new LanesError("worker_profile_hash_mismatch", `Lane ${current.lane_id}'s senior changed before its Outpost profile could be re-pinned`, {
3431
+ const message = `Lane ${current.lane_id}'s senior changed before its Outpost profile could be re-pinned`;
3432
+ throw new LanesError("worker_profile_hash_mismatch", callerAuthorization && !callerAuthorization.authorized
3433
+ ? appendCallerSeniorReason(message, callerAuthorization)
3434
+ : message, {
3441
3435
  lane_id: current.lane_id,
3442
3436
  expected_senior: authorization.expectedSenior,
3443
3437
  current_senior: current.senior,
3444
3438
  expected_hash: current.worker_profile_hash ?? null,
3445
3439
  actual_hash: authorization.newHash,
3440
+ ...(callerAuthorization && !callerAuthorization.authorized
3441
+ ? { reason: callerAuthorization.reason }
3442
+ : {}),
3446
3443
  });
3447
3444
  }
3448
3445
  }
@@ -3485,19 +3482,14 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
3485
3482
  let grantContext;
3486
3483
  let grantHierarchy;
3487
3484
  if (action === "grant") {
3488
- let hierarchy = loadAllLanes(ctx.hqRoot());
3489
- const ownerLanes = await readOutpostLaneRecords(ctx, outpostId);
3490
- const byId = new Map(hierarchy.map((record) => [record.lane_id, record]));
3491
- for (const ownerLane of ownerLanes) {
3492
- byId.set(ownerLane.lane_id, ownerLane);
3493
- }
3494
- hierarchy = [...byId.values()];
3485
+ const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId));
3495
3486
  grantHierarchy = hierarchy;
3496
3487
  grantContext = createOutpostHireGrantContext(lane, hierarchy);
3497
3488
  }
3498
3489
  else if ((action === "resume" || action === "message") &&
3499
3490
  lane.senior.kind === "lane") {
3500
- grantContext = createOutpostHireGrantContext(lane, loadAllLanes(ctx.hqRoot()));
3491
+ const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId));
3492
+ grantContext = createOutpostHireGrantContext(lane, hierarchy, action === "message" ? options.supersededMessageCallerId : undefined);
3501
3493
  }
3502
3494
  let dispatched;
3503
3495
  let grantContextRejected = false;
@@ -4304,336 +4296,6 @@ async function runOutpostIdentityBackfillTarget(ctx, opts) {
4304
4296
  marker_path: OUTPOST_ID_PATH,
4305
4297
  });
4306
4298
  }
4307
- function laneCanMoveBeforeStart(lane) {
4308
- return lane.state === "queued" &&
4309
- lane.pickup_validation_pending !== true &&
4310
- lane.admission_reserved_at === undefined &&
4311
- lane.provider_session_id === undefined &&
4312
- lane.provider_session_id_absent === undefined &&
4313
- lane.provider_home_at_spawn === undefined &&
4314
- lane.last_resume === undefined &&
4315
- lane.last_run_usage === undefined &&
4316
- lane.last_host_report_at === undefined &&
4317
- lane.pending_resume === undefined;
4318
- }
4319
- function readLaneTimeout(hqRoot, laneId) {
4320
- const raw = readJsonFile(timeoutPath(hqRoot, laneId));
4321
- if (raw === null)
4322
- return undefined;
4323
- if (typeof raw !== "object" || Array.isArray(raw) ||
4324
- typeof raw.timeout_secs !== "number" ||
4325
- typeof raw.deadline_at !== "string" ||
4326
- !Number.isSafeInteger(raw.timeout_secs) ||
4327
- raw.timeout_secs <= 0 ||
4328
- !Number.isFinite(Date.parse(raw.deadline_at))) {
4329
- throw new LanesError("lane_timeout_invalid", `Lane ${laneId} has a malformed timeout record`, {
4330
- lane_id: laneId,
4331
- });
4332
- }
4333
- const record = raw;
4334
- return { timeout_secs: record.timeout_secs, deadline_at: record.deadline_at };
4335
- }
4336
- async function runMove(ctx, laneId, opts) {
4337
- const hqRoot = ctx.hqRoot();
4338
- const policy = parsePlacementPolicy(opts.placement ?? "");
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);
4347
- if (!laneCanMoveBeforeStart(source)) {
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 });
4349
- }
4350
- const sourceHost = source.host;
4351
- const config = loadLanesConfig(hqRoot);
4352
- const capacity = await loadHostCapacitySnapshots(ctx, hqRoot, config);
4353
- let targetSnapshots = capacity.snapshots.filter((snapshot) => snapshot.host.kind !== sourceHost.kind || snapshot.host.id !== sourceHost.id);
4354
- if (policy.kind === "outpost") {
4355
- targetSnapshots = targetSnapshots.filter((snapshot) => snapshot.host.kind === "outpost" && snapshot.host.id === policy.id);
4356
- }
4357
- else if (policy.kind === "local") {
4358
- targetSnapshots = targetSnapshots.filter((snapshot) => snapshot.host.kind === "local");
4359
- }
4360
- const profileAccounts = source.worker_profile === undefined
4361
- ? undefined
4362
- : loadWorkerProfile(hqRoot, source.worker_profile).meta.accounts;
4363
- const allowedAccounts = source.admission_accounts ?? profileAccounts;
4364
- const choice = chooseHostForLane(targetSnapshots, {
4365
- provider: source.provider,
4366
- ...(source.account !== undefined ? { account: source.account } : {}),
4367
- ...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
4368
- });
4369
- let moved;
4370
- if (laneRunsHere(source, reconcileHostFromCtx(ctx))) {
4371
- moved = await commitLaneMove(ctx, laneId, choice.host, choice.account, placementEnabled);
4372
- }
4373
- else if (source.host.kind === "outpost") {
4374
- moved = await dispatchRemoteLaneMove(ctx, source, choice.host, choice.account);
4375
- }
4376
- else {
4377
- 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" });
4378
- }
4379
- printJson({
4380
- ok: true,
4381
- action: "move",
4382
- lane_id: laneId,
4383
- state: moved.state,
4384
- host: moved.host,
4385
- account: moved.account ?? null,
4386
- lane: moved,
4387
- });
4388
- }
4389
- async function commitLaneMove(ctx, laneId, target, account, readPlacementEnabled) {
4390
- const hqRoot = ctx.hqRoot();
4391
- const before = readLane(hqRoot, laneId);
4392
- assertLaneRunsHereForOperatorMutation(ctx, before, "move-target");
4393
- await assertPlacementEnabled(ctx, resolveLaneCompany(hqRoot, before).company ?? undefined, readPlacementEnabled);
4394
- if (before.host.kind === target.kind && before.host.id === target.id) {
4395
- throw new LanesError("lane_move_same_host", `Lane ${laneId} is already on ${target.kind}:${target.id}`, {
4396
- lane_id: laneId,
4397
- host: target,
4398
- });
4399
- }
4400
- if (!laneCanMoveBeforeStart(before)) {
4401
- 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 });
4402
- }
4403
- if (target.kind === "outpost") {
4404
- let known;
4405
- try {
4406
- known = await ctx.listOutpostIds();
4407
- }
4408
- catch (err) {
4409
- throw new LanesError("outpost_list_unavailable", `Failed to list Outpost ids: ${dispatchErrorMessage(err)}`, {});
4410
- }
4411
- assertKnownOutpost(target.id, known);
4412
- }
4413
- const accountConfig = loadLanesConfig(hqRoot).providers[before.provider].accounts[account];
4414
- if (accountConfig === undefined || !accountConfig.enabled) {
4415
- throw new LanesError("lane_move_account_invalid", `Account ${account} is unavailable for ${before.provider}`, {
4416
- lane_id: laneId,
4417
- provider: before.provider,
4418
- account,
4419
- });
4420
- }
4421
- const profileAccounts = before.worker_profile === undefined
4422
- ? undefined
4423
- : loadWorkerProfile(hqRoot, before.worker_profile).meta.accounts;
4424
- const allowedAccounts = before.admission_accounts ?? profileAccounts;
4425
- if (allowedAccounts !== undefined &&
4426
- !allowedAccounts.includes(account)) {
4427
- throw new LanesError("lane_move_account_invalid", `Account ${account} is not allowed by lane ${laneId}`, {
4428
- lane_id: laneId,
4429
- account,
4430
- allowed_accounts: allowedAccounts,
4431
- });
4432
- }
4433
- const briefText = target.kind === "outpost"
4434
- ? (() => {
4435
- const briefPath = path.join(runDir(hqRoot, laneId), "brief.md");
4436
- if (!ctx.existsSync(briefPath)) {
4437
- throw new LanesError("lane_move_brief_missing", `Lane ${laneId} has no stored brief to dispatch`, {
4438
- lane_id: laneId,
4439
- });
4440
- }
4441
- return ctx.readFileSync(briefPath, "utf8");
4442
- })()
4443
- : undefined;
4444
- const timeout = target.kind === "outpost" ? readLaneTimeout(hqRoot, laneId) : undefined;
4445
- const moved = await withLaneAdmissionLock(hqRoot, before.provider, async () => {
4446
- const current = readLane(hqRoot, laneId);
4447
- assertLaneRunsHereForOperatorMutation(ctx, current, "move-target");
4448
- if (!laneCanMoveBeforeStart(current)) {
4449
- 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 });
4450
- }
4451
- return updateLane(hqRoot, laneId, current.version, "command", (live) => ({
4452
- ...live,
4453
- host: target,
4454
- account,
4455
- queued_reason: undefined,
4456
- admission_reserved_at: undefined,
4457
- }));
4458
- }, ctx.sleep);
4459
- if (target.kind !== "outpost")
4460
- return moved;
4461
- try {
4462
- return await dispatchCreatedOutpostLane(ctx, {
4463
- hqRoot,
4464
- lane: moved,
4465
- outpostId: target.id,
4466
- briefText: briefText ?? "",
4467
- timeout,
4468
- preserveQueuedOnFailure: true,
4469
- });
4470
- }
4471
- catch (err) {
4472
- const dispatchDetails = isLanesError(err) ? err.details : {};
4473
- const commandId = typeof dispatchDetails.command_id === "string" &&
4474
- dispatchDetails.command_id.trim() !== ""
4475
- ? dispatchDetails.command_id
4476
- : undefined;
4477
- const definitelyNotSubmitted = commandId === undefined &&
4478
- dispatchDetails.outcome_unknown !== true &&
4479
- dispatchDetails.done !== false;
4480
- const knownNoPersistence = (isLanesError(err) && err.error === "outpost_self_target") ||
4481
- (dispatchDetails.done === true && dispatchDetails.pickup_persisted === false);
4482
- if (!definitelyNotSubmitted && !knownNoPersistence) {
4483
- const checkCommand = `hq lanes show ${laneId}`;
4484
- const message = `Failed to dispatch lane ${laneId} to Outpost ${target.id}; outcome is unknown and the lane remains assigned to Outpost ${target.id}. ` +
4485
- `Check the target copy by running \`${checkCommand}\` on Outpost ${target.id}. ` +
4486
- dispatchErrorMessage(err);
4487
- throw new LanesError("lane_move_dispatch_failed", message, {
4488
- lane_id: laneId,
4489
- message,
4490
- source_host: before.host,
4491
- target_host: target,
4492
- source_restored: false,
4493
- outcome: "unknown",
4494
- ...(commandId !== undefined ? { command_id: commandId } : {}),
4495
- check_command: checkCommand,
4496
- ...(dispatchDetails.owner_error !== undefined
4497
- ? { owner_error: dispatchDetails.owner_error }
4498
- : {}),
4499
- });
4500
- }
4501
- let restored = false;
4502
- let rollbackError;
4503
- try {
4504
- restored = await withLaneAdmissionLock(hqRoot, before.provider, async () => {
4505
- const current = readLane(hqRoot, laneId);
4506
- if (current.version !== moved.version ||
4507
- current.state !== "queued" ||
4508
- current.host.kind !== target.kind ||
4509
- current.host.id !== target.id ||
4510
- current.account !== account) {
4511
- return false;
4512
- }
4513
- updateLane(hqRoot, laneId, current.version, "command", (live) => ({
4514
- ...live,
4515
- host: before.host,
4516
- account: before.account,
4517
- queued_reason: before.queued_reason,
4518
- admission_reserved_at: before.admission_reserved_at,
4519
- }));
4520
- return true;
4521
- }, ctx.sleep);
4522
- }
4523
- catch (restoreErr) {
4524
- rollbackError = restoreErr;
4525
- }
4526
- const dispatchMessage = dispatchErrorMessage(err);
4527
- const outcome = restored
4528
- ? "the lane stayed queued on its source"
4529
- : rollbackError === undefined
4530
- ? "the lane changed after the move and was not overwritten"
4531
- : `source restoration failed: ${dispatchErrorMessage(rollbackError)}`;
4532
- const message = `Failed to dispatch lane ${laneId} to Outpost ${target.id}; ${outcome}: ${dispatchMessage}`;
4533
- throw new LanesError("lane_move_dispatch_failed", message, {
4534
- lane_id: laneId,
4535
- message,
4536
- source_host: before.host,
4537
- target_host: target,
4538
- source_restored: restored,
4539
- ...(dispatchDetails.owner_error !== undefined
4540
- ? { owner_error: dispatchDetails.owner_error }
4541
- : {}),
4542
- ...(rollbackError !== undefined
4543
- ? { rollback_error: dispatchErrorMessage(rollbackError) }
4544
- : {}),
4545
- });
4546
- }
4547
- }
4548
- async function dispatchRemoteLaneMove(ctx, source, target, account) {
4549
- if (source.host.kind !== "outpost") {
4550
- throw new LanesError("lane_move_wrong_host", `Lane ${source.lane_id} is not Outpost-hosted`, {
4551
- lane_id: source.lane_id,
4552
- host: source.host,
4553
- });
4554
- }
4555
- let dispatched;
4556
- try {
4557
- dispatched = await ctx.dispatchOutpostExec({
4558
- outpostId: source.host.id,
4559
- command: buildOutpostMoveTargetCommand({
4560
- laneId: source.lane_id,
4561
- host: target,
4562
- account,
4563
- }),
4564
- timeoutSeconds: 120,
4565
- waitMs: 120_000,
4566
- });
4567
- }
4568
- catch (err) {
4569
- 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 });
4570
- }
4571
- const ownerError = ownerErrorFromOutput(dispatched.stdout);
4572
- if (ownerError) {
4573
- throw new LanesError("outpost_dispatch_failed", `Outpost ${source.host.id} rejected the lane move: ${ownerError.message}`, {
4574
- lane_id: source.lane_id,
4575
- source_outpost: source.host.id,
4576
- command_id: dispatched.commandId,
4577
- status: dispatched.status,
4578
- done: dispatched.done ?? false,
4579
- exit_code: dispatched.exitCode,
4580
- owner_error: ownerError,
4581
- });
4582
- }
4583
- if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
4584
- throw new LanesError("lane_move_dispatch_failed", `Outpost ${source.host.id} did not confirm the lane move`, {
4585
- lane_id: source.lane_id,
4586
- source_outpost: source.host.id,
4587
- command_id: dispatched.commandId,
4588
- status: dispatched.status,
4589
- done: dispatched.done ?? false,
4590
- exit_code: dispatched.exitCode,
4591
- });
4592
- }
4593
- const remote = parseRemoteJson(dispatched.stdout, "Outpost lane move");
4594
- const remoteHost = remote?.host;
4595
- if (remote?.ok !== true ||
4596
- remote.action !== "move-target" ||
4597
- remote.lane_id !== source.lane_id ||
4598
- remote.state !== "queued" ||
4599
- remote.account !== account ||
4600
- remoteHost === null || typeof remoteHost !== "object" || Array.isArray(remoteHost) ||
4601
- remoteHost.kind !== target.kind ||
4602
- remoteHost.id !== target.id) {
4603
- throw new LanesError("lane_move_unconfirmed", `Outpost ${source.host.id} returned an invalid lane move result`, {
4604
- lane_id: source.lane_id,
4605
- source_outpost: source.host.id,
4606
- target,
4607
- });
4608
- }
4609
- return {
4610
- ...source,
4611
- state: "queued",
4612
- host: target,
4613
- account,
4614
- };
4615
- }
4616
- async function runMoveTarget(ctx, laneId, opts) {
4617
- const kind = opts.hostKind?.trim();
4618
- const id = opts.hostId?.trim();
4619
- const account = opts.account?.trim();
4620
- if ((kind !== "local" && kind !== "outpost") || !id || !account) {
4621
- throw new LanesError("lane_move_target_invalid", "Move target requires a valid host kind, host id, and account", {
4622
- host_kind: kind ?? null,
4623
- host_id: id ?? null,
4624
- account: account ?? null,
4625
- });
4626
- }
4627
- const moved = await commitLaneMove(ctx, laneId, { kind, id }, account);
4628
- printJson({
4629
- ok: true,
4630
- action: "move-target",
4631
- lane_id: moved.lane_id,
4632
- state: moved.state,
4633
- host: moved.host,
4634
- account: moved.account ?? null,
4635
- });
4636
- }
4637
4299
  async function refreshOutpostLaneCopiesForShow(ctx, lane, localLanes) {
4638
4300
  if (lane.host.kind !== "outpost" || laneRunsHere(lane, reconcileHostFromCtx(ctx))) {
4639
4301
  return lane;
@@ -4779,25 +4441,78 @@ async function runShow(ctx, laneId) {
4779
4441
  : null,
4780
4442
  });
4781
4443
  }
4782
- function callerMatchesSenior(ctx, seniorRef) {
4444
+ function formatSessionSeniorValue(value) {
4445
+ if (value === undefined)
4446
+ return "<undefined>";
4447
+ try {
4448
+ return JSON.stringify(value) ?? String(value);
4449
+ }
4450
+ catch {
4451
+ return String(value);
4452
+ }
4453
+ }
4454
+ function callerSeniorAuthorization(ctx, seniorRef) {
4783
4455
  const sessionId = sessionIdFromEnv(ctx.env);
4784
4456
  const callerLaneId = ctx.env.HQ_LANE_ID?.trim() || undefined;
4785
4457
  if (seniorRef.kind === "lane") {
4786
- return callerLaneId !== undefined && callerLaneId === seniorRef.id;
4458
+ if (callerLaneId === seniorRef.id)
4459
+ return { authorized: true };
4460
+ if (callerLaneId !== undefined)
4461
+ return { authorized: false, reason: "caller is a different lane" };
4462
+ if (!sessionId)
4463
+ return { authorized: false, reason: "no session id" };
4464
+ return { authorized: false, reason: "caller is not a lane" };
4465
+ }
4466
+ if (callerLaneId !== undefined)
4467
+ return { authorized: false, reason: "caller is a lane" };
4468
+ if (!sessionId)
4469
+ return { authorized: false, reason: "no session id" };
4470
+ if (seniorRef.kind === "session") {
4471
+ return sessionId === seniorRef.id
4472
+ ? { authorized: true }
4473
+ : { authorized: false, reason: `session id ${sessionId} is not senior session ${seniorRef.id}` };
4787
4474
  }
4788
- if (!sessionId || callerLaneId !== undefined)
4789
- return false;
4790
- if (seniorRef.kind === "session")
4791
- return sessionId === seniorRef.id;
4792
4475
  if (seniorRef.kind !== "user")
4793
- return false;
4476
+ return { authorized: false, reason: "senior is not a user" };
4794
4477
  const loaded = loadSessionMeta(ctx.hqRoot(), sessionId, ctx);
4795
- if (loaded.status !== "ok")
4796
- return false;
4797
- const senior = loaded.doc.senior;
4798
- return senior === "user" ||
4478
+ if (loaded.status === "missing") {
4479
+ return {
4480
+ authorized: false,
4481
+ reason: "session meta missing",
4482
+ repair: "if this is the owner's session, run bash core/scripts/hq-session.sh set senior user",
4483
+ };
4484
+ }
4485
+ if (loaded.status === "unreadable") {
4486
+ return { authorized: false, reason: "session meta unreadable" };
4487
+ }
4488
+ if (loaded.status === "malformed") {
4489
+ return { authorized: false, reason: "session meta invalid" };
4490
+ }
4491
+ const senior = sessionSeniorValue(loaded.doc);
4492
+ if (senior === "user" ||
4799
4493
  (senior !== null && typeof senior === "object" && !Array.isArray(senior) &&
4800
- senior.kind === "user");
4494
+ senior.kind === "user")) {
4495
+ return { authorized: true };
4496
+ }
4497
+ return {
4498
+ authorized: false,
4499
+ reason: `session senior is ${formatSessionSeniorValue(senior)}`,
4500
+ };
4501
+ }
4502
+ function callerSeniorReason(authorization) {
4503
+ if (authorization.authorized)
4504
+ return "";
4505
+ return authorization.repair
4506
+ ? `${authorization.reason}; ${authorization.repair}`
4507
+ : authorization.reason;
4508
+ }
4509
+ function callerMatchesSenior(ctx, seniorRef) {
4510
+ return callerSeniorAuthorization(ctx, seniorRef).authorized;
4511
+ }
4512
+ function appendCallerSeniorReason(message, authorization) {
4513
+ return authorization.authorized
4514
+ ? message
4515
+ : `${message}; authorization refused: ${callerSeniorReason(authorization)}`;
4801
4516
  }
4802
4517
  function laneGrantTargetHostedHere(ctx, lane) {
4803
4518
  if (lane.host.kind === "local") {
@@ -4805,14 +4520,17 @@ function laneGrantTargetHostedHere(ctx, lane) {
4805
4520
  }
4806
4521
  return lane.host.kind === "outpost" && laneIsHostedHere(ctx, lane);
4807
4522
  }
4808
- function forceAdmissionAuthorized(ctx, lane) {
4809
- return callerMatchesSenior(ctx, lane.senior);
4523
+ function forceAdmissionAuthorization(ctx, lane) {
4524
+ return callerSeniorAuthorization(ctx, lane.senior);
4810
4525
  }
4811
4526
  async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContext) {
4812
4527
  const hqRoot = ctx.hqRoot();
4813
4528
  const initial = readLane(hqRoot, laneId);
4814
- if (authorization === "senior" && !callerMatchesSenior(ctx, initial.senior)) {
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 });
4529
+ const initialCallerAuthorization = authorization === "senior"
4530
+ ? callerSeniorAuthorization(ctx, initial.senior)
4531
+ : { authorized: true };
4532
+ if (!initialCallerAuthorization.authorized) {
4533
+ throw new LanesError("hire_grant_unauthorized", appendCallerSeniorReason(`Only senior ${seniorKey(initial.senior)} may change lane ${laneId}'s hire grant`, initialCallerAuthorization), { lane_id: laneId, senior: initial.senior, reason: initialCallerAuthorization.reason });
4816
4534
  }
4817
4535
  if (authorization === "ssm-owner-dispatch" &&
4818
4536
  !laneGrantTargetHostedHere(ctx, initial)) {
@@ -4830,8 +4548,11 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
4830
4548
  ];
4831
4549
  const updated = ctx.withGrantHireLocks(hqRoot, lockedLaneIds, () => {
4832
4550
  const current = readLane(hqRoot, laneId);
4833
- if (authorization === "senior" && !callerMatchesSenior(ctx, current.senior)) {
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 });
4551
+ const currentCallerAuthorization = authorization === "senior"
4552
+ ? callerSeniorAuthorization(ctx, current.senior)
4553
+ : { authorized: true };
4554
+ if (!currentCallerAuthorization.authorized) {
4555
+ throw new LanesError("hire_grant_unauthorized", appendCallerSeniorReason(`Only senior ${seniorKey(current.senior)} may change lane ${laneId}'s hire grant`, currentCallerAuthorization), { lane_id: laneId, senior: current.senior, reason: currentCallerAuthorization.reason });
4835
4556
  }
4836
4557
  if (authorization === "ssm-owner-dispatch" &&
4837
4558
  !laneGrantTargetHostedHere(ctx, current)) {
@@ -4944,8 +4665,9 @@ async function runKeepAlive(ctx, laneId, opts) {
4944
4665
  });
4945
4666
  }
4946
4667
  function assertKeepAliveChangeAllowed(ctx, lane, laneId) {
4947
- if (!callerMatchesSenior(ctx, lane.senior)) {
4948
- throw new LanesError("keep_alive_unauthorized", "Only senior " + seniorKey(lane.senior) + " may change lane " + laneId + "'s keep-alive setting", { lane_id: laneId, senior: lane.senior });
4668
+ const callerAuthorization = callerSeniorAuthorization(ctx, lane.senior);
4669
+ if (!callerAuthorization.authorized) {
4670
+ throw new LanesError("keep_alive_unauthorized", appendCallerSeniorReason("Only senior " + seniorKey(lane.senior) + " may change lane " + laneId + "'s keep-alive setting", callerAuthorization), { lane_id: laneId, senior: lane.senior, reason: callerAuthorization.reason });
4949
4671
  }
4950
4672
  if (!laneRunsHere(lane, reconcileHostFromCtx(ctx))) {
4951
4673
  throw new LanesError("keep_alive_owner_host_required", "Run hq lanes keep-alive " + laneId + " on its owner host " + lane.host.kind + ":" + lane.host.id, { lane_id: laneId, host: lane.host });
@@ -4960,10 +4682,14 @@ async function runGrant(ctx, laneId, opts) {
4960
4682
  }
4961
4683
  const hireGrant = parseHireGrant(opts.hire);
4962
4684
  const hqRoot = ctx.hqRoot();
4963
- const target = await resolveCommandLaneTarget(ctx, laneId, { outpostHint: true });
4685
+ const target = await resolveCommandLaneTarget(ctx, laneId, {
4686
+ outpostHint: true,
4687
+ outpostId: opts.outpost,
4688
+ });
4964
4689
  const initial = target.lane;
4965
- if (!callerMatchesSenior(ctx, initial.senior)) {
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 });
4690
+ const callerAuthorization = callerSeniorAuthorization(ctx, initial.senior);
4691
+ if (!callerAuthorization.authorized) {
4692
+ throw new LanesError("hire_grant_unauthorized", appendCallerSeniorReason(`Only senior ${seniorKey(initial.senior)} may change lane ${laneId}'s hire grant`, callerAuthorization), { lane_id: laneId, senior: initial.senior, reason: callerAuthorization.reason });
4967
4693
  }
4968
4694
  if (opts.outpost !== undefined) {
4969
4695
  const requestedOutpost = opts.outpost.trim();
@@ -5018,8 +4744,9 @@ async function runApprove(ctx, laneId, opts) {
5018
4744
  if (!allowed.includes(checkpoint)) {
5019
4745
  throw new LanesError("registry_worker_checkpoint_unknown", `Checkpoint ${JSON.stringify(checkpoint)} is not declared by the lane's registry worker`, { lane_id: laneId, checkpoint, accepted: allowed });
5020
4746
  }
5021
- if (!callerMatchesSenior(ctx, initial.senior)) {
5022
- throw new LanesError("registry_worker_approval_unauthorized", `Only senior ${seniorKey(initial.senior)} may approve lane ${laneId}'s registry worker checkpoint`, { lane_id: laneId, senior: initial.senior });
4747
+ const callerAuthorization = callerSeniorAuthorization(ctx, initial.senior);
4748
+ if (!callerAuthorization.authorized) {
4749
+ throw new LanesError("registry_worker_approval_unauthorized", appendCallerSeniorReason(`Only senior ${seniorKey(initial.senior)} may approve lane ${laneId}'s registry worker checkpoint`, callerAuthorization), { lane_id: laneId, senior: initial.senior, reason: callerAuthorization.reason });
5023
4750
  }
5024
4751
  if (remoteOutpost) {
5025
4752
  const approvalContext = createOutpostRegistryWorkerApprovalContext({
@@ -5045,8 +4772,9 @@ async function runApprove(ctx, laneId, opts) {
5045
4772
  const actor = resumeProfileRepinActor(ctx);
5046
4773
  const updated = retryUpdateLane(hqRoot, laneId, "command", (current) => {
5047
4774
  assertLaneRunsHereForOperatorMutation(ctx, current, "approve");
5048
- if (!callerMatchesSenior(ctx, current.senior)) {
5049
- throw new LanesError("registry_worker_approval_unauthorized", `Only senior ${seniorKey(current.senior)} may approve lane ${laneId}'s registry worker checkpoint`, { lane_id: laneId, senior: current.senior });
4775
+ const currentCallerAuthorization = callerSeniorAuthorization(ctx, current.senior);
4776
+ if (!currentCallerAuthorization.authorized) {
4777
+ throw new LanesError("registry_worker_approval_unauthorized", appendCallerSeniorReason(`Only senior ${seniorKey(current.senior)} may approve lane ${laneId}'s registry worker checkpoint`, currentCallerAuthorization), { lane_id: laneId, senior: current.senior, reason: currentCallerAuthorization.reason });
5050
4778
  }
5051
4779
  if (JSON.stringify(current.registry_worker) !== JSON.stringify(initial.registry_worker) ||
5052
4780
  !current.registry_worker ||
@@ -5212,8 +4940,9 @@ async function runAdmit(ctx, laneId, opts) {
5212
4940
  }
5213
4941
  let blockedBy;
5214
4942
  if (opts.force) {
5215
- if (!forceAdmissionAuthorized(ctx, lane)) {
5216
- throw new LanesError("force_not_authorized", `Only lane ${lane.senior.kind === "lane" ? lane.senior.id : lane.senior.kind === "session" ? lane.senior.id : "senior/operator"} may force admission of ${laneId}`, { lane_id: laneId, senior: lane.senior });
4943
+ const callerAuthorization = forceAdmissionAuthorization(ctx, lane);
4944
+ if (!callerAuthorization.authorized) {
4945
+ throw new LanesError("force_not_authorized", appendCallerSeniorReason(`Only lane ${lane.senior.kind === "lane" ? lane.senior.id : lane.senior.kind === "session" ? lane.senior.id : "senior/operator"} may force admission of ${laneId}`, callerAuthorization), { lane_id: laneId, senior: lane.senior, reason: callerAuthorization.reason });
5217
4946
  }
5218
4947
  const config = loadLanesConfig(hqRoot);
5219
4948
  const lanes = loadAllLanes(hqRoot).filter((row) => row.lane_id !== laneId && laneRunsHere(row, host));
@@ -5341,7 +5070,7 @@ function applyResumeProfileAndEffort(current, next, input) {
5341
5070
  ? registryWorkerRecord(profile.registryWorker)
5342
5071
  : undefined;
5343
5072
  if (input.profileRepin !== undefined &&
5344
- current.host.kind === "outpost" &&
5073
+ (current.host.kind === "outpost" || input.profileRepin.kind === "ssm-owner-dispatch") &&
5345
5074
  current.worker_profile_hash !== profile.contentHash) {
5346
5075
  const entry = {
5347
5076
  old_hash: current.worker_profile_hash ?? null,
@@ -7080,26 +6809,25 @@ export function registerLanesCommand(parent, deps = {}) {
7080
6809
  readOutpostCapacity,
7081
6810
  };
7082
6811
  const watchHandlers = createLaneWatchHandlers(watchHandlerDeps);
6812
+ const moveHandlers = createLaneMoveHandlers({
6813
+ assertKnownOutpost,
6814
+ assertLaneRunsHereForOperatorMutation,
6815
+ dispatchCreatedOutpostLane,
6816
+ loadHostCapacitySnapshots,
6817
+ ownerErrorFromOutput,
6818
+ parseRemoteJson,
6819
+ printJson,
6820
+ readJsonFile,
6821
+ reconcileHostFromCtx,
6822
+ runDir,
6823
+ timeoutPath,
6824
+ });
7083
6825
  const lanes = parent
7084
6826
  .command("lanes", { hidden: true })
7085
6827
  .description("Create, supervise, and steer worker lanes; routed decision queue");
7086
- registerLaneTrackerCommands(lanes, {
7087
- hqRoot: ctx.hqRoot,
7088
- resolveCompany: ctx.resolveCompany,
7089
- trackerEnabled: ctx.laneTrackerEnabled,
7090
- now: ctx.now,
7091
- actor: ctx.trackerActor,
7092
- }, wrap);
7093
- registerLaneTrackerImportCommand(lanes, {
7094
- hqRoot: ctx.hqRoot,
7095
- resolveCompany: ctx.resolveCompany,
7096
- trackerEnabled: ctx.laneTrackerEnabled,
7097
- now: ctx.now,
7098
- }, wrap);
7099
6828
  registerLaneChecksCommand(lanes, {
7100
6829
  hqRoot: ctx.hqRoot,
7101
6830
  resolveCompany: ctx.resolveCompany,
7102
- trackerEnabled: ctx.laneTrackerEnabled,
7103
6831
  now: ctx.now,
7104
6832
  stopLane: (laneId, reason) => runStop(ctx, laneId, reason, true),
7105
6833
  sessionState: (root, sessionId, now) => sessionLiveness(root, sessionId, {
@@ -7111,7 +6839,6 @@ export function registerLanesCommand(parent, deps = {}) {
7111
6839
  registerLaneReportCommands(lanes, {
7112
6840
  hqRoot: ctx.hqRoot,
7113
6841
  resolveCompany: ctx.resolveCompany,
7114
- trackerEnabled: ctx.laneTrackerEnabled,
7115
6842
  now: ctx.now,
7116
6843
  readQuestionBody: (root, question) => {
7117
6844
  const body = questionHandlers.requireQuestionBody(root, question);
@@ -7124,6 +6851,7 @@ export function registerLanesCommand(parent, deps = {}) {
7124
6851
  resolveCompany: ctx.resolveCompany,
7125
6852
  now: ctx.now,
7126
6853
  env: ctx.env,
6854
+ mergeHoldEnabled: ctx.mergeHoldEnabled,
7127
6855
  mergeReviewEnabled: ctx.mergeReviewEnabled,
7128
6856
  }, wrap);
7129
6857
  registerLaneReviewCommand(lanes, {
@@ -7262,7 +6990,7 @@ export function registerLanesCommand(parent, deps = {}) {
7262
6990
  .argument("<lane-id>", "Lane id")
7263
6991
  .requiredOption("--placement <mode>", "local | auto | outpost:<id>")
7264
6992
  .action(wrap(async (laneId, opts) => {
7265
- await runMove(ctx, laneId, opts);
6993
+ await moveHandlers.runMove(ctx, laneId, opts);
7266
6994
  }));
7267
6995
  lanes
7268
6996
  .command("move-target", { hidden: true })
@@ -7271,7 +6999,7 @@ export function registerLanesCommand(parent, deps = {}) {
7271
6999
  .requiredOption("--host-id <id>", "Target host id")
7272
7000
  .requiredOption("--account <id>", "Provider account id")
7273
7001
  .action(wrap(async (laneId, opts) => {
7274
- await runMoveTarget(ctx, laneId, opts);
7002
+ await moveHandlers.runMoveTarget(ctx, laneId, opts);
7275
7003
  }));
7276
7004
  lanes
7277
7005
  .command("balance-move-target", { hidden: true })