@indigoai-us/hq-cli 5.228.0 → 5.228.2

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,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.228.2] — 2026-09-27
6
+
7
+ ## [5.228.1] — 2026-09-27
8
+
9
+ ### Fixed
10
+
11
+ - Reparenting an Outpost lane from the primary now dispatches to the owner,
12
+ where the hierarchy, tenant, depth, and enabled hire-grant checks are repeated.
13
+ The primary syncs the changed lane and each descendant whose depth changed.
14
+ `hq lanes show <lane-id>` refreshes that Outpost lane and its descendants
15
+ from the owner, including senior history, depth, and grants. Lane sessions
16
+ now share their lane's depth, and an SSM owner dispatch can change a
17
+ user-senior lane's grant without allowing a lane caller to impersonate the
18
+ user.
19
+
5
20
  ## [5.228.0] — 2026-09-27
6
21
 
7
22
  ### 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")
@@ -3,6 +3,13 @@ interface CachedWorkerMode {
3
3
  version: number;
4
4
  value: boolean;
5
5
  }
6
+ export type FlagRegistryWorkerModeCacheOperation = "read" | "write";
7
+ export type FlagRegistryWorkerModeCacheStep = "mkdir" | "write_temp" | "link" | "unlink" | "prune" | "readdir" | "read";
8
+ /** Build bounded filesystem diagnostics without exposing the state directory. */
9
+ export declare function flagRegistryWorkerModeCacheFailureContext(error: unknown, operation: FlagRegistryWorkerModeCacheOperation, filePath: string, env?: NodeJS.ProcessEnv): {
10
+ exception: unknown;
11
+ tags: Record<string, string>;
12
+ };
6
13
  /** Keep the last server snapshot decision for this one startup-mode flag. */
7
14
  export declare function flagRegistryWorkerModeCachePath(stateDir: string, scopeIdentifiers: readonly string[]): string;
8
15
  /** A missing, invalid, or inaccessible cache never enables the rollout. */
@@ -1,8 +1,116 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import * as fs from "node:fs";
3
4
  import * as os from "node:os";
4
5
  import * as path from "node:path";
5
6
  export const FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG = "cli.flag-worker-self-contained";
7
+ class FlagRegistryWorkerModeCacheStepError extends Error {
8
+ cacheStep;
9
+ originalError;
10
+ constructor(cacheStep, originalError) {
11
+ super(`Flag registry worker-mode cache failed at ${cacheStep}`);
12
+ this.cacheStep = cacheStep;
13
+ this.originalError = originalError;
14
+ this.name = "FlagRegistryWorkerModeCacheStepError";
15
+ }
16
+ }
17
+ const KNOWN_ERRNOS = new Set([
18
+ "EACCES",
19
+ "EBUSY",
20
+ "EEXIST",
21
+ "EIO",
22
+ "EISDIR",
23
+ "EMFILE",
24
+ "ENFILE",
25
+ "ENOENT",
26
+ "ENOSPC",
27
+ "ENOTDIR",
28
+ "EPERM",
29
+ "EROFS",
30
+ ]);
31
+ function withCacheFsStep(cacheStep, operation) {
32
+ try {
33
+ return operation();
34
+ }
35
+ catch (error) {
36
+ if (error instanceof FlagRegistryWorkerModeCacheStepError)
37
+ throw error;
38
+ throw new FlagRegistryWorkerModeCacheStepError(cacheStep, error);
39
+ }
40
+ }
41
+ function originalCacheError(error) {
42
+ return error instanceof FlagRegistryWorkerModeCacheStepError
43
+ ? error.originalError
44
+ : error;
45
+ }
46
+ function cacheErrorCode(error) {
47
+ const original = originalCacheError(error);
48
+ return typeof original === "object" && original !== null && "code" in original
49
+ ? original.code
50
+ : undefined;
51
+ }
52
+ function directoryHasUchg(directory) {
53
+ if (process.platform !== "darwin")
54
+ return "unknown";
55
+ try {
56
+ const rawFlags = execFileSync("/usr/bin/stat", ["-f", "%f", directory], { timeout: 1000 }).toString("utf8").trim();
57
+ if (!/^\d+$/.test(rawFlags))
58
+ return "unknown";
59
+ const flags = Number(rawFlags);
60
+ if (!Number.isSafeInteger(flags))
61
+ return "unknown";
62
+ return String((flags & 0x00000002) !== 0 || (flags & 0x00020000) !== 0);
63
+ }
64
+ catch {
65
+ // This bounded diagnostic is best-effort; preserve unknown and the original cache error.
66
+ return "unknown";
67
+ }
68
+ }
69
+ /** Build bounded filesystem diagnostics without exposing the state directory. */
70
+ export function flagRegistryWorkerModeCacheFailureContext(error, operation, filePath, env = process.env) {
71
+ const cacheError = error instanceof FlagRegistryWorkerModeCacheStepError
72
+ ? error
73
+ : undefined;
74
+ const directory = path.dirname(filePath);
75
+ let dirExists = "unknown";
76
+ let stats;
77
+ try {
78
+ stats = fs.statSync(directory);
79
+ dirExists = "true";
80
+ }
81
+ catch (metadataError) {
82
+ if (cacheErrorCode(metadataError) === "ENOENT")
83
+ dirExists = "false";
84
+ // Keep inaccessible metadata unknown; diagnostics must not hide the cache error.
85
+ }
86
+ const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
87
+ const ownerMatches = typeof uid === "number" && typeof stats?.uid === "number"
88
+ ? String(stats.uid === uid)
89
+ : "unknown";
90
+ const dirMode = typeof stats?.mode === "number"
91
+ ? (stats.mode & 0o777).toString(8).padStart(3, "0")
92
+ : "unknown";
93
+ const dirHasUchg = directoryHasUchg(directory);
94
+ const rawErrno = cacheErrorCode(error);
95
+ const errno = typeof rawErrno === "string" && KNOWN_ERRNOS.has(rawErrno)
96
+ ? rawErrno
97
+ : "UNKNOWN";
98
+ const stateDirSource = env.HQ_STATE_DIR?.trim() ? "env" : "home";
99
+ return {
100
+ exception: originalCacheError(error),
101
+ tags: {
102
+ cache_op: operation,
103
+ cache_step: cacheError?.cacheStep ?? (operation === "read" ? "read" : "write_temp"),
104
+ errno,
105
+ state_dir_source: stateDirSource,
106
+ dir_exists: dirExists,
107
+ dir_owner_matches_uid: ownerMatches,
108
+ dir_mode: dirMode,
109
+ dir_has_uchg: dirHasUchg,
110
+ cache_path_kind: "<stateDir>/flags/<basename>",
111
+ },
112
+ };
113
+ }
6
114
  /** Keep the last server snapshot decision for this one startup-mode flag. */
7
115
  export function flagRegistryWorkerModeCachePath(stateDir, scopeIdentifiers) {
8
116
  const basename = scopeIdentifiers.length === 0
@@ -17,11 +125,11 @@ export function flagRegistryWorkerModeCachePath(stateDir, scopeIdentifiers) {
17
125
  export function readCachedFlagRegistryWorkerMode(filePath) {
18
126
  let latest = readCacheRecordIfPresent(filePath);
19
127
  const directory = path.dirname(filePath);
20
- if (!fs.existsSync(directory))
128
+ if (!withCacheFsStep("read", () => fs.existsSync(directory)))
21
129
  return latest;
22
130
  const basename = path.basename(filePath);
23
131
  const versionedPrefix = `${basename}.v`;
24
- for (const entry of fs.readdirSync(directory)) {
132
+ for (const entry of withCacheFsStep("readdir", () => fs.readdirSync(directory))) {
25
133
  if (!entry.startsWith(versionedPrefix) || !entry.endsWith(".json"))
26
134
  continue;
27
135
  const versionedPath = path.join(directory, entry);
@@ -32,7 +140,7 @@ export function readCachedFlagRegistryWorkerMode(filePath) {
32
140
  return latest;
33
141
  }
34
142
  export function writeCachedFlagRegistryWorkerMode(filePath, snapshot) {
35
- fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
143
+ withCacheFsStep("mkdir", () => fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 }));
36
144
  const existing = readCachedFlagRegistryWorkerMode(filePath);
37
145
  if (existing && snapshot.version < existing.version)
38
146
  return;
@@ -42,26 +150,27 @@ export function writeCachedFlagRegistryWorkerMode(filePath, snapshot) {
42
150
  const versionedPath = `${filePath}.v${snapshot.version}.json`;
43
151
  const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
44
152
  try {
45
- fs.writeFileSync(tempPath, `${JSON.stringify(snapshot)}\n`, {
153
+ withCacheFsStep("write_temp", () => fs.writeFileSync(tempPath, `${JSON.stringify(snapshot)}\n`, {
46
154
  encoding: "utf8",
47
155
  mode: 0o600,
48
156
  flag: "wx",
49
- });
157
+ }));
50
158
  try {
51
- fs.linkSync(tempPath, versionedPath);
159
+ withCacheFsStep("link", () => fs.linkSync(tempPath, versionedPath));
52
160
  }
53
161
  catch (error) {
54
162
  if (!isAlreadyExistsError(error))
55
163
  throw error;
56
164
  const existingVersion = readCacheRecord(versionedPath);
57
165
  if (existingVersion.value !== snapshot.value) {
58
- throw new Error("Conflicting flag-registry worker cache values share a version");
166
+ throw new FlagRegistryWorkerModeCacheStepError("link", new Error("Conflicting flag-registry worker cache values share a version"));
59
167
  }
60
168
  }
61
169
  }
62
170
  finally {
63
- if (fs.existsSync(tempPath))
64
- fs.unlinkSync(tempPath);
171
+ if (withCacheFsStep("unlink", () => fs.existsSync(tempPath))) {
172
+ withCacheFsStep("unlink", () => fs.unlinkSync(tempPath));
173
+ }
65
174
  }
66
175
  pruneOlderCacheRecords(filePath);
67
176
  }
@@ -76,7 +185,8 @@ function isCachedWorkerMode(value) {
76
185
  "value" in value && typeof value.value === "boolean";
77
186
  }
78
187
  function readCacheRecord(filePath) {
79
- const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
188
+ const contents = withCacheFsStep("read", () => fs.readFileSync(filePath, "utf8"));
189
+ const value = JSON.parse(contents);
80
190
  if (!isCachedWorkerMode(value)) {
81
191
  throw new Error("Cached flag-registry worker mode has an invalid shape");
82
192
  }
@@ -97,14 +207,14 @@ function pruneOlderCacheRecords(filePath) {
97
207
  const directory = path.dirname(filePath);
98
208
  const basename = path.basename(filePath);
99
209
  const versionedPrefix = `${basename}.v`;
100
- for (const entry of fs.readdirSync(directory)) {
210
+ for (const entry of withCacheFsStep("readdir", () => fs.readdirSync(directory))) {
101
211
  if (!entry.startsWith(versionedPrefix) || !entry.endsWith(".json"))
102
212
  continue;
103
213
  const recordPath = path.join(directory, entry);
104
214
  const record = readCacheRecordIfPresent(recordPath);
105
215
  if (record && record.version < latest.version) {
106
216
  try {
107
- fs.unlinkSync(recordPath);
217
+ withCacheFsStep("prune", () => fs.unlinkSync(recordPath));
108
218
  }
109
219
  catch (error) {
110
220
  if (!isNotFoundError(error))
@@ -112,11 +222,11 @@ function pruneOlderCacheRecords(filePath) {
112
222
  }
113
223
  }
114
224
  }
115
- if (fs.existsSync(filePath)) {
225
+ if (withCacheFsStep("prune", () => fs.existsSync(filePath))) {
116
226
  const legacyRecord = readCacheRecordIfPresent(filePath);
117
227
  if (legacyRecord && legacyRecord.version < latest.version) {
118
228
  try {
119
- fs.unlinkSync(filePath);
229
+ withCacheFsStep("prune", () => fs.unlinkSync(filePath));
120
230
  }
121
231
  catch (error) {
122
232
  if (!isNotFoundError(error))
@@ -136,11 +246,9 @@ function readCacheRecordIfPresent(filePath) {
136
246
  }
137
247
  }
138
248
  function isAlreadyExistsError(error) {
139
- return typeof error === "object" && error !== null &&
140
- "code" in error && error.code === "EEXIST";
249
+ return cacheErrorCode(error) === "EEXIST";
141
250
  }
142
251
  function isNotFoundError(error) {
143
- return typeof error === "object" && error !== null &&
144
- "code" in error && error.code === "ENOENT";
252
+ return cacheErrorCode(error) === "ENOENT";
145
253
  }
146
254
  //# sourceMappingURL=flag-registry-worker-mode-cache.js.map
@@ -14,7 +14,7 @@ import { workContextRoot } from "./work-context/paths.js";
14
14
  import { readCompaniesManifestMap } from "./work-context/repo-remote.js";
15
15
  import { loadCachedTokens, resolveDefaultHqRoot } from "../utils/cognito-session.js";
16
16
  import { resolveFlagRegistryEndpoint, } from "./flag-registry-endpoint.js";
17
- import { FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG, flagRegistryStateDir, flagRegistryWorkerModeCachePath, readCachedFlagRegistryWorkerMode, writeCachedFlagRegistryWorkerMode, } from "./flag-registry-worker-mode-cache.js";
17
+ import { FLAG_REGISTRY_SELF_CONTAINED_WORKER_FLAG, flagRegistryWorkerModeCacheFailureContext, flagRegistryStateDir, flagRegistryWorkerModeCachePath, readCachedFlagRegistryWorkerMode, writeCachedFlagRegistryWorkerMode, } from "./flag-registry-worker-mode-cache.js";
18
18
  import { FLAG_REGISTRY_WORKER_SOURCE } from "./flag-registry-worker-source.generated.js";
19
19
  export { FLAG_REGISTRY_ENDPOINT_ENV } from "./flag-registry-endpoint.js";
20
20
  const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
@@ -140,8 +140,13 @@ function selfContainedWorkerEnabled(company) {
140
140
  return readCachedFlagRegistryWorkerMode(filePath)?.value ?? false;
141
141
  }
142
142
  catch (error) {
143
- Sentry.captureException(error, {
144
- tags: { component: "flag-registry-worker-mode-cache", worker: "flag-registry" },
143
+ const failure = flagRegistryWorkerModeCacheFailureContext(error, "read", filePath);
144
+ Sentry.captureException(failure.exception, {
145
+ tags: {
146
+ component: "flag-registry-worker-mode-cache",
147
+ worker: "flag-registry",
148
+ ...failure.tags,
149
+ },
145
150
  });
146
151
  return false;
147
152
  }
@@ -165,8 +170,14 @@ function cacheWorkerModeFromSnapshot(snapshot, company) {
165
170
  writeCachedFlagRegistryWorkerMode(flagRegistryWorkerModeCachePath(flagRegistryStateDir(), scopes), { version: snapshot.version, value: decision.value });
166
171
  }
167
172
  catch (error) {
168
- Sentry.captureException(error, {
169
- tags: { component: "flag-registry-worker-mode-cache", worker: "flag-registry" },
173
+ const filePath = flagRegistryWorkerModeCachePath(flagRegistryStateDir(), scopes);
174
+ const failure = flagRegistryWorkerModeCacheFailureContext(error, "write", filePath);
175
+ Sentry.captureException(failure.exception, {
176
+ tags: {
177
+ component: "flag-registry-worker-mode-cache",
178
+ worker: "flag-registry",
179
+ ...failure.tags,
180
+ },
170
181
  });
171
182
  }
172
183
  }
@@ -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.2",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {