@indigoai-us/hq-cli 5.273.0 → 5.274.1

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 (55) hide show
  1. package/CHANGELOG.md +44 -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-message.js +40 -7
  17. package/dist/commands/lanes-move.d.ts +12 -0
  18. package/dist/commands/lanes-move.js +342 -0
  19. package/dist/commands/lanes-resume.js +3 -1
  20. package/dist/commands/lanes-shared.d.ts +40 -1
  21. package/dist/commands/lanes-shared.js +64 -0
  22. package/dist/commands/lanes.d.ts +1 -1
  23. package/dist/commands/lanes.js +211 -460
  24. package/dist/commands/report.js +2 -14
  25. package/dist/fast-core.js +1 -1
  26. package/dist/{commands/scaffold-fast.d.ts → fast-scaffold.d.ts} +1 -1
  27. package/dist/{commands/scaffold-fast.js → fast-scaffold.js} +1 -1
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +2 -2
  30. package/dist/lib/checkpoint-ontology/apply.d.ts +8 -0
  31. package/dist/lib/checkpoint-ontology/apply.js +33 -0
  32. package/dist/lib/checkpoint-ontology/plan.d.ts +15 -0
  33. package/dist/lib/checkpoint-ontology/plan.js +15 -0
  34. package/dist/lib/deploy-api.d.ts +6 -0
  35. package/dist/lib/deploy-api.js +21 -0
  36. package/dist/lib/doctor/fix/apply.d.ts +17 -2
  37. package/dist/lib/doctor/fix/apply.js +111 -57
  38. package/dist/lib/doctor/fix/json-output.d.ts +9 -1
  39. package/dist/lib/doctor/fix/json-output.js +11 -0
  40. package/dist/lib/first-sync-next-step.d.ts +37 -0
  41. package/dist/lib/first-sync-next-step.js +189 -0
  42. package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -1
  43. package/dist/lib/lanes/hire-grant-dispatch.js +37 -2
  44. package/dist/lib/lanes/keep-alive.js +2 -2
  45. package/dist/main.js +4 -1
  46. package/dist/utils/cli-telemetry.d.ts +7 -1
  47. package/dist/utils/cli-telemetry.js +84 -8
  48. package/dist/utils/contribution-table.d.ts +1 -1
  49. package/dist/utils/scaffold-fast-import-guard.d.ts +3 -0
  50. package/dist/utils/scaffold-fast-import-guard.js +28 -0
  51. package/dist/utils/scaffold-packlist-guard.d.ts +7 -0
  52. package/dist/utils/scaffold-packlist-guard.js +23 -0
  53. package/dist/utils/scaffold-sibling-references.d.ts +21 -0
  54. package/dist/utils/scaffold-sibling-references.js +156 -0
  55. 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 { callerMatchesSessionSenior, 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,11 +825,10 @@ 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) ||
862
- lane.senior.kind !== "lane" ||
828
+ !laneGrantTargetHostedHere(ctx, lane) ||
829
+ (lane.senior.kind !== "lane" && lane.senior.kind !== "session") ||
863
830
  !sameHireGrantSenior(lane.senior, grantContext.expected_senior))) {
864
- 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 });
831
+ throw new LanesError("hire_grant_senior_changed", `Lane ${lane.lane_id}'s senior changed after the primary authorized its Outpost dispatch`, { lane_id: lane.lane_id, expected_senior: grantContext.expected_senior, current_senior: lane.senior });
865
832
  }
866
833
  if (!isTerminalLaneState(lane.state) ||
867
834
  lane.senior.kind !== "lane" ||
@@ -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
- else if ((action === "resume" || action === "message") &&
3493
- lane.senior.kind === "lane") {
3494
- grantContext = createOutpostHireGrantContext(lane, loadAllLanes(ctx.hqRoot()), action === "message" ? options.supersededMessageCallerId : undefined);
3489
+ else if ((action === "resume" && lane.senior.kind === "lane") ||
3490
+ (action === "message" && (lane.senior.kind === "lane" || lane.senior.kind === "session"))) {
3491
+ const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId));
3492
+ grantContext = createOutpostHireGrantContext(lane, hierarchy, action === "message" ? options.supersededMessageCallerId : undefined, action === "message" ? options.supersededMessageSessionId : 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,81 @@ 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
+ const seniorSessionId = seniorRef.id;
4472
+ if (!seniorSessionId)
4473
+ return { authorized: false, reason: "session senior has no id" };
4474
+ return callerMatchesSessionSenior(ctx, seniorSessionId)
4475
+ ? { authorized: true }
4476
+ : { authorized: false, reason: `session id ${sessionId} is not senior session ${seniorSessionId}` };
4781
4477
  }
4782
- if (!sessionId || callerLaneId !== undefined)
4783
- return false;
4784
- if (seniorRef.kind === "session")
4785
- return sessionId === seniorRef.id;
4786
4478
  if (seniorRef.kind !== "user")
4787
- return false;
4479
+ return { authorized: false, reason: "senior is not a user" };
4788
4480
  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" ||
4481
+ if (loaded.status === "missing") {
4482
+ return {
4483
+ authorized: false,
4484
+ reason: "session meta missing",
4485
+ repair: "if this is the owner's session, run bash core/scripts/hq-session.sh set senior user",
4486
+ };
4487
+ }
4488
+ if (loaded.status === "unreadable") {
4489
+ return { authorized: false, reason: "session meta unreadable" };
4490
+ }
4491
+ if (loaded.status === "malformed") {
4492
+ return { authorized: false, reason: "session meta invalid" };
4493
+ }
4494
+ const senior = sessionSeniorValue(loaded.doc);
4495
+ if (senior === "user" ||
4793
4496
  (senior !== null && typeof senior === "object" && !Array.isArray(senior) &&
4794
- senior.kind === "user");
4497
+ senior.kind === "user")) {
4498
+ return { authorized: true };
4499
+ }
4500
+ return {
4501
+ authorized: false,
4502
+ reason: `session senior is ${formatSessionSeniorValue(senior)}`,
4503
+ };
4504
+ }
4505
+ function callerSeniorReason(authorization) {
4506
+ if (authorization.authorized)
4507
+ return "";
4508
+ return authorization.repair
4509
+ ? `${authorization.reason}; ${authorization.repair}`
4510
+ : authorization.reason;
4511
+ }
4512
+ function callerMatchesSenior(ctx, seniorRef) {
4513
+ return callerSeniorAuthorization(ctx, seniorRef).authorized;
4514
+ }
4515
+ function appendCallerSeniorReason(message, authorization) {
4516
+ return authorization.authorized
4517
+ ? message
4518
+ : `${message}; authorization refused: ${callerSeniorReason(authorization)}`;
4795
4519
  }
4796
4520
  function laneGrantTargetHostedHere(ctx, lane) {
4797
4521
  if (lane.host.kind === "local") {
@@ -4799,14 +4523,17 @@ function laneGrantTargetHostedHere(ctx, lane) {
4799
4523
  }
4800
4524
  return lane.host.kind === "outpost" && laneIsHostedHere(ctx, lane);
4801
4525
  }
4802
- function forceAdmissionAuthorized(ctx, lane) {
4803
- return callerMatchesSenior(ctx, lane.senior);
4526
+ function forceAdmissionAuthorization(ctx, lane) {
4527
+ return callerSeniorAuthorization(ctx, lane.senior);
4804
4528
  }
4805
4529
  async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContext) {
4806
4530
  const hqRoot = ctx.hqRoot();
4807
4531
  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 });
4532
+ const initialCallerAuthorization = authorization === "senior"
4533
+ ? callerSeniorAuthorization(ctx, initial.senior)
4534
+ : { authorized: true };
4535
+ if (!initialCallerAuthorization.authorized) {
4536
+ 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
4537
  }
4811
4538
  if (authorization === "ssm-owner-dispatch" &&
4812
4539
  !laneGrantTargetHostedHere(ctx, initial)) {
@@ -4824,8 +4551,11 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
4824
4551
  ];
4825
4552
  const updated = ctx.withGrantHireLocks(hqRoot, lockedLaneIds, () => {
4826
4553
  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 });
4554
+ const currentCallerAuthorization = authorization === "senior"
4555
+ ? callerSeniorAuthorization(ctx, current.senior)
4556
+ : { authorized: true };
4557
+ if (!currentCallerAuthorization.authorized) {
4558
+ 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
4559
  }
4830
4560
  if (authorization === "ssm-owner-dispatch" &&
4831
4561
  !laneGrantTargetHostedHere(ctx, current)) {
@@ -4938,8 +4668,9 @@ async function runKeepAlive(ctx, laneId, opts) {
4938
4668
  });
4939
4669
  }
4940
4670
  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 });
4671
+ const callerAuthorization = callerSeniorAuthorization(ctx, lane.senior);
4672
+ if (!callerAuthorization.authorized) {
4673
+ 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
4674
  }
4944
4675
  if (!laneRunsHere(lane, reconcileHostFromCtx(ctx))) {
4945
4676
  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 +4685,14 @@ async function runGrant(ctx, laneId, opts) {
4954
4685
  }
4955
4686
  const hireGrant = parseHireGrant(opts.hire);
4956
4687
  const hqRoot = ctx.hqRoot();
4957
- const target = await resolveCommandLaneTarget(ctx, laneId, { outpostHint: true });
4688
+ const target = await resolveCommandLaneTarget(ctx, laneId, {
4689
+ outpostHint: true,
4690
+ outpostId: opts.outpost,
4691
+ });
4958
4692
  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 });
4693
+ const callerAuthorization = callerSeniorAuthorization(ctx, initial.senior);
4694
+ if (!callerAuthorization.authorized) {
4695
+ 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
4696
  }
4962
4697
  if (opts.outpost !== undefined) {
4963
4698
  const requestedOutpost = opts.outpost.trim();
@@ -5012,8 +4747,9 @@ async function runApprove(ctx, laneId, opts) {
5012
4747
  if (!allowed.includes(checkpoint)) {
5013
4748
  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
4749
  }
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 });
4750
+ const callerAuthorization = callerSeniorAuthorization(ctx, initial.senior);
4751
+ if (!callerAuthorization.authorized) {
4752
+ 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
4753
  }
5018
4754
  if (remoteOutpost) {
5019
4755
  const approvalContext = createOutpostRegistryWorkerApprovalContext({
@@ -5039,8 +4775,9 @@ async function runApprove(ctx, laneId, opts) {
5039
4775
  const actor = resumeProfileRepinActor(ctx);
5040
4776
  const updated = retryUpdateLane(hqRoot, laneId, "command", (current) => {
5041
4777
  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 });
4778
+ const currentCallerAuthorization = callerSeniorAuthorization(ctx, current.senior);
4779
+ if (!currentCallerAuthorization.authorized) {
4780
+ 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
4781
  }
5045
4782
  if (JSON.stringify(current.registry_worker) !== JSON.stringify(initial.registry_worker) ||
5046
4783
  !current.registry_worker ||
@@ -5206,8 +4943,9 @@ async function runAdmit(ctx, laneId, opts) {
5206
4943
  }
5207
4944
  let blockedBy;
5208
4945
  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 });
4946
+ const callerAuthorization = forceAdmissionAuthorization(ctx, lane);
4947
+ if (!callerAuthorization.authorized) {
4948
+ 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
4949
  }
5212
4950
  const config = loadLanesConfig(hqRoot);
5213
4951
  const lanes = loadAllLanes(hqRoot).filter((row) => row.lane_id !== laneId && laneRunsHere(row, host));
@@ -5335,7 +5073,7 @@ function applyResumeProfileAndEffort(current, next, input) {
5335
5073
  ? registryWorkerRecord(profile.registryWorker)
5336
5074
  : undefined;
5337
5075
  if (input.profileRepin !== undefined &&
5338
- current.host.kind === "outpost" &&
5076
+ (current.host.kind === "outpost" || input.profileRepin.kind === "ssm-owner-dispatch") &&
5339
5077
  current.worker_profile_hash !== profile.contentHash) {
5340
5078
  const entry = {
5341
5079
  old_hash: current.worker_profile_hash ?? null,
@@ -7074,6 +6812,19 @@ export function registerLanesCommand(parent, deps = {}) {
7074
6812
  readOutpostCapacity,
7075
6813
  };
7076
6814
  const watchHandlers = createLaneWatchHandlers(watchHandlerDeps);
6815
+ const moveHandlers = createLaneMoveHandlers({
6816
+ assertKnownOutpost,
6817
+ assertLaneRunsHereForOperatorMutation,
6818
+ dispatchCreatedOutpostLane,
6819
+ loadHostCapacitySnapshots,
6820
+ ownerErrorFromOutput,
6821
+ parseRemoteJson,
6822
+ printJson,
6823
+ readJsonFile,
6824
+ reconcileHostFromCtx,
6825
+ runDir,
6826
+ timeoutPath,
6827
+ });
7077
6828
  const lanes = parent
7078
6829
  .command("lanes", { hidden: true })
7079
6830
  .description("Create, supervise, and steer worker lanes; routed decision queue");
@@ -7242,7 +6993,7 @@ export function registerLanesCommand(parent, deps = {}) {
7242
6993
  .argument("<lane-id>", "Lane id")
7243
6994
  .requiredOption("--placement <mode>", "local | auto | outpost:<id>")
7244
6995
  .action(wrap(async (laneId, opts) => {
7245
- await runMove(ctx, laneId, opts);
6996
+ await moveHandlers.runMove(ctx, laneId, opts);
7246
6997
  }));
7247
6998
  lanes
7248
6999
  .command("move-target", { hidden: true })
@@ -7251,7 +7002,7 @@ export function registerLanesCommand(parent, deps = {}) {
7251
7002
  .requiredOption("--host-id <id>", "Target host id")
7252
7003
  .requiredOption("--account <id>", "Provider account id")
7253
7004
  .action(wrap(async (laneId, opts) => {
7254
- await runMoveTarget(ctx, laneId, opts);
7005
+ await moveHandlers.runMoveTarget(ctx, laneId, opts);
7255
7006
  }));
7256
7007
  lanes
7257
7008
  .command("balance-move-target", { hidden: true })