@indigoai-us/hq-cli 5.273.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 (51) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +32 -1
  3. package/dist/command-catalog.generated.d.ts +72 -0
  4. package/dist/command-catalog.generated.js +92 -0
  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/deploy-comments.d.ts +3 -0
  15. package/dist/commands/deploy-comments.js +159 -0
  16. package/dist/commands/lanes-move.d.ts +12 -0
  17. package/dist/commands/lanes-move.js +342 -0
  18. package/dist/commands/lanes-resume.js +3 -1
  19. package/dist/commands/lanes-shared.d.ts +36 -1
  20. package/dist/commands/lanes-shared.js +59 -0
  21. package/dist/commands/lanes.d.ts +1 -1
  22. package/dist/commands/lanes.js +204 -456
  23. package/dist/commands/report.js +2 -14
  24. package/dist/fast-core.js +1 -1
  25. package/dist/{commands/scaffold-fast.d.ts → fast-scaffold.d.ts} +1 -1
  26. package/dist/{commands/scaffold-fast.js → fast-scaffold.js} +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +2 -2
  29. package/dist/lib/checkpoint-ontology/apply.d.ts +8 -0
  30. package/dist/lib/checkpoint-ontology/apply.js +33 -0
  31. package/dist/lib/checkpoint-ontology/plan.d.ts +15 -0
  32. package/dist/lib/checkpoint-ontology/plan.js +15 -0
  33. package/dist/lib/deploy-api.d.ts +6 -0
  34. package/dist/lib/deploy-api.js +21 -0
  35. package/dist/lib/doctor/fix/apply.d.ts +17 -2
  36. package/dist/lib/doctor/fix/apply.js +111 -57
  37. package/dist/lib/doctor/fix/json-output.d.ts +9 -1
  38. package/dist/lib/doctor/fix/json-output.js +11 -0
  39. package/dist/lib/first-sync-next-step.d.ts +37 -0
  40. package/dist/lib/first-sync-next-step.js +189 -0
  41. package/dist/main.js +4 -1
  42. package/dist/utils/cli-telemetry.d.ts +7 -1
  43. package/dist/utils/cli-telemetry.js +84 -8
  44. package/dist/utils/contribution-table.d.ts +1 -1
  45. package/dist/utils/scaffold-fast-import-guard.d.ts +3 -0
  46. package/dist/utils/scaffold-fast-import-guard.js +28 -0
  47. package/dist/utils/scaffold-packlist-guard.d.ts +7 -0
  48. package/dist/utils/scaffold-packlist-guard.js +23 -0
  49. package/dist/utils/scaffold-sibling-references.d.ts +21 -0
  50. package/dist/utils/scaffold-sibling-references.js +156 -0
  51. package/package.json +1 -1
@@ -13,9 +13,12 @@ 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";
@@ -27,13 +30,12 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
27
30
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
28
31
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
29
32
  import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
30
- import { resolveProcessFlagCompanyContext } from "../lib/flag-registry.js";
31
33
  import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
32
34
  import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } from "../lib/lanes/outpost-profile-repin.js";
33
35
  import { createOutpostRegistryWorkerApprovalContext, parseOutpostRegistryWorkerApprovalContext, } from "../lib/lanes/outpost-registry-worker-approval.js";
34
36
  import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
35
37
  export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
36
- 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";
37
39
  import { outpostHireSummaryCacheKey, readOutpostHireSummaryCache, writeOutpostHireSummaryCache, } from "../lib/lanes/outpost-hire-cache.js";
38
40
  import { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "../lib/lanes/host-capacity.js";
39
41
  import { BALANCE_MOVABLE_STATES, selectClaudeAccount, } from "../lib/lanes/claude-balance.js";
@@ -73,46 +75,6 @@ export { MAX_LANE_DEPTH };
73
75
  export const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
74
76
  /** The lane → mesh seam. Defined in lib/lanes/mesh-events.ts; the watcher shares it. */
75
77
  export { emitLaneDecisionEvents };
76
- export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
77
- function lanePlacementFlagErrorCode(error) {
78
- if (error && typeof error === "object") {
79
- for (const key of ["code", "error", "name"]) {
80
- const value = error[key];
81
- if (typeof value === "string" && value.trim())
82
- return value;
83
- }
84
- }
85
- return "UnknownError";
86
- }
87
- function reportLanePlacementFlagReadFailure(error) {
88
- process.stderr.write(`${JSON.stringify({
89
- event: "lanes.outpost_load_balancing.flag_read_failed",
90
- flag: LANE_OUTPOST_LOAD_BALANCING_FLAG,
91
- error: lanePlacementFlagErrorCode(error),
92
- fallback: false,
93
- })}\n`);
94
- }
95
- function defaultLanePlacementEnabled(company) {
96
- return readLanesRegistryFlag(LANE_OUTPOST_LOAD_BALANCING_FLAG, {
97
- company,
98
- reportFailure: reportLanePlacementFlagReadFailure,
99
- });
100
- }
101
- async function readLanePlacementEnabled(ctx, company) {
102
- try {
103
- return await ctx.lanePlacementEnabled(company);
104
- }
105
- catch (error) {
106
- reportLanePlacementFlagReadFailure(error);
107
- return false;
108
- }
109
- }
110
- function processLanePlacementCompany(ctx) {
111
- const context = resolveProcessFlagCompanyContext(ctx.env);
112
- if (!context.companySlug)
113
- return undefined;
114
- return { slug: context.companySlug, uid: context.companyUid ?? "" };
115
- }
116
78
  function withoutProviderHome(value) {
117
79
  if (Array.isArray(value))
118
80
  return value.map(withoutProviderHome);
@@ -507,6 +469,12 @@ function mergeOwnerLaneHierarchy(supplied, ownerLanes) {
507
469
  byId.set(lane.lane_id, lane);
508
470
  return [...byId.values()];
509
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
+ }
510
478
  function outpostCliOverride(ctx, outpostId) {
511
479
  const envKey = `HQ_LANES_OUTPOST_CLI_${outpostId.toUpperCase().replace(/[^A-Z0-9_]/g, "_")}`;
512
480
  const configured = ctx.env[envKey];
@@ -857,8 +825,7 @@ async function mapWithConcurrency(values, concurrency, map) {
857
825
  async function withResumeHireAdmission(ctx, lane, fn, grantContext) {
858
826
  const hqRoot = ctx.hqRoot();
859
827
  if (grantContext !== undefined && (!ctx.isSsmOwnerDispatch() ||
860
- lane.host.kind !== "outpost" ||
861
- !laneIsHostedHere(ctx, lane) ||
828
+ !laneGrantTargetHostedHere(ctx, lane) ||
862
829
  lane.senior.kind !== "lane" ||
863
830
  !sameHireGrantSenior(lane.senior, grantContext.expected_senior))) {
864
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 });
@@ -1118,9 +1085,27 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1118
1085
  new Set(outpostIds).size !== outpostIds.length) {
1119
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 });
1120
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
+ }
1121
1095
  const matches = [];
1122
- for (const outpostId of outpostIds) {
1123
- 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
+ }
1124
1109
  for (const lane of ownerLanes) {
1125
1110
  if (lane.lane_id === laneId &&
1126
1111
  lane.host.kind === "local" &&
@@ -1130,7 +1115,7 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1130
1115
  }
1131
1116
  }
1132
1117
  if (matches.length === 0) {
1133
- throw commandLaneNotFound(laneId, options.outpostHint === true);
1118
+ throw commandLaneNotFound(laneId, options.outpostHint === true, failedOutpostIds.length > 0 ? { failed_outpost_ids: failedOutpostIds } : undefined);
1134
1119
  }
1135
1120
  if (matches.length !== 1) {
1136
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) });
@@ -1143,11 +1128,11 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1143
1128
  ownerHost: match.lane.host,
1144
1129
  };
1145
1130
  }
1146
- function commandLaneNotFound(laneId, outpostHint) {
1131
+ function commandLaneNotFound(laneId, outpostHint, details = {}) {
1147
1132
  const hint = outpostHint
1148
1133
  ? "; pass --outpost <id> to confirm a registered Outpost"
1149
1134
  : "";
1150
- 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 });
1151
1136
  }
1152
1137
  async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
1153
1138
  const localHost = laneHostFromCtx(ctx);
@@ -1274,29 +1259,18 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
1274
1259
  }
1275
1260
  return { snapshots, unavailableHosts };
1276
1261
  }
1277
- function parsePlacementPolicy(raw) {
1278
- const value = raw.trim();
1279
- if (value === "local")
1280
- return { kind: "local" };
1281
- if (value === "auto")
1282
- return { kind: "auto" };
1283
- const match = /^outpost:(.+)$/.exec(value);
1284
- const id = match?.[1]?.trim();
1285
- if (id)
1286
- return { kind: "outpost", id };
1287
- throw new LanesError("lane_placement_invalid", `Invalid placement ${JSON.stringify(raw)}. Accepted: local, auto, outpost:<id>`, { value: raw, accepted: ["local", "auto", "outpost:<id>"] });
1288
- }
1289
- async function assertPlacementEnabled(ctx, company, readPlacementEnabled = () => readLanePlacementEnabled(ctx, company)) {
1290
- if (await readPlacementEnabled())
1291
- return;
1292
- throw new LanesError("lane_placement_disabled", `Lane placement options require hq-flags gate ${LANE_OUTPOST_LOAD_BALANCING_FLAG}`, { flag: LANE_OUTPOST_LOAD_BALANCING_FLAG });
1293
- }
1294
1262
  function loadAllQuestions(hqRoot) {
1295
1263
  return listQuestionIds(hqRoot).map((id) => readQuestion(hqRoot, id));
1296
1264
  }
1297
1265
  function sessionMetaPath(hqRoot, sessionId) {
1298
1266
  return path.join(hqRoot, "workspace", "sessions", sessionId, "meta.yaml");
1299
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
+ }
1300
1274
  function loadSessionMeta(hqRoot, sessionId, ctx) {
1301
1275
  const filePath = sessionMetaPath(hqRoot, sessionId);
1302
1276
  if (!ctx.existsSync(filePath))
@@ -1317,7 +1291,7 @@ function loadSessionMeta(hqRoot, sessionId, ctx) {
1317
1291
  process.stderr.write(`hq lanes: failed to parse session meta ${filePath}: ${err instanceof Error ? err.message : String(err)}\n`);
1318
1292
  return { status: "malformed" };
1319
1293
  }
1320
- if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
1294
+ if (!isSessionMetaDocument(doc)) {
1321
1295
  return { status: "malformed" };
1322
1296
  }
1323
1297
  return { status: "ok", doc: doc };
@@ -1328,11 +1302,21 @@ function readSessionMeta(hqRoot, sessionId, ctx) {
1328
1302
  return null;
1329
1303
  return loaded.doc;
1330
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
+ }
1331
1315
  function seniorOfSession(hqRoot, sessionId, ctx) {
1332
1316
  const meta = readSessionMeta(hqRoot, sessionId, ctx);
1333
1317
  if (!meta)
1334
1318
  return { kind: "user" };
1335
- const senior = meta.senior;
1319
+ const senior = sessionSeniorValue(meta);
1336
1320
  if (typeof senior === "string" && senior.trim()) {
1337
1321
  return parseSeniorRef(senior);
1338
1322
  }
@@ -2319,13 +2303,16 @@ async function runCreate(ctx, opts) {
2319
2303
  const company = companyFlag
2320
2304
  ? await ctx.resolveCompany(companyFlag)
2321
2305
  : null;
2306
+ const callerAuthorization = requestedHireGrant !== undefined || ctx.env.HQ_LANE_ID?.trim()
2307
+ ? callerSeniorAuthorization(ctx, senior)
2308
+ : { authorized: true };
2322
2309
  if (requestedHireGrant !== undefined &&
2323
- !callerMatchesSenior(ctx, senior)) {
2324
- 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 });
2325
2312
  }
2326
2313
  if (ctx.env.HQ_LANE_ID?.trim() &&
2327
- !callerMatchesSenior(ctx, senior)) {
2328
- 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 });
2329
2316
  }
2330
2317
  const hireGrant = requestedHireGrant ?? 0;
2331
2318
  const laneDepth = derivedLaneDepth(hqRoot, senior, ctx);
@@ -3349,7 +3336,7 @@ function authorizeFailedResume(ctx, lane, encodedContext) {
3349
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 });
3350
3337
  }
3351
3338
  const context = parseOutpostFailedResumeContext(encodedContext, lane.lane_id);
3352
- if (lane.host.kind !== "outpost" ||
3339
+ if (!laneGrantTargetHostedHere(ctx, lane) ||
3353
3340
  !sameHireGrantSenior(lane.senior, context.expectedSenior)) {
3354
3341
  throw new LanesError("failed_resume_senior_changed", `Lane ${lane.lane_id}'s senior changed after its failed-resume request was authorized`, {
3355
3342
  lane_id: lane.lane_id,
@@ -3359,18 +3346,25 @@ function authorizeFailedResume(ctx, lane, encodedContext) {
3359
3346
  }
3360
3347
  return context.actor;
3361
3348
  }
3362
- if (!callerMatchesSenior(ctx, lane.senior)) {
3363
- 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 });
3364
3352
  }
3365
3353
  return resumeProfileRepinActor(ctx);
3366
3354
  }
3367
- function workerProfileHashMismatch(lane, profile) {
3368
- 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, {
3369
3360
  lane_id: lane.lane_id,
3370
3361
  worker: profile.id,
3371
3362
  outpost: lane.host.kind === "outpost" ? lane.host.id : null,
3372
3363
  expected_hash: lane.worker_profile_hash ?? null,
3373
3364
  actual_hash: profile.profile.contentHash,
3365
+ ...(callerAuthorization && !callerAuthorization.authorized
3366
+ ? { reason: callerAuthorization.reason }
3367
+ : {}),
3374
3368
  });
3375
3369
  }
3376
3370
  function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
@@ -3390,7 +3384,7 @@ function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
3390
3384
  ? new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is invalid", { lane_id: lane.lane_id })
3391
3385
  : workerProfileHashMismatch(lane, profile);
3392
3386
  }
3393
- if (lane.host.kind !== "outpost" ||
3387
+ if (!laneGrantTargetHostedHere(ctx, lane) ||
3394
3388
  !sameHireGrantSenior(lane.senior, context.expected_senior)) {
3395
3389
  throw profile === undefined
3396
3390
  ? new LanesError("worker_profile_hash_mismatch", "Lane senior changed after profile re-pin authorization", { lane_id: lane.lane_id })
@@ -3398,7 +3392,7 @@ function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
3398
3392
  }
3399
3393
  }
3400
3394
  if (profile === undefined ||
3401
- lane.host.kind !== "outpost" ||
3395
+ (lane.host.kind !== "outpost" && !(ssmOwnerDispatch && laneGrantTargetHostedHere(ctx, lane))) ||
3402
3396
  lane.worker_profile_hash === profile.profile.contentHash) {
3403
3397
  return undefined;
3404
3398
  }
@@ -3413,8 +3407,9 @@ function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
3413
3407
  newHash: profile.profile.contentHash,
3414
3408
  };
3415
3409
  }
3416
- if (context !== undefined || !callerMatchesSenior(ctx, lane.senior)) {
3417
- 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);
3418
3413
  }
3419
3414
  return {
3420
3415
  kind: "senior",
@@ -3425,18 +3420,26 @@ function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
3425
3420
  };
3426
3421
  }
3427
3422
  function assertResumeProfileRepinStillAuthorized(ctx, current, authorization) {
3428
- const authorized = current.host.kind === "outpost" &&
3429
- 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) &&
3430
3427
  (authorization.kind === "ssm-owner-dispatch"
3431
- ? ctx.isSsmOwnerDispatch()
3432
- : callerMatchesSenior(ctx, current.senior));
3428
+ ? ctx.isSsmOwnerDispatch() && laneGrantTargetHostedHere(ctx, current)
3429
+ : current.host.kind === "outpost" && callerAuthorization?.authorized === true);
3433
3430
  if (!authorized) {
3434
- 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, {
3435
3435
  lane_id: current.lane_id,
3436
3436
  expected_senior: authorization.expectedSenior,
3437
3437
  current_senior: current.senior,
3438
3438
  expected_hash: current.worker_profile_hash ?? null,
3439
3439
  actual_hash: authorization.newHash,
3440
+ ...(callerAuthorization && !callerAuthorization.authorized
3441
+ ? { reason: callerAuthorization.reason }
3442
+ : {}),
3440
3443
  });
3441
3444
  }
3442
3445
  }
@@ -3479,19 +3482,14 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
3479
3482
  let grantContext;
3480
3483
  let grantHierarchy;
3481
3484
  if (action === "grant") {
3482
- let hierarchy = loadAllLanes(ctx.hqRoot());
3483
- const ownerLanes = await readOutpostLaneRecords(ctx, outpostId);
3484
- const byId = new Map(hierarchy.map((record) => [record.lane_id, record]));
3485
- for (const ownerLane of ownerLanes) {
3486
- byId.set(ownerLane.lane_id, ownerLane);
3487
- }
3488
- hierarchy = [...byId.values()];
3485
+ const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId));
3489
3486
  grantHierarchy = hierarchy;
3490
3487
  grantContext = createOutpostHireGrantContext(lane, hierarchy);
3491
3488
  }
3492
3489
  else if ((action === "resume" || action === "message") &&
3493
3490
  lane.senior.kind === "lane") {
3494
- grantContext = createOutpostHireGrantContext(lane, loadAllLanes(ctx.hqRoot()), action === "message" ? options.supersededMessageCallerId : undefined);
3491
+ const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId));
3492
+ grantContext = createOutpostHireGrantContext(lane, hierarchy, action === "message" ? options.supersededMessageCallerId : undefined);
3495
3493
  }
3496
3494
  let dispatched;
3497
3495
  let grantContextRejected = false;
@@ -4298,336 +4296,6 @@ async function runOutpostIdentityBackfillTarget(ctx, opts) {
4298
4296
  marker_path: OUTPOST_ID_PATH,
4299
4297
  });
4300
4298
  }
4301
- function laneCanMoveBeforeStart(lane) {
4302
- return lane.state === "queued" &&
4303
- lane.pickup_validation_pending !== true &&
4304
- lane.admission_reserved_at === undefined &&
4305
- lane.provider_session_id === undefined &&
4306
- lane.provider_session_id_absent === undefined &&
4307
- lane.provider_home_at_spawn === undefined &&
4308
- lane.last_resume === undefined &&
4309
- lane.last_run_usage === undefined &&
4310
- lane.last_host_report_at === undefined &&
4311
- lane.pending_resume === undefined;
4312
- }
4313
- function readLaneTimeout(hqRoot, laneId) {
4314
- const raw = readJsonFile(timeoutPath(hqRoot, laneId));
4315
- if (raw === null)
4316
- return undefined;
4317
- if (typeof raw !== "object" || Array.isArray(raw) ||
4318
- typeof raw.timeout_secs !== "number" ||
4319
- typeof raw.deadline_at !== "string" ||
4320
- !Number.isSafeInteger(raw.timeout_secs) ||
4321
- raw.timeout_secs <= 0 ||
4322
- !Number.isFinite(Date.parse(raw.deadline_at))) {
4323
- throw new LanesError("lane_timeout_invalid", `Lane ${laneId} has a malformed timeout record`, {
4324
- lane_id: laneId,
4325
- });
4326
- }
4327
- const record = raw;
4328
- return { timeout_secs: record.timeout_secs, deadline_at: record.deadline_at };
4329
- }
4330
- async function runMove(ctx, laneId, opts) {
4331
- const hqRoot = ctx.hqRoot();
4332
- const policy = parsePlacementPolicy(opts.placement ?? "");
4333
- const source = readLane(hqRoot, laneId);
4334
- const placementCompany = resolveLaneCompany(hqRoot, source).company ?? undefined;
4335
- let placementGate;
4336
- const placementEnabled = () => {
4337
- placementGate ??= readLanePlacementEnabled(ctx, placementCompany);
4338
- return placementGate;
4339
- };
4340
- await assertPlacementEnabled(ctx, placementCompany, placementEnabled);
4341
- if (!laneCanMoveBeforeStart(source)) {
4342
- 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 });
4343
- }
4344
- const sourceHost = source.host;
4345
- const config = loadLanesConfig(hqRoot);
4346
- const capacity = await loadHostCapacitySnapshots(ctx, hqRoot, config);
4347
- let targetSnapshots = capacity.snapshots.filter((snapshot) => snapshot.host.kind !== sourceHost.kind || snapshot.host.id !== sourceHost.id);
4348
- if (policy.kind === "outpost") {
4349
- targetSnapshots = targetSnapshots.filter((snapshot) => snapshot.host.kind === "outpost" && snapshot.host.id === policy.id);
4350
- }
4351
- else if (policy.kind === "local") {
4352
- targetSnapshots = targetSnapshots.filter((snapshot) => snapshot.host.kind === "local");
4353
- }
4354
- const profileAccounts = source.worker_profile === undefined
4355
- ? undefined
4356
- : loadWorkerProfile(hqRoot, source.worker_profile).meta.accounts;
4357
- const allowedAccounts = source.admission_accounts ?? profileAccounts;
4358
- const choice = chooseHostForLane(targetSnapshots, {
4359
- provider: source.provider,
4360
- ...(source.account !== undefined ? { account: source.account } : {}),
4361
- ...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
4362
- });
4363
- let moved;
4364
- if (laneRunsHere(source, reconcileHostFromCtx(ctx))) {
4365
- moved = await commitLaneMove(ctx, laneId, choice.host, choice.account, placementEnabled);
4366
- }
4367
- else if (source.host.kind === "outpost") {
4368
- moved = await dispatchRemoteLaneMove(ctx, source, choice.host, choice.account);
4369
- }
4370
- else {
4371
- 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" });
4372
- }
4373
- printJson({
4374
- ok: true,
4375
- action: "move",
4376
- lane_id: laneId,
4377
- state: moved.state,
4378
- host: moved.host,
4379
- account: moved.account ?? null,
4380
- lane: moved,
4381
- });
4382
- }
4383
- async function commitLaneMove(ctx, laneId, target, account, readPlacementEnabled) {
4384
- const hqRoot = ctx.hqRoot();
4385
- const before = readLane(hqRoot, laneId);
4386
- assertLaneRunsHereForOperatorMutation(ctx, before, "move-target");
4387
- await assertPlacementEnabled(ctx, resolveLaneCompany(hqRoot, before).company ?? undefined, readPlacementEnabled);
4388
- if (before.host.kind === target.kind && before.host.id === target.id) {
4389
- throw new LanesError("lane_move_same_host", `Lane ${laneId} is already on ${target.kind}:${target.id}`, {
4390
- lane_id: laneId,
4391
- host: target,
4392
- });
4393
- }
4394
- if (!laneCanMoveBeforeStart(before)) {
4395
- 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 });
4396
- }
4397
- if (target.kind === "outpost") {
4398
- let known;
4399
- try {
4400
- known = await ctx.listOutpostIds();
4401
- }
4402
- catch (err) {
4403
- throw new LanesError("outpost_list_unavailable", `Failed to list Outpost ids: ${dispatchErrorMessage(err)}`, {});
4404
- }
4405
- assertKnownOutpost(target.id, known);
4406
- }
4407
- const accountConfig = loadLanesConfig(hqRoot).providers[before.provider].accounts[account];
4408
- if (accountConfig === undefined || !accountConfig.enabled) {
4409
- throw new LanesError("lane_move_account_invalid", `Account ${account} is unavailable for ${before.provider}`, {
4410
- lane_id: laneId,
4411
- provider: before.provider,
4412
- account,
4413
- });
4414
- }
4415
- const profileAccounts = before.worker_profile === undefined
4416
- ? undefined
4417
- : loadWorkerProfile(hqRoot, before.worker_profile).meta.accounts;
4418
- const allowedAccounts = before.admission_accounts ?? profileAccounts;
4419
- if (allowedAccounts !== undefined &&
4420
- !allowedAccounts.includes(account)) {
4421
- throw new LanesError("lane_move_account_invalid", `Account ${account} is not allowed by lane ${laneId}`, {
4422
- lane_id: laneId,
4423
- account,
4424
- allowed_accounts: allowedAccounts,
4425
- });
4426
- }
4427
- const briefText = target.kind === "outpost"
4428
- ? (() => {
4429
- const briefPath = path.join(runDir(hqRoot, laneId), "brief.md");
4430
- if (!ctx.existsSync(briefPath)) {
4431
- throw new LanesError("lane_move_brief_missing", `Lane ${laneId} has no stored brief to dispatch`, {
4432
- lane_id: laneId,
4433
- });
4434
- }
4435
- return ctx.readFileSync(briefPath, "utf8");
4436
- })()
4437
- : undefined;
4438
- const timeout = target.kind === "outpost" ? readLaneTimeout(hqRoot, laneId) : undefined;
4439
- const moved = await withLaneAdmissionLock(hqRoot, before.provider, async () => {
4440
- const current = readLane(hqRoot, laneId);
4441
- assertLaneRunsHereForOperatorMutation(ctx, current, "move-target");
4442
- if (!laneCanMoveBeforeStart(current)) {
4443
- 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 });
4444
- }
4445
- return updateLane(hqRoot, laneId, current.version, "command", (live) => ({
4446
- ...live,
4447
- host: target,
4448
- account,
4449
- queued_reason: undefined,
4450
- admission_reserved_at: undefined,
4451
- }));
4452
- }, ctx.sleep);
4453
- if (target.kind !== "outpost")
4454
- return moved;
4455
- try {
4456
- return await dispatchCreatedOutpostLane(ctx, {
4457
- hqRoot,
4458
- lane: moved,
4459
- outpostId: target.id,
4460
- briefText: briefText ?? "",
4461
- timeout,
4462
- preserveQueuedOnFailure: true,
4463
- });
4464
- }
4465
- catch (err) {
4466
- const dispatchDetails = isLanesError(err) ? err.details : {};
4467
- const commandId = typeof dispatchDetails.command_id === "string" &&
4468
- dispatchDetails.command_id.trim() !== ""
4469
- ? dispatchDetails.command_id
4470
- : undefined;
4471
- const definitelyNotSubmitted = commandId === undefined &&
4472
- dispatchDetails.outcome_unknown !== true &&
4473
- dispatchDetails.done !== false;
4474
- const knownNoPersistence = (isLanesError(err) && err.error === "outpost_self_target") ||
4475
- (dispatchDetails.done === true && dispatchDetails.pickup_persisted === false);
4476
- if (!definitelyNotSubmitted && !knownNoPersistence) {
4477
- const checkCommand = `hq lanes show ${laneId}`;
4478
- const message = `Failed to dispatch lane ${laneId} to Outpost ${target.id}; outcome is unknown and the lane remains assigned to Outpost ${target.id}. ` +
4479
- `Check the target copy by running \`${checkCommand}\` on Outpost ${target.id}. ` +
4480
- dispatchErrorMessage(err);
4481
- throw new LanesError("lane_move_dispatch_failed", message, {
4482
- lane_id: laneId,
4483
- message,
4484
- source_host: before.host,
4485
- target_host: target,
4486
- source_restored: false,
4487
- outcome: "unknown",
4488
- ...(commandId !== undefined ? { command_id: commandId } : {}),
4489
- check_command: checkCommand,
4490
- ...(dispatchDetails.owner_error !== undefined
4491
- ? { owner_error: dispatchDetails.owner_error }
4492
- : {}),
4493
- });
4494
- }
4495
- let restored = false;
4496
- let rollbackError;
4497
- try {
4498
- restored = await withLaneAdmissionLock(hqRoot, before.provider, async () => {
4499
- const current = readLane(hqRoot, laneId);
4500
- if (current.version !== moved.version ||
4501
- current.state !== "queued" ||
4502
- current.host.kind !== target.kind ||
4503
- current.host.id !== target.id ||
4504
- current.account !== account) {
4505
- return false;
4506
- }
4507
- updateLane(hqRoot, laneId, current.version, "command", (live) => ({
4508
- ...live,
4509
- host: before.host,
4510
- account: before.account,
4511
- queued_reason: before.queued_reason,
4512
- admission_reserved_at: before.admission_reserved_at,
4513
- }));
4514
- return true;
4515
- }, ctx.sleep);
4516
- }
4517
- catch (restoreErr) {
4518
- rollbackError = restoreErr;
4519
- }
4520
- const dispatchMessage = dispatchErrorMessage(err);
4521
- const outcome = restored
4522
- ? "the lane stayed queued on its source"
4523
- : rollbackError === undefined
4524
- ? "the lane changed after the move and was not overwritten"
4525
- : `source restoration failed: ${dispatchErrorMessage(rollbackError)}`;
4526
- const message = `Failed to dispatch lane ${laneId} to Outpost ${target.id}; ${outcome}: ${dispatchMessage}`;
4527
- throw new LanesError("lane_move_dispatch_failed", message, {
4528
- lane_id: laneId,
4529
- message,
4530
- source_host: before.host,
4531
- target_host: target,
4532
- source_restored: restored,
4533
- ...(dispatchDetails.owner_error !== undefined
4534
- ? { owner_error: dispatchDetails.owner_error }
4535
- : {}),
4536
- ...(rollbackError !== undefined
4537
- ? { rollback_error: dispatchErrorMessage(rollbackError) }
4538
- : {}),
4539
- });
4540
- }
4541
- }
4542
- async function dispatchRemoteLaneMove(ctx, source, target, account) {
4543
- if (source.host.kind !== "outpost") {
4544
- throw new LanesError("lane_move_wrong_host", `Lane ${source.lane_id} is not Outpost-hosted`, {
4545
- lane_id: source.lane_id,
4546
- host: source.host,
4547
- });
4548
- }
4549
- let dispatched;
4550
- try {
4551
- dispatched = await ctx.dispatchOutpostExec({
4552
- outpostId: source.host.id,
4553
- command: buildOutpostMoveTargetCommand({
4554
- laneId: source.lane_id,
4555
- host: target,
4556
- account,
4557
- }),
4558
- timeoutSeconds: 120,
4559
- waitMs: 120_000,
4560
- });
4561
- }
4562
- catch (err) {
4563
- 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 });
4564
- }
4565
- const ownerError = ownerErrorFromOutput(dispatched.stdout);
4566
- if (ownerError) {
4567
- throw new LanesError("outpost_dispatch_failed", `Outpost ${source.host.id} rejected the lane move: ${ownerError.message}`, {
4568
- lane_id: source.lane_id,
4569
- source_outpost: source.host.id,
4570
- command_id: dispatched.commandId,
4571
- status: dispatched.status,
4572
- done: dispatched.done ?? false,
4573
- exit_code: dispatched.exitCode,
4574
- owner_error: ownerError,
4575
- });
4576
- }
4577
- if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
4578
- throw new LanesError("lane_move_dispatch_failed", `Outpost ${source.host.id} did not confirm the lane move`, {
4579
- lane_id: source.lane_id,
4580
- source_outpost: source.host.id,
4581
- command_id: dispatched.commandId,
4582
- status: dispatched.status,
4583
- done: dispatched.done ?? false,
4584
- exit_code: dispatched.exitCode,
4585
- });
4586
- }
4587
- const remote = parseRemoteJson(dispatched.stdout, "Outpost lane move");
4588
- const remoteHost = remote?.host;
4589
- if (remote?.ok !== true ||
4590
- remote.action !== "move-target" ||
4591
- remote.lane_id !== source.lane_id ||
4592
- remote.state !== "queued" ||
4593
- remote.account !== account ||
4594
- remoteHost === null || typeof remoteHost !== "object" || Array.isArray(remoteHost) ||
4595
- remoteHost.kind !== target.kind ||
4596
- remoteHost.id !== target.id) {
4597
- throw new LanesError("lane_move_unconfirmed", `Outpost ${source.host.id} returned an invalid lane move result`, {
4598
- lane_id: source.lane_id,
4599
- source_outpost: source.host.id,
4600
- target,
4601
- });
4602
- }
4603
- return {
4604
- ...source,
4605
- state: "queued",
4606
- host: target,
4607
- account,
4608
- };
4609
- }
4610
- async function runMoveTarget(ctx, laneId, opts) {
4611
- const kind = opts.hostKind?.trim();
4612
- const id = opts.hostId?.trim();
4613
- const account = opts.account?.trim();
4614
- if ((kind !== "local" && kind !== "outpost") || !id || !account) {
4615
- throw new LanesError("lane_move_target_invalid", "Move target requires a valid host kind, host id, and account", {
4616
- host_kind: kind ?? null,
4617
- host_id: id ?? null,
4618
- account: account ?? null,
4619
- });
4620
- }
4621
- const moved = await commitLaneMove(ctx, laneId, { kind, id }, account);
4622
- printJson({
4623
- ok: true,
4624
- action: "move-target",
4625
- lane_id: moved.lane_id,
4626
- state: moved.state,
4627
- host: moved.host,
4628
- account: moved.account ?? null,
4629
- });
4630
- }
4631
4299
  async function refreshOutpostLaneCopiesForShow(ctx, lane, localLanes) {
4632
4300
  if (lane.host.kind !== "outpost" || laneRunsHere(lane, reconcileHostFromCtx(ctx))) {
4633
4301
  return lane;
@@ -4773,25 +4441,78 @@ async function runShow(ctx, laneId) {
4773
4441
  : null,
4774
4442
  });
4775
4443
  }
4776
- 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) {
4777
4455
  const sessionId = sessionIdFromEnv(ctx.env);
4778
4456
  const callerLaneId = ctx.env.HQ_LANE_ID?.trim() || undefined;
4779
4457
  if (seniorRef.kind === "lane") {
4780
- 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}` };
4781
4474
  }
4782
- if (!sessionId || callerLaneId !== undefined)
4783
- return false;
4784
- if (seniorRef.kind === "session")
4785
- return sessionId === seniorRef.id;
4786
4475
  if (seniorRef.kind !== "user")
4787
- return false;
4476
+ return { authorized: false, reason: "senior is not a user" };
4788
4477
  const loaded = loadSessionMeta(ctx.hqRoot(), sessionId, ctx);
4789
- if (loaded.status !== "ok")
4790
- return false;
4791
- const senior = loaded.doc.senior;
4792
- 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" ||
4793
4493
  (senior !== null && typeof senior === "object" && !Array.isArray(senior) &&
4794
- 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)}`;
4795
4516
  }
4796
4517
  function laneGrantTargetHostedHere(ctx, lane) {
4797
4518
  if (lane.host.kind === "local") {
@@ -4799,14 +4520,17 @@ function laneGrantTargetHostedHere(ctx, lane) {
4799
4520
  }
4800
4521
  return lane.host.kind === "outpost" && laneIsHostedHere(ctx, lane);
4801
4522
  }
4802
- function forceAdmissionAuthorized(ctx, lane) {
4803
- return callerMatchesSenior(ctx, lane.senior);
4523
+ function forceAdmissionAuthorization(ctx, lane) {
4524
+ return callerSeniorAuthorization(ctx, lane.senior);
4804
4525
  }
4805
4526
  async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContext) {
4806
4527
  const hqRoot = ctx.hqRoot();
4807
4528
  const initial = readLane(hqRoot, laneId);
4808
- if (authorization === "senior" && !callerMatchesSenior(ctx, initial.senior)) {
4809
- 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 });
4810
4534
  }
4811
4535
  if (authorization === "ssm-owner-dispatch" &&
4812
4536
  !laneGrantTargetHostedHere(ctx, initial)) {
@@ -4824,8 +4548,11 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
4824
4548
  ];
4825
4549
  const updated = ctx.withGrantHireLocks(hqRoot, lockedLaneIds, () => {
4826
4550
  const current = readLane(hqRoot, laneId);
4827
- if (authorization === "senior" && !callerMatchesSenior(ctx, current.senior)) {
4828
- 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 });
4829
4556
  }
4830
4557
  if (authorization === "ssm-owner-dispatch" &&
4831
4558
  !laneGrantTargetHostedHere(ctx, current)) {
@@ -4938,8 +4665,9 @@ async function runKeepAlive(ctx, laneId, opts) {
4938
4665
  });
4939
4666
  }
4940
4667
  function assertKeepAliveChangeAllowed(ctx, lane, laneId) {
4941
- if (!callerMatchesSenior(ctx, lane.senior)) {
4942
- 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 });
4943
4671
  }
4944
4672
  if (!laneRunsHere(lane, reconcileHostFromCtx(ctx))) {
4945
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 });
@@ -4954,10 +4682,14 @@ async function runGrant(ctx, laneId, opts) {
4954
4682
  }
4955
4683
  const hireGrant = parseHireGrant(opts.hire);
4956
4684
  const hqRoot = ctx.hqRoot();
4957
- const target = await resolveCommandLaneTarget(ctx, laneId, { outpostHint: true });
4685
+ const target = await resolveCommandLaneTarget(ctx, laneId, {
4686
+ outpostHint: true,
4687
+ outpostId: opts.outpost,
4688
+ });
4958
4689
  const initial = target.lane;
4959
- if (!callerMatchesSenior(ctx, initial.senior)) {
4960
- 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 });
4961
4693
  }
4962
4694
  if (opts.outpost !== undefined) {
4963
4695
  const requestedOutpost = opts.outpost.trim();
@@ -5012,8 +4744,9 @@ async function runApprove(ctx, laneId, opts) {
5012
4744
  if (!allowed.includes(checkpoint)) {
5013
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 });
5014
4746
  }
5015
- if (!callerMatchesSenior(ctx, initial.senior)) {
5016
- 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 });
5017
4750
  }
5018
4751
  if (remoteOutpost) {
5019
4752
  const approvalContext = createOutpostRegistryWorkerApprovalContext({
@@ -5039,8 +4772,9 @@ async function runApprove(ctx, laneId, opts) {
5039
4772
  const actor = resumeProfileRepinActor(ctx);
5040
4773
  const updated = retryUpdateLane(hqRoot, laneId, "command", (current) => {
5041
4774
  assertLaneRunsHereForOperatorMutation(ctx, current, "approve");
5042
- if (!callerMatchesSenior(ctx, current.senior)) {
5043
- 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 });
5044
4778
  }
5045
4779
  if (JSON.stringify(current.registry_worker) !== JSON.stringify(initial.registry_worker) ||
5046
4780
  !current.registry_worker ||
@@ -5206,8 +4940,9 @@ async function runAdmit(ctx, laneId, opts) {
5206
4940
  }
5207
4941
  let blockedBy;
5208
4942
  if (opts.force) {
5209
- if (!forceAdmissionAuthorized(ctx, lane)) {
5210
- 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 });
5211
4946
  }
5212
4947
  const config = loadLanesConfig(hqRoot);
5213
4948
  const lanes = loadAllLanes(hqRoot).filter((row) => row.lane_id !== laneId && laneRunsHere(row, host));
@@ -5335,7 +5070,7 @@ function applyResumeProfileAndEffort(current, next, input) {
5335
5070
  ? registryWorkerRecord(profile.registryWorker)
5336
5071
  : undefined;
5337
5072
  if (input.profileRepin !== undefined &&
5338
- current.host.kind === "outpost" &&
5073
+ (current.host.kind === "outpost" || input.profileRepin.kind === "ssm-owner-dispatch") &&
5339
5074
  current.worker_profile_hash !== profile.contentHash) {
5340
5075
  const entry = {
5341
5076
  old_hash: current.worker_profile_hash ?? null,
@@ -7074,6 +6809,19 @@ export function registerLanesCommand(parent, deps = {}) {
7074
6809
  readOutpostCapacity,
7075
6810
  };
7076
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
+ });
7077
6825
  const lanes = parent
7078
6826
  .command("lanes", { hidden: true })
7079
6827
  .description("Create, supervise, and steer worker lanes; routed decision queue");
@@ -7242,7 +6990,7 @@ export function registerLanesCommand(parent, deps = {}) {
7242
6990
  .argument("<lane-id>", "Lane id")
7243
6991
  .requiredOption("--placement <mode>", "local | auto | outpost:<id>")
7244
6992
  .action(wrap(async (laneId, opts) => {
7245
- await runMove(ctx, laneId, opts);
6993
+ await moveHandlers.runMove(ctx, laneId, opts);
7246
6994
  }));
7247
6995
  lanes
7248
6996
  .command("move-target", { hidden: true })
@@ -7251,7 +6999,7 @@ export function registerLanesCommand(parent, deps = {}) {
7251
6999
  .requiredOption("--host-id <id>", "Target host id")
7252
7000
  .requiredOption("--account <id>", "Provider account id")
7253
7001
  .action(wrap(async (laneId, opts) => {
7254
- await runMoveTarget(ctx, laneId, opts);
7002
+ await moveHandlers.runMoveTarget(ctx, laneId, opts);
7255
7003
  }));
7256
7004
  lanes
7257
7005
  .command("balance-move-target", { hidden: true })