@indigoai-us/hq-cli 5.228.0 → 5.228.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,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.228.1] — 2026-09-27
6
+
7
+ ### Fixed
8
+
9
+ - Reparenting an Outpost lane from the primary now dispatches to the owner,
10
+ where the hierarchy, tenant, depth, and enabled hire-grant checks are repeated.
11
+ The primary syncs the changed lane and each descendant whose depth changed.
12
+ `hq lanes show <lane-id>` refreshes that Outpost lane and its descendants
13
+ from the owner, including senior history, depth, and grants. Lane sessions
14
+ now share their lane's depth, and an SSM owner dispatch can change a
15
+ user-senior lane's grant without allowing a lane caller to impersonate the
16
+ user.
17
+
5
18
  ## [5.228.0] — 2026-09-27
6
19
 
7
20
  ### Changed
@@ -7480,6 +7480,28 @@ export declare const COMMAND_CATALOG: readonly [{
7480
7480
  readonly description: "JSON on stdout (always)";
7481
7481
  }];
7482
7482
  readonly subcommands: readonly [];
7483
+ }, {
7484
+ readonly name: "reparent-target";
7485
+ readonly description: "";
7486
+ readonly aliases: readonly [];
7487
+ readonly hidden: true;
7488
+ readonly usage: "[options] <lane-id>";
7489
+ readonly arguments: readonly [{
7490
+ readonly name: "lane-id";
7491
+ readonly required: true;
7492
+ readonly variadic: false;
7493
+ }];
7494
+ readonly options: readonly [{
7495
+ readonly flags: "--senior <ref>";
7496
+ readonly description: "New senior ref";
7497
+ }, {
7498
+ readonly flags: "--reason <text>";
7499
+ readonly description: "Why the lane is being re-parented";
7500
+ }, {
7501
+ readonly flags: "--json";
7502
+ readonly description: "JSON on stdout (always)";
7503
+ }];
7504
+ readonly subcommands: readonly [];
7483
7505
  }, {
7484
7506
  readonly name: "grant-target";
7485
7507
  readonly description: "";
@@ -9670,6 +9670,35 @@ export const COMMAND_CATALOG = [
9670
9670
  ],
9671
9671
  "subcommands": []
9672
9672
  },
9673
+ {
9674
+ "name": "reparent-target",
9675
+ "description": "",
9676
+ "aliases": [],
9677
+ "hidden": true,
9678
+ "usage": "[options] <lane-id>",
9679
+ "arguments": [
9680
+ {
9681
+ "name": "lane-id",
9682
+ "required": true,
9683
+ "variadic": false
9684
+ }
9685
+ ],
9686
+ "options": [
9687
+ {
9688
+ "flags": "--senior <ref>",
9689
+ "description": "New senior ref"
9690
+ },
9691
+ {
9692
+ "flags": "--reason <text>",
9693
+ "description": "Why the lane is being re-parented"
9694
+ },
9695
+ {
9696
+ "flags": "--json",
9697
+ "description": "JSON on stdout (always)"
9698
+ }
9699
+ ],
9700
+ "subcommands": []
9701
+ },
9673
9702
  {
9674
9703
  "name": "grant-target",
9675
9704
  "description": "",
@@ -1668,7 +1668,11 @@ function depthOfRef(hqRoot, ref, ctx, visited = new Set(), override) {
1668
1668
  senior: ref,
1669
1669
  });
1670
1670
  }
1671
- return depthOfRef(hqRoot, seniorOfSession(hqRoot, ref.id, ctx), ctx, visited, override) + 1;
1671
+ const sessionSenior = seniorOfSession(hqRoot, ref.id, ctx);
1672
+ const seniorDepth = depthOfRef(hqRoot, sessionSenior, ctx, visited, override);
1673
+ // A lane's execution session shares the lane's depth. Other sessions are
1674
+ // their own authority hop, including user-owned and nested sessions.
1675
+ return seniorDepth + (sessionSenior.kind === "lane" ? 0 : 1);
1672
1676
  }
1673
1677
  if (!ref.id) {
1674
1678
  throw new LanesError("invalid_senior_ref", "Lane senior is missing an id", {
@@ -3617,9 +3621,15 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3617
3621
  dispatched = await ctx.dispatchOutpostExec({
3618
3622
  outpostId,
3619
3623
  command: buildOutpostLaneCommand({
3620
- action: action === "grant" ? "grant-target" : action,
3624
+ action: action === "grant"
3625
+ ? "grant-target"
3626
+ : action === "reparent"
3627
+ ? "reparent-target"
3628
+ : action,
3621
3629
  laneId: lane.lane_id,
3622
- hqBinary: action === "grant" ? outpostCliOverride(ctx, outpostId) : undefined,
3630
+ hqBinary: action === "grant" || action === "reparent"
3631
+ ? outpostCliOverride(ctx, outpostId)
3632
+ : undefined,
3623
3633
  text: options.text,
3624
3634
  project: options.project,
3625
3635
  story: options.story,
@@ -3629,6 +3639,8 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3629
3639
  worker: options.worker,
3630
3640
  keepAlive: options.keepAlive,
3631
3641
  hire: options.hire,
3642
+ senior: options.senior,
3643
+ reason: options.reason,
3632
3644
  grantContext,
3633
3645
  compact: options.compact,
3634
3646
  }),
@@ -3682,6 +3694,9 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3682
3694
  owner_error: ownerErrorFromOutput(dispatched.stdout),
3683
3695
  });
3684
3696
  }
3697
+ const changedDescendantIds = action === "reparent"
3698
+ ? parseReparentDescendantIds(remote?.changed_descendant_ids, lane.lane_id, outpostId)
3699
+ : [];
3685
3700
  const resultConfirmed = remote !== null &&
3686
3701
  remote.ok === true &&
3687
3702
  remote.action === action &&
@@ -3690,8 +3705,11 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3690
3705
  ? remote.accepted === true
3691
3706
  : action === "grant"
3692
3707
  ? remote.hire_grant === Number(options.hire)
3693
- : remote.delivery_status === "worker_spawned" ||
3694
- remote.delivery_status === "queued_on_target_account");
3708
+ : action === "reparent"
3709
+ ? remote.lane !== null && typeof remote.lane === "object" &&
3710
+ Array.isArray(remote.changed_descendant_ids)
3711
+ : remote.delivery_status === "worker_spawned" ||
3712
+ remote.delivery_status === "queued_on_target_account");
3695
3713
  if (!resultConfirmed) {
3696
3714
  throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without a confirmed ${action} result for lane ${lane.lane_id}`, {
3697
3715
  lane_id: lane.lane_id,
@@ -3712,6 +3730,25 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3712
3730
  });
3713
3731
  return;
3714
3732
  }
3733
+ if (action === "reparent") {
3734
+ const ownerLane = syncLaneRecordFromOwner(ctx.hqRoot(), lane.lane_id, outpostId, remote.lane, undefined, { allowNonTerminal: true });
3735
+ 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 }));
3739
+ }
3740
+ printJson({
3741
+ ...remote,
3742
+ lane_id: ownerLane.lane_id,
3743
+ lane: ownerLane,
3744
+ changed_descendant_ids: changedDescendantIds,
3745
+ descendants,
3746
+ outpost_id: outpostId,
3747
+ outpost_command_id: dispatched.commandId,
3748
+ outpost_transport: "outposts_exec",
3749
+ });
3750
+ return;
3751
+ }
3715
3752
  printJson({
3716
3753
  ...remote,
3717
3754
  outpost_id: outpostId,
@@ -3719,6 +3756,42 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
3719
3756
  outpost_transport: "outposts_exec",
3720
3757
  });
3721
3758
  }
3759
+ function parseReparentDescendantIds(value, laneId, outpostId) {
3760
+ if (!Array.isArray(value) || value.some((id) => typeof id !== "string" || id.trim() === "")) {
3761
+ throw new LanesError("outpost_reparent_result_invalid", `Outpost ${outpostId} returned an invalid descendant list for lane ${laneId}`, { lane_id: laneId, outpost_id: outpostId });
3762
+ }
3763
+ const ids = value;
3764
+ if (new Set(ids).size !== ids.length || ids.includes(laneId)) {
3765
+ throw new LanesError("outpost_reparent_result_invalid", `Outpost ${outpostId} returned duplicate or self-referential descendants for lane ${laneId}`, { lane_id: laneId, outpost_id: outpostId, descendant_lane_ids: ids });
3766
+ }
3767
+ return ids;
3768
+ }
3769
+ async function readOutpostLaneSnapshot(ctx, outpostId, laneId) {
3770
+ let dispatched;
3771
+ try {
3772
+ dispatched = await ctx.dispatchOutpostExec({
3773
+ outpostId,
3774
+ command: buildOutpostLaneCommand({
3775
+ action: "show",
3776
+ laneId,
3777
+ hqBinary: outpostCliOverride(ctx, outpostId),
3778
+ }),
3779
+ timeoutSeconds: OUTPOST_HIRE_SUMMARY_TIMEOUT_SECONDS,
3780
+ waitMs: OUTPOST_HIRE_SUMMARY_WAIT_MS,
3781
+ });
3782
+ }
3783
+ catch (err) {
3784
+ throw new LanesError("outpost_lane_sync_unavailable", `Could not refresh descendant lane ${laneId} from Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: laneId, outpost_id: outpostId });
3785
+ }
3786
+ if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
3787
+ throw new LanesError("outpost_lane_sync_unavailable", `Outpost ${outpostId} did not confirm the snapshot for descendant lane ${laneId}`, { lane_id: laneId, outpost_id: outpostId, command_id: dispatched.commandId });
3788
+ }
3789
+ const remote = parseRemoteJson(dispatched.stdout, "Outpost descendant lane snapshot");
3790
+ if (!remote || remote.ok !== true || remote.action !== "show" || remote.lane === undefined) {
3791
+ throw new LanesError("outpost_lane_sync_invalid", `Outpost ${outpostId} returned an invalid snapshot for descendant lane ${laneId}`, { lane_id: laneId, outpost_id: outpostId, command_id: dispatched.commandId });
3792
+ }
3793
+ return remote.lane;
3794
+ }
3722
3795
  async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
3723
3796
  if (lane.host.kind !== "outpost") {
3724
3797
  throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to answer its question`, { lane_id: lane.lane_id, question_id: questionId });
@@ -4717,9 +4790,38 @@ async function runMoveTarget(ctx, laneId, opts) {
4717
4790
  account: moved.account ?? null,
4718
4791
  });
4719
4792
  }
4793
+ async function refreshOutpostLaneCopiesForShow(ctx, lane, localLanes) {
4794
+ if (lane.host.kind !== "outpost" || laneRunsHere(lane, reconcileHostFromCtx(ctx))) {
4795
+ return lane;
4796
+ }
4797
+ const outpostId = lane.host.id;
4798
+ const descendantIds = localLanes
4799
+ .filter((candidate) => candidate.lane_id !== lane.lane_id &&
4800
+ candidate.host.kind === "outpost" && candidate.host.id === outpostId &&
4801
+ walkHitsLane(ctx.hqRoot(), candidate.senior, lane.lane_id, ctx))
4802
+ .map((candidate) => candidate.lane_id);
4803
+ const refresh = async (laneId) => {
4804
+ try {
4805
+ const ownerLane = await readOutpostLaneSnapshot(ctx, outpostId, laneId);
4806
+ return syncLaneRecordFromOwner(ctx.hqRoot(), laneId, outpostId, ownerLane, undefined, { allowNonTerminal: true });
4807
+ }
4808
+ catch (err) {
4809
+ const detail = isLanesError(err)
4810
+ ? `${err.error}: ${err.message}`
4811
+ : `outpost_lane_sync_unavailable: ${dispatchErrorMessage(err)}`;
4812
+ process.stderr.write(`hq lanes: owner snapshot refresh failed for lane ${laneId}: ${detail}\n`);
4813
+ return null;
4814
+ }
4815
+ };
4816
+ const refreshed = await mapWithConcurrency([lane.lane_id, ...descendantIds], OUTPOST_HIRE_SUMMARY_CONCURRENCY, refresh);
4817
+ const refreshedLane = refreshed[0] ?? null;
4818
+ return refreshedLane ?? lane;
4819
+ }
4720
4820
  async function runShow(ctx, laneId) {
4721
4821
  const hqRoot = ctx.hqRoot();
4722
- const storedLane = readLane(hqRoot, laneId);
4822
+ const initialLane = readLane(hqRoot, laneId);
4823
+ const initialLanes = loadAllLanes(hqRoot);
4824
+ const storedLane = await refreshOutpostLaneCopiesForShow(ctx, initialLane, initialLanes);
4723
4825
  const allLanes = loadAllLanes(hqRoot);
4724
4826
  const hire = await laneHirePresentation(ctx, storedLane, allLanes);
4725
4827
  const remoteQueued = storedLane.state === "queued" &&
@@ -6913,6 +7015,29 @@ function archiveResumeMessage(hqRoot, laneId, message) {
6913
7015
  fs.writeFileSync(path.join(dir, `${message.message_id}.resume.jsonl`), `${JSON.stringify(message)}\n`, "utf8");
6914
7016
  }
6915
7017
  async function runReparent(ctx, laneId, opts) {
7018
+ const hqRoot = ctx.hqRoot();
7019
+ if (!opts.senior) {
7020
+ throw new LanesError("missing_senior", "--senior is required", {});
7021
+ }
7022
+ const initial = readLane(hqRoot, laneId);
7023
+ if (initial.host.kind === "outpost" && !laneRunsHere(initial, reconcileHostFromCtx(ctx))) {
7024
+ await dispatchOutpostLaneCommand(ctx, initial, "reparent", {
7025
+ senior: opts.senior,
7026
+ reason: opts.reason,
7027
+ });
7028
+ return;
7029
+ }
7030
+ const result = await reparentOnOwner(ctx, laneId, opts);
7031
+ printJson({
7032
+ ok: true,
7033
+ action: "reparent",
7034
+ lane_id: result.lane.lane_id,
7035
+ lane: result.lane,
7036
+ pending_questions: result.pendingQuestions,
7037
+ changed_descendant_ids: result.changedDescendantIds,
7038
+ });
7039
+ }
7040
+ async function reparentOnOwner(ctx, laneId, opts) {
6916
7041
  const hqRoot = ctx.hqRoot();
6917
7042
  if (!opts.senior) {
6918
7043
  throw new LanesError("missing_senior", "--senior is required", {});
@@ -6925,7 +7050,25 @@ async function runReparent(ctx, laneId, opts) {
6925
7050
  ...(initial.senior.kind === "lane" && initial.senior.id ? [initial.senior.id] : []),
6926
7051
  ...(nextSenior.kind === "lane" && nextSenior.id ? [nextSenior.id] : []),
6927
7052
  ];
6928
- withLaneHireLocks(hqRoot, lockIds, () => runReparentLocked(ctx, laneId, opts));
7053
+ return withLaneHireLocks(hqRoot, lockIds, () => runReparentLocked(ctx, laneId, opts));
7054
+ }
7055
+ async function runReparentOwnerTarget(ctx, laneId, opts) {
7056
+ if (!ctx.isSsmOwnerDispatch()) {
7057
+ throw new LanesError("outpost_reparent_dispatch_unauthorized", "The Outpost reparent target is available only to an SSM Run Command dispatch", { lane_id: laneId });
7058
+ }
7059
+ const lane = readLane(ctx.hqRoot(), laneId);
7060
+ if (lane.host.kind !== "outpost" || !laneIsHostedHere(ctx, lane)) {
7061
+ throw new LanesError("lane_remote", `Outpost reparent target cannot mutate lane ${laneId} on this host`, { lane_id: laneId, host: lane.host });
7062
+ }
7063
+ const result = await reparentOnOwner(ctx, laneId, opts);
7064
+ printJson({
7065
+ ok: true,
7066
+ action: "reparent",
7067
+ lane_id: result.lane.lane_id,
7068
+ lane: result.lane,
7069
+ pending_questions: result.pendingQuestions,
7070
+ changed_descendant_ids: result.changedDescendantIds,
7071
+ });
6929
7072
  }
6930
7073
  function runReparentLocked(ctx, laneId, opts) {
6931
7074
  const hqRoot = ctx.hqRoot();
@@ -6996,7 +7139,11 @@ function runReparentLocked(ctx, laneId, opts) {
6996
7139
  to_depth: descendantDepth,
6997
7140
  });
6998
7141
  }
6999
- descendants.push({ laneId: other.lane_id, depth: descendantDepth });
7142
+ descendants.push({
7143
+ laneId: other.lane_id,
7144
+ depth: descendantDepth,
7145
+ changed: descendantDepth !== oldDescendantDepth,
7146
+ });
7000
7147
  }
7001
7148
  const at = ctx.now().toISOString();
7002
7149
  const updated = casUpdateLane(hqRoot, laneId, (current) => {
@@ -7041,12 +7188,13 @@ function runReparentLocked(ctx, laneId, opts) {
7041
7188
  };
7042
7189
  });
7043
7190
  }
7044
- printJson({
7045
- ok: true,
7046
- action: "reparent",
7191
+ return {
7047
7192
  lane: updated,
7048
- pending_questions: pending.length,
7049
- });
7193
+ pendingQuestions: pending.length,
7194
+ changedDescendantIds: descendants
7195
+ .filter((descendant) => descendant.changed)
7196
+ .map((descendant) => descendant.laneId),
7197
+ };
7050
7198
  }
7051
7199
  const CANONICAL_LOG_STREAMS = [
7052
7200
  "watcher.err",
@@ -8979,6 +9127,14 @@ export function registerLanesCommand(parent, deps = {}) {
8979
9127
  .action(wrap(async (opts) => {
8980
9128
  await runCreate(ctx, opts);
8981
9129
  }));
9130
+ lanes
9131
+ .command("reparent-target", { hidden: true })
9132
+ .argument("<lane-id>", "Lane id")
9133
+ .requiredOption("--senior <ref>", "New senior ref")
9134
+ .option("--reason <text>", "Why the lane is being re-parented")
9135
+ .action(wrap(async (laneId, opts) => {
9136
+ await runReparentOwnerTarget(ctx, laneId, opts);
9137
+ }));
8982
9138
  lanes
8983
9139
  .command("grant-target", { hidden: true })
8984
9140
  .argument("<lane-id>", "Lane id")
@@ -27,7 +27,7 @@ export interface OutpostDispatchResult {
27
27
  stderr: string;
28
28
  }
29
29
  export interface OutpostLaneCommand {
30
- action: "message" | "resume" | "stop" | "show" | "grant" | "grant-target";
30
+ action: "message" | "resume" | "stop" | "show" | "grant" | "grant-target" | "reparent-target";
31
31
  laneId: string;
32
32
  hqBinary?: string;
33
33
  text?: string;
@@ -40,6 +40,8 @@ export interface OutpostLaneCommand {
40
40
  keepAlive?: boolean;
41
41
  hire?: string;
42
42
  grantContext?: string;
43
+ senior?: string;
44
+ reason?: string;
43
45
  compact?: boolean;
44
46
  }
45
47
  export declare function buildOutpostQuestionAnswerCommand(input: {
@@ -150,6 +150,8 @@ export function buildOutpostLaneCommand(input) {
150
150
  ["--worker", input.worker],
151
151
  ["--hire", input.hire],
152
152
  ["--grant-context", input.grantContext],
153
+ ["--senior", input.senior],
154
+ ["--reason", input.reason],
153
155
  ]) {
154
156
  if (value !== undefined)
155
157
  args.push(flag, value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.228.0",
3
+ "version": "5.228.1",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {