@indigoai-us/hq-cli 5.229.0 → 5.229.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.229.1] — 2026-09-27
6
+
7
+ ### Fixed
8
+
9
+ - An authorized `hq lanes resume --worker` on an Outpost re-pins a changed
10
+ worker profile before spawn, preserves the provider session, records the
11
+ hash change in lane history, and syncs the resulting lane record to primary.
12
+
5
13
  ## [5.229.0] — 2026-09-27
6
14
 
7
15
  ### Added
@@ -13,6 +21,8 @@
13
21
  new entity, and to skip glossary or translation table rows, paths and names
14
22
  built from tags or slugs.
15
23
 
24
+ ## [5.228.4] — 2026-09-27
25
+
16
26
  ### Fixed
17
27
 
18
28
  - Account history now distinguishes automatic moves caused by a quota hold, an
@@ -25,6 +25,7 @@ import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
25
25
  import { resolveProcessFlagGate } from "../lib/flag-registry.js";
26
26
  import { defaultTrackerActor, LANES_TRACKER_FLAG, LANES_TRACKER_MESH_FLAG, registerLaneTrackerCommands, } from "./lanes-tracker.js";
27
27
  import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
28
+ import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } from "../lib/lanes/outpost-profile-repin.js";
28
29
  import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
29
30
  export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
30
31
  import { buildOutpostCapacityCommand, buildOutpostBalanceMoveCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostQuestionAnswerCommand, buildOutpostQuestionListCommand, buildOutpostQuestionShowCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
@@ -3564,6 +3565,10 @@ function parseRemoteJson(stdout, surface) {
3564
3565
  }
3565
3566
  return null;
3566
3567
  }
3568
+ function outpostRejectedProfileRepinOption(result) {
3569
+ return result.exitCode !== 0 &&
3570
+ /unknown option\s+(?:['"])?--profile-repin-context(?:['"])?(?:\s|$)/i.test(`${result.stderr}\n${result.stdout}`);
3571
+ }
3567
3572
  function verifyOutpostProfileContent(hqRoot, lane) {
3568
3573
  if (lane.host.kind !== "outpost" || lane.worker_profile === undefined)
3569
3574
  return;
@@ -3579,6 +3584,89 @@ function verifyOutpostProfileContent(hqRoot, lane) {
3579
3584
  });
3580
3585
  }
3581
3586
  }
3587
+ function resumeProfileRepinActor(ctx) {
3588
+ const laneId = ctx.env.HQ_LANE_ID?.trim();
3589
+ if (laneId)
3590
+ return `lane:${laneId}`;
3591
+ const sessionId = sessionIdFromEnv(ctx.env);
3592
+ return sessionId ? `session:${sessionId}` : "ssm-owner-dispatch";
3593
+ }
3594
+ function workerProfileHashMismatch(lane, profile) {
3595
+ 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`, {
3596
+ lane_id: lane.lane_id,
3597
+ worker: profile.id,
3598
+ outpost: lane.host.kind === "outpost" ? lane.host.id : null,
3599
+ expected_hash: lane.worker_profile_hash ?? null,
3600
+ actual_hash: profile.profile.contentHash,
3601
+ });
3602
+ }
3603
+ function authorizeResumeProfileRepin(ctx, lane, profile, encodedContext) {
3604
+ const ssmOwnerDispatch = ctx.isSsmOwnerDispatch();
3605
+ if (encodedContext !== undefined && !ssmOwnerDispatch) {
3606
+ throw profile === undefined
3607
+ ? new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is only valid on an Outpost owner dispatch", { lane_id: lane.lane_id })
3608
+ : workerProfileHashMismatch(lane, profile);
3609
+ }
3610
+ let context;
3611
+ if (encodedContext !== undefined) {
3612
+ try {
3613
+ context = parseOutpostProfileRepinContext(encodedContext, lane.lane_id);
3614
+ }
3615
+ catch {
3616
+ throw profile === undefined
3617
+ ? new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is invalid", { lane_id: lane.lane_id })
3618
+ : workerProfileHashMismatch(lane, profile);
3619
+ }
3620
+ if (lane.host.kind !== "outpost" ||
3621
+ !sameHireGrantSenior(lane.senior, context.expected_senior)) {
3622
+ throw profile === undefined
3623
+ ? new LanesError("worker_profile_hash_mismatch", "Lane senior changed after profile re-pin authorization", { lane_id: lane.lane_id })
3624
+ : workerProfileHashMismatch(lane, profile);
3625
+ }
3626
+ }
3627
+ if (profile === undefined ||
3628
+ lane.host.kind !== "outpost" ||
3629
+ lane.worker_profile_hash === profile.profile.contentHash) {
3630
+ return undefined;
3631
+ }
3632
+ if (ssmOwnerDispatch) {
3633
+ if (context === undefined)
3634
+ throw workerProfileHashMismatch(lane, profile);
3635
+ return {
3636
+ kind: "ssm-owner-dispatch",
3637
+ expectedSenior: context.expected_senior,
3638
+ actor: context.actor,
3639
+ reason: context.reason,
3640
+ newHash: profile.profile.contentHash,
3641
+ };
3642
+ }
3643
+ if (context !== undefined || !callerMatchesSenior(ctx, lane.senior)) {
3644
+ throw workerProfileHashMismatch(lane, profile);
3645
+ }
3646
+ return {
3647
+ kind: "senior",
3648
+ expectedSenior: lane.senior,
3649
+ actor: resumeProfileRepinActor(ctx),
3650
+ reason: "resume --worker",
3651
+ newHash: profile.profile.contentHash,
3652
+ };
3653
+ }
3654
+ function assertResumeProfileRepinStillAuthorized(ctx, current, authorization) {
3655
+ const authorized = current.host.kind === "outpost" &&
3656
+ sameHireGrantSenior(current.senior, authorization.expectedSenior) &&
3657
+ (authorization.kind === "ssm-owner-dispatch"
3658
+ ? ctx.isSsmOwnerDispatch()
3659
+ : callerMatchesSenior(ctx, current.senior));
3660
+ if (!authorized) {
3661
+ throw new LanesError("worker_profile_hash_mismatch", `Lane ${current.lane_id}'s senior changed before its Outpost profile could be re-pinned`, {
3662
+ lane_id: current.lane_id,
3663
+ expected_senior: authorization.expectedSenior,
3664
+ current_senior: current.senior,
3665
+ expected_hash: current.worker_profile_hash ?? null,
3666
+ actual_hash: authorization.newHash,
3667
+ });
3668
+ }
3669
+ }
3582
3670
  function laneIsHostedHere(ctx, lane) {
3583
3671
  if (lane.host.kind !== "outpost")
3584
3672
  return true;
@@ -3603,6 +3691,15 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3603
3691
  throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch to`, { lane_id: lane.lane_id });
3604
3692
  }
3605
3693
  const outpostId = lane.host.id;
3694
+ const profileRepinContext = action === "resume" &&
3695
+ options.worker !== undefined &&
3696
+ callerMatchesSenior(ctx, lane.senior)
3697
+ ? createOutpostProfileRepinContext({
3698
+ laneId: lane.lane_id,
3699
+ expectedSenior: lane.senior,
3700
+ actor: resumeProfileRepinActor(ctx),
3701
+ })
3702
+ : undefined;
3606
3703
  let grantContext;
3607
3704
  let grantHierarchy;
3608
3705
  if (action === "grant") {
@@ -3618,7 +3715,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3618
3715
  }
3619
3716
  let dispatched;
3620
3717
  try {
3621
- dispatched = await ctx.dispatchOutpostExec({
3718
+ const dispatchCommand = (repinContext) => ctx.dispatchOutpostExec({
3622
3719
  outpostId,
3623
3720
  command: buildOutpostLaneCommand({
3624
3721
  action: action === "grant"
@@ -3642,9 +3739,15 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3642
3739
  senior: options.senior,
3643
3740
  reason: options.reason,
3644
3741
  grantContext,
3742
+ ...(repinContext !== undefined ? { profileRepinContext: repinContext } : {}),
3645
3743
  compact: options.compact,
3646
3744
  }),
3647
3745
  });
3746
+ dispatched = await dispatchCommand(profileRepinContext);
3747
+ if (profileRepinContext !== undefined &&
3748
+ outpostRejectedProfileRepinOption(dispatched)) {
3749
+ dispatched = await dispatchCommand();
3750
+ }
3648
3751
  }
3649
3752
  catch (err) {
3650
3753
  throw new LanesError("outpost_dispatch_failed", `Could not dispatch ${action} for lane ${lane.lane_id} to Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, outpost_id: outpostId });
@@ -3663,6 +3766,14 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3663
3766
  });
3664
3767
  }
3665
3768
  if (ownerError) {
3769
+ if (action === "resume" && ownerError.error === "worker_profile_hash_mismatch") {
3770
+ throw new LanesError(ownerError.error, ownerError.message, {
3771
+ ...ownerError.details,
3772
+ lane_id: lane.lane_id,
3773
+ outpost_id: outpostId,
3774
+ command_id: dispatched.commandId,
3775
+ });
3776
+ }
3666
3777
  throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected the ${action} command: ${ownerError.message}`, {
3667
3778
  lane_id: lane.lane_id,
3668
3779
  outpost_id: outpostId,
@@ -3708,8 +3819,9 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3708
3819
  : action === "reparent"
3709
3820
  ? remote.lane !== null && typeof remote.lane === "object" &&
3710
3821
  Array.isArray(remote.changed_descendant_ids)
3711
- : remote.delivery_status === "worker_spawned" ||
3712
- remote.delivery_status === "queued_on_target_account");
3822
+ : (remote.delivery_status === "worker_spawned" ||
3823
+ remote.delivery_status === "queued_on_target_account") &&
3824
+ (options.worker === undefined || remote.lane !== undefined));
3713
3825
  if (!resultConfirmed) {
3714
3826
  throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without a confirmed ${action} result for lane ${lane.lane_id}`, {
3715
3827
  lane_id: lane.lane_id,
@@ -3730,19 +3842,22 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3730
3842
  });
3731
3843
  return;
3732
3844
  }
3733
- if (action === "reparent") {
3845
+ if (action === "reparent" || (action === "resume" && options.worker !== undefined)) {
3734
3846
  const ownerLane = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, remote.lane, undefined, { allowNonTerminal: true });
3735
3847
  const descendants = [];
3736
- for (const descendantId of changedDescendantIds) {
3737
- const ownerDescendant = await readOutpostLaneSnapshot(ctx, outpostId, descendantId);
3738
- descendants.push(syncLaneRecordFromOwner(ctx.hqRoot(), descendantId, outpostId, ownerDescendant, undefined, { allowNonTerminal: true }));
3848
+ if (action === "reparent") {
3849
+ for (const descendantId of changedDescendantIds) {
3850
+ const ownerDescendant = await readOutpostLaneSnapshot(ctx, outpostId, descendantId);
3851
+ descendants.push(syncLaneRecordFromOwner(ctx.hqRoot(), descendantId, outpostId, ownerDescendant, undefined, { allowNonTerminal: true }));
3852
+ }
3739
3853
  }
3740
3854
  printJson({
3741
3855
  ...remote,
3742
3856
  lane_id: ownerLane.lane_id,
3743
3857
  lane: ownerLane,
3744
- changed_descendant_ids: changedDescendantIds,
3745
- descendants,
3858
+ ...(action === "reparent"
3859
+ ? { changed_descendant_ids: changedDescendantIds, descendants }
3860
+ : {}),
3746
3861
  outpost_id: outpostId,
3747
3862
  outpost_command_id: dispatched.commandId,
3748
3863
  outpost_transport: "outposts_exec",
@@ -5248,6 +5363,18 @@ function applyResumeProfileAndEffort(current, next, input) {
5248
5363
  next.worker_pack = input.profile.pack;
5249
5364
  next.worker_profile = profile.id;
5250
5365
  next.worker_profile_hash = profile.contentHash;
5366
+ if (input.profileRepin !== undefined &&
5367
+ current.host.kind === "outpost" &&
5368
+ current.worker_profile_hash !== profile.contentHash) {
5369
+ const entry = {
5370
+ old_hash: current.worker_profile_hash ?? null,
5371
+ new_hash: profile.contentHash,
5372
+ actor: input.profileRepin.actor,
5373
+ at: input.at ?? new Date().toISOString(),
5374
+ reason: input.profileRepin.reason,
5375
+ };
5376
+ next.worker_profile_repin_history = appendCappedHistory(current.worker_profile_repin_history ?? [], entry);
5377
+ }
5251
5378
  next.worker_profile_ignored = ignoredWorkerProfileFields(current.provider);
5252
5379
  next.worker_policy_delivery = undefined;
5253
5380
  if (profile.meta.keep_alive !== undefined) {
@@ -5308,6 +5435,9 @@ function recordResumeSetup(ctx, lane, input) {
5308
5435
  if (current.version !== lane.version) {
5309
5436
  throw new LanesError("lane_resume_setup_conflict", `Lane ${lane.lane_id} changed while resume settings were prepared; run the resume again`, { lane_id: lane.lane_id, prepared_version: lane.version, version: current.version });
5310
5437
  }
5438
+ if (input.profileRepin !== undefined) {
5439
+ assertResumeProfileRepinStillAuthorized(ctx, current, input.profileRepin);
5440
+ }
5311
5441
  const next = { ...current };
5312
5442
  applyResumeProfileAndEffort(current, next, { ...input, at: ctx.now().toISOString() });
5313
5443
  if (input.cliModel !== undefined) {
@@ -5525,6 +5655,9 @@ async function prepareAccountMove(ctx, lane, input) {
5525
5655
  if (current.version !== lane.version) {
5526
5656
  throw new LanesError("account_move_conflict", `Lane ${laneId} changed while the move was prepared (version ${lane.version} → ${current.version}, state ${current.state}, account ${current.account ?? "(none)"}); run the move again`, { lane_id: laneId, prepared_version: lane.version, version: current.version, state: current.state, account: current.account ?? null });
5527
5657
  }
5658
+ if (input.profileRepin !== undefined) {
5659
+ assertResumeProfileRepinStillAuthorized(ctx, current, input.profileRepin);
5660
+ }
5528
5661
  const next = {
5529
5662
  ...current,
5530
5663
  account: target,
@@ -5549,6 +5682,7 @@ async function prepareAccountMove(ctx, lane, input) {
5549
5682
  };
5550
5683
  applyResumeProfileAndEffort(current, next, {
5551
5684
  ...(input.workerProfile !== undefined ? { profile: input.workerProfile } : {}),
5685
+ ...(input.profileRepin !== undefined ? { profileRepin: input.profileRepin } : {}),
5552
5686
  ...(input.effort !== undefined ? { effort: input.effort } : {}),
5553
5687
  at,
5554
5688
  });
@@ -6410,6 +6544,9 @@ function laneWithRecoveredProviderSession(hqRoot, lane) {
6410
6544
  async function runResume(ctx, laneId, opts) {
6411
6545
  const hqRoot = ctx.hqRoot();
6412
6546
  let lane = readLane(hqRoot, laneId);
6547
+ if (opts.profileRepinContext !== undefined && !ctx.isSsmOwnerDispatch()) {
6548
+ throw new LanesError("worker_profile_hash_mismatch", "Profile re-pin context is only valid on an Outpost owner dispatch", { lane_id: laneId });
6549
+ }
6413
6550
  if (lane.host.kind === "outpost" && !laneIsHostedHere(ctx, lane)) {
6414
6551
  await dispatchOutpostLaneCommand(ctx, lane, "resume", opts);
6415
6552
  return;
@@ -6478,6 +6615,7 @@ async function runResume(ctx, laneId, opts) {
6478
6615
  const resumeProfile = opts.worker !== undefined
6479
6616
  ? resolveResumeProfile(ctx, lane, opts.worker)
6480
6617
  : undefined;
6618
+ const profileRepin = authorizeResumeProfileRepin(ctx, lane, resumeProfile, opts.profileRepinContext);
6481
6619
  const keepAlive = opts.keepAlive === true
6482
6620
  ? true
6483
6621
  : resumeProfile?.profile.meta.keep_alive;
@@ -6502,6 +6640,7 @@ async function runResume(ctx, laneId, opts) {
6502
6640
  account: opts.account,
6503
6641
  cliModel: opts.model?.trim() || undefined,
6504
6642
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
6643
+ ...(profileRepin !== undefined ? { profileRepin } : {}),
6505
6644
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
6506
6645
  pending: {
6507
6646
  trigger: "resume",
@@ -6526,6 +6665,7 @@ async function runResume(ctx, laneId, opts) {
6526
6665
  ? { preservedModel: automaticAccountSelection.preservedModel }
6527
6666
  : {}),
6528
6667
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
6668
+ ...(profileRepin !== undefined ? { profileRepin } : {}),
6529
6669
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
6530
6670
  pending: {
6531
6671
  trigger: "resume",
@@ -6544,6 +6684,7 @@ async function runResume(ctx, laneId, opts) {
6544
6684
  }
6545
6685
  lane = recordResumeSetup(ctx, lane, {
6546
6686
  ...(move === null && resumeProfile !== undefined ? { profile: resumeProfile } : {}),
6687
+ ...(move === null && profileRepin !== undefined ? { profileRepin } : {}),
6547
6688
  ...(move === null && resumeEffort !== undefined ? { effort: resumeEffort } : {}),
6548
6689
  ...(keepAlive !== undefined ? { keepAlive } : {}),
6549
6690
  ...(move === null && keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
@@ -9419,6 +9560,7 @@ export function registerLanesCommand(parent, deps = {}) {
9419
9560
  .option("--model <id>", "Model for the moved lane (with --account only)")
9420
9561
  .option("--effort <level>", "Reasoning effort for the resumed provider turn")
9421
9562
  .option("--worker <profile>", "Attach a personal worker profile for the resumed turn")
9563
+ .addOption(new Option("--profile-repin-context <base64url>").hideHelp())
9422
9564
  .option("--keep-alive", "Automatically resume done lead lanes while their story remains open")
9423
9565
  .option("--mesh-company <slug>", "Attach Work Mesh attribution after verifying company membership")
9424
9566
  .option("--no-compact", "Skip the resume-time compaction check for this resume")
@@ -10,7 +10,7 @@ export type { AdmissionCapReason, AdmissionHost } from "./admission.js";
10
10
  export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
11
11
  export type { HostCapacityAccount, HostCapacityProvider, HostCapacitySnapshot, HostPlacementChoice, } from "./host-capacity.js";
12
12
  export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, LANE_STORY_STATUSES, LANE_STATES, LIVENESS_STATES, QUESTION_BODY_STORES, QUESTION_SCHEMA_VERSION, QUESTION_STATUSES, QUEUED_REASONS, RAISED_VIA, ROUTING_REASONS, SENIOR_KINDS, STORY_STATUSES, TERMINAL_LANE_STATES, isEnvelopeDecision, isHostKind, isLaneProvider, isLaneState, isLivenessState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseLaneEnvelope, parseLaneProvider, parseLaneState, parseLivenessState, parseMajorSchema, parseQuestionStatus, parseSeniorKind, } from "./types.js";
13
- export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneAccountSelection, LaneAccountSelectionConsidered, LaneAccountSelectionHostCount, LaneEnvelope, LaneHost, LaneProvider, LanePlacementPolicy, LaneRecord, LaneStory, LaneStoryStatus, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
13
+ export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneAccountSelection, LaneAccountSelectionConsidered, LaneAccountSelectionHostCount, LaneEnvelope, LaneHost, LaneProvider, LanePlacementPolicy, LaneRecord, LaneStory, LaneStoryStatus, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, WorkerProfileRepinHistoryEntry, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
14
14
  export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory, storyKey, storyStatusForEnvelope, storyWarning, withCurrentStory, withLaneStories, } from "./stories.js";
15
15
  export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerAutoCompactTokens, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
16
16
  export type { ConfigLoadDeps, ConfigWriteDeps } from "./config.js";
@@ -40,6 +40,7 @@ export interface OutpostLaneCommand {
40
40
  keepAlive?: boolean;
41
41
  hire?: string;
42
42
  grantContext?: string;
43
+ profileRepinContext?: string;
43
44
  senior?: string;
44
45
  reason?: string;
45
46
  compact?: boolean;
@@ -150,6 +150,7 @@ export function buildOutpostLaneCommand(input) {
150
150
  ["--worker", input.worker],
151
151
  ["--hire", input.hire],
152
152
  ["--grant-context", input.grantContext],
153
+ ["--profile-repin-context", input.profileRepinContext],
153
154
  ["--senior", input.senior],
154
155
  ["--reason", input.reason],
155
156
  ]) {
@@ -0,0 +1,17 @@
1
+ import { type SeniorRef } from "./types.js";
2
+ declare const PROFILE_REPIN_CONTEXT_SCHEMA: "hq-lane-profile-repin-context.v1";
3
+ export interface OutpostProfileRepinContext {
4
+ schema_version: typeof PROFILE_REPIN_CONTEXT_SCHEMA;
5
+ lane_id: string;
6
+ expected_senior: SeniorRef;
7
+ actor: string;
8
+ reason: "resume --worker";
9
+ }
10
+ export declare function createOutpostProfileRepinContext(input: {
11
+ laneId: string;
12
+ expectedSenior: SeniorRef;
13
+ actor: string;
14
+ }): string;
15
+ export declare function parseOutpostProfileRepinContext(encoded: string, expectedLaneId: string): OutpostProfileRepinContext;
16
+ export {};
17
+ //# sourceMappingURL=outpost-profile-repin.d.ts.map
@@ -0,0 +1,84 @@
1
+ import { LanesError } from "./errors.js";
2
+ import { isSeniorKind } from "./types.js";
3
+ const PROFILE_REPIN_CONTEXT_SCHEMA = "hq-lane-profile-repin-context.v1";
4
+ const MAX_CONTEXT_BYTES = 4 * 1024;
5
+ function contextError(message) {
6
+ return new LanesError("worker_profile_repin_context_invalid", message, {});
7
+ }
8
+ function parseSenior(value) {
9
+ if (value === null || typeof value !== "object" || Array.isArray(value))
10
+ return null;
11
+ const record = value;
12
+ if (Object.keys(record).some((key) => key !== "kind" && key !== "id"))
13
+ return null;
14
+ if (!isSeniorKind(record.kind))
15
+ return null;
16
+ if (record.id !== undefined &&
17
+ (typeof record.id !== "string" || !record.id.trim() || record.id.trim() !== record.id))
18
+ return null;
19
+ if ((record.kind === "session" || record.kind === "lane") && record.id === undefined)
20
+ return null;
21
+ if (record.kind === "user" && record.id !== undefined)
22
+ return null;
23
+ return record.id === undefined
24
+ ? { kind: record.kind }
25
+ : { kind: record.kind, id: record.id };
26
+ }
27
+ export function createOutpostProfileRepinContext(input) {
28
+ const context = {
29
+ schema_version: PROFILE_REPIN_CONTEXT_SCHEMA,
30
+ lane_id: input.laneId,
31
+ expected_senior: input.expectedSenior.id === undefined
32
+ ? { kind: input.expectedSenior.kind }
33
+ : { kind: input.expectedSenior.kind, id: input.expectedSenior.id },
34
+ actor: input.actor,
35
+ reason: "resume --worker",
36
+ };
37
+ const encoded = Buffer.from(JSON.stringify(context), "utf8").toString("base64url");
38
+ if (Buffer.byteLength(encoded, "utf8") > MAX_CONTEXT_BYTES) {
39
+ throw contextError(`Profile re-pin context for lane ${input.laneId} is too large`);
40
+ }
41
+ return encoded;
42
+ }
43
+ export function parseOutpostProfileRepinContext(encoded, expectedLaneId) {
44
+ if (encoded.length === 0 ||
45
+ encoded.length > MAX_CONTEXT_BYTES ||
46
+ !/^[A-Za-z0-9_-]+$/.test(encoded)) {
47
+ throw contextError("Profile re-pin context must be bounded base64url text");
48
+ }
49
+ let raw;
50
+ try {
51
+ const decoded = Buffer.from(encoded, "base64url");
52
+ if (decoded.toString("base64url") !== encoded) {
53
+ throw new Error("non-canonical base64url");
54
+ }
55
+ raw = JSON.parse(decoded.toString("utf8"));
56
+ }
57
+ catch {
58
+ throw contextError("Profile re-pin context is not valid base64url JSON");
59
+ }
60
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
61
+ throw contextError("Profile re-pin context must be a JSON object");
62
+ }
63
+ const record = raw;
64
+ if (Object.keys(record).some((key) => !["schema_version", "lane_id", "expected_senior", "actor", "reason"].includes(key))) {
65
+ throw contextError("Profile re-pin context contains unknown fields");
66
+ }
67
+ const expectedSenior = parseSenior(record.expected_senior);
68
+ if (record.schema_version !== PROFILE_REPIN_CONTEXT_SCHEMA ||
69
+ record.lane_id !== expectedLaneId ||
70
+ expectedSenior === null ||
71
+ typeof record.actor !== "string" ||
72
+ !record.actor.trim() ||
73
+ record.reason !== "resume --worker") {
74
+ throw contextError(`Profile re-pin context is invalid for lane ${expectedLaneId}`);
75
+ }
76
+ return {
77
+ schema_version: PROFILE_REPIN_CONTEXT_SCHEMA,
78
+ lane_id: expectedLaneId,
79
+ expected_senior: expectedSenior,
80
+ actor: record.actor,
81
+ reason: "resume --worker",
82
+ };
83
+ }
84
+ //# sourceMappingURL=outpost-profile-repin.js.map
@@ -562,6 +562,29 @@ export function parseLaneRecord(raw, filePath) {
562
562
  path: filePath,
563
563
  });
564
564
  }
565
+ if (raw.worker_profile_repin_history !== undefined) {
566
+ if (!Array.isArray(raw.worker_profile_repin_history)) {
567
+ throw new LaneSchemaError("worker_profile_repin_history must be an array when present", {
568
+ path: filePath,
569
+ field: "worker_profile_repin_history",
570
+ });
571
+ }
572
+ for (const [index, entry] of raw.worker_profile_repin_history.entries()) {
573
+ const field = `worker_profile_repin_history[${index}]`;
574
+ if (!isPlainObject(entry) || Object.keys(entry).some((key) => !["old_hash", "new_hash", "actor", "at", "reason"].includes(key))) {
575
+ throw new LaneSchemaError(`${field} must be an audit entry`, { path: filePath, field });
576
+ }
577
+ if (entry.old_hash !== null && (typeof entry.old_hash !== "string" || !entry.old_hash.trim())) {
578
+ throw new LaneSchemaError(`${field}.old_hash must be a non-empty string or null`, { path: filePath, field });
579
+ }
580
+ for (const key of ["new_hash", "actor", "reason"]) {
581
+ if (typeof entry[key] !== "string" || !entry[key].trim()) {
582
+ throw new LaneSchemaError(`${field}.${key} must be a non-empty string`, { path: filePath, field });
583
+ }
584
+ }
585
+ requireTimestamp(entry.at, `${field}.at`, filePath);
586
+ }
587
+ }
565
588
  if (raw.account_selection !== undefined) {
566
589
  parseLaneAccountSelection(raw.account_selection, filePath);
567
590
  }
@@ -853,6 +876,11 @@ export function mergeLaneRecords(live, sidecars) {
853
876
  account_history: unionHistory(all.map((record) => record.account_history ?? []), entryIdentity, base.account_history ?? []),
854
877
  }
855
878
  : {}),
879
+ ...(all.some((record) => record.worker_profile_repin_history !== undefined)
880
+ ? {
881
+ worker_profile_repin_history: unionHistory(all.map((record) => record.worker_profile_repin_history ?? []), entryIdentity, base.worker_profile_repin_history ?? []),
882
+ }
883
+ : {}),
856
884
  stories: mergeLaneStories(all, base),
857
885
  };
858
886
  }
@@ -283,6 +283,14 @@ export interface SeniorHistoryEntry {
283
283
  by: string;
284
284
  reason: string;
285
285
  }
286
+ /** Evidence for a senior-authorized Outpost worker-profile content re-pin. */
287
+ export interface WorkerProfileRepinHistoryEntry {
288
+ old_hash: string | null;
289
+ new_hash: string;
290
+ actor: string;
291
+ at: string;
292
+ reason: string;
293
+ }
286
294
  export interface AttributionHistoryEntry {
287
295
  project_id: string;
288
296
  story_id: string;
@@ -372,6 +380,8 @@ export interface LaneRecord {
372
380
  hire_grant?: number;
373
381
  /** Content hash of the profile last applied to this lane. */
374
382
  worker_profile_hash?: string;
383
+ /** Bounded audit history of authorized Outpost profile hash re-pins. */
384
+ worker_profile_repin_history?: WorkerProfileRepinHistoryEntry[];
375
385
  /** Profile fields intentionally unsupported by the selected provider. */
376
386
  worker_profile_ignored?: string[];
377
387
  /** How policy files were delivered on the most recent profile spawn. */
@@ -223,7 +223,7 @@ export const LANE_FIELD_WRITERS = {
223
223
  state: ["creator", "command", "watcher", "reconcile", "spawn"], queued_reason: ["creator", "command", "spawn", "watcher", "reconcile"], queued_at: ["creator", "command", "spawn", "watcher", "reconcile"], admission_reserved_at: ["creator", "command", "reconcile", "spawn", "watcher"],
224
224
  pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
225
225
  pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
226
- priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
226
+ priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
227
227
  keep_alive: ["creator", "command"], keep_alive_opted_in_at: ["creator", "command"], keep_alive_blocked_reason: ["command", "reconcile"], keep_alive_resume_count: ["command", "reconcile"], keep_alive_last_resume_at: ["command", "reconcile"], keep_alive_next_eligible_at: ["command", "reconcile"], keep_alive_exhausted_at: ["command", "reconcile"],
228
228
  provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
229
229
  version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.229.0",
3
+ "version": "5.229.1",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {