@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 +15 -0
- package/dist/command-catalog.generated.d.ts +22 -0
- package/dist/command-catalog.generated.js +29 -0
- package/dist/commands/lanes.js +169 -13
- package/dist/lib/flag-registry-worker-mode-cache.d.ts +7 -0
- package/dist/lib/flag-registry-worker-mode-cache.js +126 -18
- package/dist/lib/flag-registry.js +16 -5
- package/dist/lib/lanes/outpost-dispatch.d.ts +3 -1
- package/dist/lib/lanes/outpost-dispatch.js +2 -0
- package/package.json +1 -1
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": "",
|
package/dist/commands/lanes.js
CHANGED
|
@@ -1668,7 +1668,11 @@ function depthOfRef(hqRoot, ref, ctx, visited = new Set(), override) {
|
|
|
1668
1668
|
senior: ref,
|
|
1669
1669
|
});
|
|
1670
1670
|
}
|
|
1671
|
-
|
|
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"
|
|
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"
|
|
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
|
-
:
|
|
3694
|
-
remote.
|
|
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
|
|
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({
|
|
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
|
-
|
|
7045
|
-
ok: true,
|
|
7046
|
-
action: "reparent",
|
|
7191
|
+
return {
|
|
7047
7192
|
lane: updated,
|
|
7048
|
-
|
|
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
|
|
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
|
|
140
|
-
"code" in error && error.code === "EEXIST";
|
|
249
|
+
return cacheErrorCode(error) === "EEXIST";
|
|
141
250
|
}
|
|
142
251
|
function isNotFoundError(error) {
|
|
143
|
-
return
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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);
|